本文目录导读:

对于PHP项目的滚动发布(Rolling Update),核心目标是在不中断服务的前提下,逐步替换集群中的节点,由于PHP本身是无状态的(不存储Session在本地),这比Java等有状态应用要简单得多。
下面是一个标准的、生产级的PHP项目分批更新集群节点的方案。
核心策略:先摘流,后更新,再入流
假设你有一个负载均衡器(如Nginx, HAProxy, AWS ALB)后面挂了N个PHP-FPM节点。
第一阶段:准备(代码与配置)
- 代码仓库:确保最新代码已合并到部署分支(如
release或main)。 - 构建产物:
- 强烈建议进行本地构建(如Composer安装、前端打包、生成Symfony缓存预热等),然后只将构建后的
dist/或vendor/文件夹同步到服务器,不要在服务器上直接composer install,这能显著减少更新窗口。
- 强烈建议进行本地构建(如Composer安装、前端打包、生成Symfony缓存预热等),然后只将构建后的
- 版本标签:为这次发布打上Git Tag(如
v2.3.1)。
第二阶段:分批更新(核心步骤)
方案A:基于负载均衡器摘流(推荐,最通用)
这是最标准、对用户影响最小的方式,步骤如下(以Nginx+PHP-FPM,4个节点为例):
-
节点分组:将4个节点分为2批(根据你的容忍度,可以是
1/4、1/2或1/3)。- Batch 1:web-node-01, web-node-02
- Batch 2:web-node-03, web-node-04
-
摘除Batch 1(Drain):
- 在负载均衡器(Nginx)或云服务控制台(如AWS ALB Target Group)中,将Batch 1的节点标记为“down”或从上游池移除。
- 关键操作:发送
SIGUSR2信号给PHP-FPM的master进程,让FPM优雅退出,这会告诉FPM停止接受新请求,但等待当前正在处理的请求完成(由pm.max_requests和process_control_timeout控制)。# 命令示例(根据系统配置调整路径) sudo kill -USR2 $(cat /var/run/php-fpm.pid)
- 等待:等待30-60秒(或监控FPM连接数降到0),确保该节点没有活跃请求。
-
更新Batch 1(Update):
- 将新代码(构建后的产物)部署到Batch 1的服务器上。
- 重新加载PHP-FPM(使用
SIGHUP或service php8.1-fpm reload),这会启动新的worker进程,加载新代码。 - 清除本地OPcache(
opcache_reset())或重启PHP-FPM(如果代码改动涉及核心扩展)。 - 快速验证:通过内网IP或修改本地Hosts文件,直接访问该节点的某个健康检查URL(如
/health.php),确保接口返回200且版本号正确。
-
重新接入Batch 1(Join):
- 在负载均衡器中,将Batch 1的节点重新标记为“up”,重新接受流量。
- 监控:观察错误率和延迟1-2分钟。
-
处理Batch 2:
重复步骤2-4,更新Batch 2。
方案B:基于Kubernetes(自动化)
如果你的集群是K8s,过程完全自动化,你只需:
- 更新Deployment的镜像Tag(或ConfigMap)。
- K8s会创建新的Pod(新代码)。
- Service的
readinessProbe会检查新Pod是否就绪。 - 默认策略是RollingUpdate,参数如:
spec: replicas: 4 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # 最大可超过期望副本数1个 maxUnavailable: 1 # 最大允许不可用副本数为1个 - 它会逐个(或按比例)替换Pod,直到所有都是新版本。
必须解决的3个关键问题
Session一致性问题
- 问题:用户登录后,Session文件存在于旧节点A,滚动更新后,用户请求被路由到新节点B。
- 解决方案(N选1):
- Redis/Memcached:将Session存储到集中式缓存中(标准方案)。
- Sticky Session:在负载均衡器开启Session亲和性(不推荐用于高可用)。
- 无状态JWT:完全不用Session,用Token。
数据库兼容性(最难的点)
- 问题:新代码需要数据库新增字段,而旧代码仍在运行。
- 解决方案:
- 向前兼容:新代码必须兼容旧数据库结构,即数据库迁移必须分为两步:
- Step 1:先执行
ADD COLUMN name VARCHAR(255) NULL(可空),新代码读写新字段,旧代码忽略它(没问题)。 - Step 2:等所有节点都更新为新版后,再执行
ALTER TABLE ... SET NOT NULL或删除旧字段。
- Step 1:先执行
- 金丝雀发布:只更新1个节点,让新代码运行一段时间,验证数据库没有死锁或慢查询。
- 向前兼容:新代码必须兼容旧数据库结构,即数据库迁移必须分为两步:
OPcache 和 代码缓存
- 问题:PHP代码被OPcache缓存,即使文件被替换,OPcache仍返回旧代码。
- 解决方案:
- 方案A(推荐):部署脚本末尾执行
touch命令修改opcache.revalidate_freq对应的文件时间戳,或直接重启PHP-FPM。service php8.1-fpm reload会触发OPcache重置。 - 方案B:使用
opcache_reset()函数,放在健康检查URL中。 - 方案C:在代码版本号中添加查询字符串,强制浏览器和OPcache不缓存?不行,OPcache基于文件路径缓存,不是URL。
- 方案A(推荐):部署脚本末尾执行
生产脚本示例(简化版)
#!/bin/bash
# 假定有一个负载均衡器API,通过 curl 操作
set -e
CLUSTER=("192.168.1.10" "192.168.1.11" "192.168.1.12" "192.168.1.13")
BATCH_SIZE=2 # 每次更新2台
for (( i=0; i<${#CLUSTER[@]}; i+=BATCH_SIZE )); do
BATCH=("${CLUSTER[@]:i:BATCH_SIZE}")
echo "正在更新批次: ${BATCH[*]}"
# 1. 摘流 (假设有一个API)
for node in "${BATCH[@]}"; do
curl -X POST "http://load-balancer-api:8080/drain?ip=$node" # 移除节点
# 2. 优雅关闭PHP-FPM
ssh deployer@$node "sudo kill -USR2 \$(cat /var/run/php8.1-fpm.pid)"
sleep 10 # 等待请求完成
done
# 3. 部署代码
for node in "${BATCH[@]}"; do
# rsync 新代码 (只同步变更文件,推荐)
rsync -avz --delete /path/to/new/code/ deployer@$node:/var/www/html/
# 或者解压tar包
# ssh deployer@$node "tar xzf /tmp/release.tgz -C /var/www/html"
done
# 4. 重载PHP (新worker加载新代码)
for node in "${BATCH[@]}"; do
ssh deployer@$node "sudo service php8.1-fpm reload"
# 验证健康
curl -f "http://$node:80/health.php" || exit 1
done
# 5. 重新入流
for node in "${BATCH[@]}"; do
curl -X POST "http://load-balancer-api:8080/join?ip=$node"
done
echo "批次完成,等待观察..."
sleep 30
done
echo "所有节点更新完成"
总结建议
| 场景 | 推荐策略 | 关键工具 |
|---|---|---|
| 物理机/VM + 手动 | 方案A:LB摘流 + 优雅退出 | Nginx Upstream, kill -USR2, rsync |
| Kubernetes | 使用K8s原生RollingUpdate | Deployment, ReadinessProbe |
| 云原生(AWS/GCP) | 使用自动伸缩组 + 启动配置/模板 | ASG Rolling Update, ALB Target Group |
底线:无论哪种方式,务必先在预发环境演练,特别是要测试数据库迁移的回滚脚本。