本文目录导读:

对于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 |
底线:无论哪种方式,务必先在预发环境演练,特别是要测试数据库迁移的回滚脚本。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播