滚动升级必须按节点分组、状态反馈驱动、超时熔断,不可仅靠并发数控制;否则易致半升级、雪崩或版本混用。

直接上结论:滚动升级不能靠“并发数”硬控,必须按节点分组 + 状态反馈驱动 + 超时熔断。否则容易出现半升级状态、服务雪崩或版本混用。
为什么单纯用 parallel 或 wait -n 做并发升级是危险的
很多脚本把所有目标节点丢进一个 for 循环,用后台子进程 + wait -n 控制并发数,看似省事,实际埋了三个雷:
- 节点间无依赖感知:A 节点升级失败,B 节点仍会继续,最终集群里混着新旧版本
- 健康检查被跳过:并发启动后只等进程退出,不验证服务是否真正就绪(比如端口开了但 /health 返回 503)
- 超时无兜底:某节点卡在下载包或 reload 阶段,整个升级流程就 hang 住,没人知道它卡在哪
必须用状态反馈驱动滚动节奏:每升完一个节点,必须确认它“真活了”才动下一个
核心逻辑不是“同时开 N 个 ssh”,而是“逐个推进,每个节点走完「停服 → 升级 → 启动 → 健康检查 → 等待就绪」闭环”。关键点:
- 健康检查必须三重验证:
curl -f --max-time 5 http://$node:8080/health优先;失败则查进程:ssh $node 'pgrep -f "app.name=myapi"';再失败才查端口:ssh $node 'nc -z 127.0.0.1 8080' - 每次检查失败要记录到
/var/log/upgrade/$node.log,并带时间戳和 exit code,不能只 echo 到终端 - 单节点超时设为 180 秒(含停服、解压、启动、健康等待),超时立即标记失败、跳过,不阻塞后续节点
- 升级前先做 dry-run 检查:确认目标路径可写、包存在、
systemctl is-active返回 expected 状态
分组控制 + 并发安全边界:避免资源打满又不失效率
全量串行太慢,全量并发太莽。折中方案是按拓扑或负载分组,每组内严格串行,组间允许并发:
- 例如 12 台节点,按机房分为 group-a(4台)、group-b(4台)、group-c(4台),最多允许 3 组并发,但每组内必须等前一台成功后再启下一台
- 实现上用临时锁文件 +
mkdir原子性判断:if mkdir "/tmp/upgrade_group_a.lock" 2>/dev/null; then ... fi,避免竞态 - 并发组数通过环境变量传入:
UPGRADE_GROUPS=${UPGRADE_GROUPS:-3},线上按 CPU/内存动态调 - 每组内执行时加
set -e -o pipefail,任何命令失败立刻中断当前组,不尝试“继续往下走”
回滚必须是原子操作,且与升级步骤一一镜像
升级脚本里写的 rollback.sh 不是“把旧包拷回去”就完事。它必须复刻升级的每一步反向动作:
- 停新服务 → 恢复旧二进制 → 替换配置(注意备份时是否带 timestamp)→ 启动旧服务 → 等待旧版健康检查通过
- 回滚触发条件明确写死:任一节点升级失败且
ROLLBACK_ON_FAIL=true时,自动对已升级节点执行回滚(不是对全部节点) - 回滚日志单独写入
/var/log/upgrade/rollback_$(date +%Y%m%d_%H%M%S).log,和升级日志隔离 - 禁止在升级中途手动删锁文件或 kill 进程——这会让回滚逻辑失去上下文,变成“猜上一次升到哪了”
最易被忽略的是健康检查的等待策略:不要写 while ! curl ...; do sleep 1; done,它没上限。必须用计数器或 timeout 命令包裹,比如 timeout 60s bash -c 'until curl -f --max-time 3 http://$node/health; do sleep 2; done'。否则一个节点网络抖动,整组就卡死。











