发布策略:蓝绿、灰度与回滚
发布是运维风险最集中的动作:代码、配置、数据库、缓存四类变更同时落地,用户流量还在持续进来。发布策略的选择本质上是在回答一个问题——出问题时,多久能回到已知可用的状态。本章给出四种策略的对照,并重点讲清蓝绿切换、权重灰度、数据库变更顺序与回滚触发条件。
要解决的问题
- 停机发布、滚动、蓝绿、灰度该怎么选?
- 蓝绿切换怎么做才能「瞬时完成、一键回滚」?
- 新版本只想让 5% 的用户用到,怎么实现?
- 数据库结构与代码的顺序搞错会怎样?
- 发布前要准备什么,发布后要看多久?
- 什么指标出现异常就必须立刻回滚?
四种策略对照
| 策略 | 停机时间 | 资源成本 | 回滚速度 | 适用场景 |
|---|---|---|---|---|
| 停机发布 | 分钟级 | 最低(单份资源) | 慢,需重新部署旧版本 | 内部系统、有明确夜间窗口、用户量极小 |
| 滚动发布 | 无 | 多一台的余量 | 中,再滚回去要几分钟 | 多实例服务、接口类应用 |
| 蓝绿发布 | 无 | 双份资源 | 秒级,切软链/换 upstream | 有明确两套环境、要求秒级回滚 |
| 灰度发布 | 无 | 视实现 | 秒级,权重归零 | 用户量大、变更风险高、需要看业务指标 |
选择建议:个人站和中小项目,蓝绿是性价比最高的选择——两套目录、一个软链,实施成本很低,却能拿到「秒级回滚」这个最值钱的能力。滚动发布适合多实例后端,灰度适合用户量大、必须按比例验证的场景。
蓝绿发布实操
# 目录结构:blue 是当前线上,green 是待发布
# /srv/www/site-blue 当前版本
# /srv/www/site-green 新版本
# /srv/www/site → 软链,Nginx 的 root 指向它
# 1) 部署新版本到 green(增量同步,不碰 blue)
rsync -avz --delete dist/ prod:/srv/www/site-green/
# 2) 灰度自测:用 --resolve 直连服务器但伪造 Host,验证的就是 green 吗?
# Nginx root 指向软链,所以自测前先把软链指过去再验,或用一个测试端口指向 green
ssh prod 'curl -s -o /dev/null -w "%{http_code}\n" -H "Host: example.com" http://127.0.0.1/'
# 3) 切换:一条命令,瞬时完成
ssh prod 'ln -sfn /srv/www/site-green /srv/www/site && sudo nginx -t && sudo systemctl reload nginx'
# 4) 回滚:还是同一条命令,切回 blue
ssh prod 'ln -sfn /srv/www/site-blue /srv/www/site && sudo nginx -t && sudo systemctl reload nginx'
Nginx 侧配置只需要让 root 指向软链:root /srv/www/site;。切换软链后必须 reload,因为 Nginx 会缓存已打开文件的路径信息(open_file_cache)与已解析的根目录,不 reload 可能继续读到旧文件。蓝绿的三个前提:产物必须自包含(不依赖服务器上手工改过的文件)、两个版本的接口向后兼容(用户浏览器里可能还缓存着旧前端)、回滚包必须还在。
Nginx 权重灰度
upstream app_cluster {
server 10.0.0.11:8080 weight=95; # 旧版本
server 10.0.0.21:8080 weight=5; # 新版本,先吃 5%
keepalive 32;
}
放量节奏:5% → 25% → 50% → 100%,每档至少观察 15~30 分钟,并且每档都要看「错误率、P95 延迟、核心业务指标」三项。权重灰度的好处是回滚极快(把新实例权重改成 0 或直接标 down 并 reload);坏处是新旧版本必须能同时工作,这对数据库结构提出了硬要求。
如果按用户而不是按流量灰度,用 map 生成固定分组更合适:
# 用 cookie 或用户 ID 做稳定分流,同一用户始终落在同一版本
map $cookie_ab_group $backend_pool {
default pool_old;
"new" pool_new;
}
upstream pool_old { server 10.0.0.11:8080; }
upstream pool_new { server 10.0.0.21:8080; }
server { location / { proxy_pass http://$backend_pool; } }
注意 proxy_pass 用变量时会走不同的解析路径,此时 upstream 名要能被解析,且不能依赖 URI 替换规则。
数据库变更与代码发布的顺序
这是发布事故里破坏力最大的一类:代码已经删掉了对旧字段的引用,而数据库里还有实例在读它。正确做法是**「先兼容后上线,扩 → 改 → 删三步走」**:
| 步骤 | 动作 | 代码状态 | 观察点 |
|---|---|---|---|
| 1 扩 | 加新列(可空或有默认值)、加新表、加索引 | 老代码完全不感知 | 确认加列不锁表、索引不在高峰期建 |
| 2 双写 | 新代码同时写新旧字段,但先读旧的 | 新版本上线 | 比对两条数据是否一致 |
| 3 切读 | 新代码改为读新字段 | 已在跑新版本 | 观察一到两天,确认无缺数据 |
| 4 删 | 确认无任何引用后,删旧列/旧表 | 老版本已全部下线 | 单独一次变更,绝不与代码发布同批 |
绝对不要做的三件事:发布时 DROP 或 RENAME 老代码仍在使用的列;在没有双写的情况下直接切读;把「结构变更 + 代码发布 + 数据订正」放在同一个时间窗口里做——出了问题你都不知道该回滚哪一个。
缓存与前端资源版本
- 前端产物用内容哈希命名(
app.a1b2c3.js),HTML 用no-cache,发布后用户拿到新 HTML 才会去请求新资源(见第 19 章)。 no-cache只保证「会去问服务器」,用户不打开页面就不会更新,所以接口必须保持向后兼容至少一个版本:老前端 + 新后端要能正常工作。- 发布时清理应用层缓存要用带版本的方式,不要
FLUSHALL——全量清空会把数据库瞬间打穿。 - CDN 只刷 HTML 与
index.html,静态资源靠文件名变更自然生效。
发布检查清单
| 检查项 | 完成标准 |
|---|---|
| 备份 | 发布前完成数据库与上传目录备份,并确认备份文件可读 |
| 回滚包 | 上一版本的目录/镜像 tag 仍在,且验证过能启动 |
| 变更窗口 | 避开业务高峰、大促、竞品活动期 |
| 监控就位 | 错误率、P95 延迟、核心业务指标面板已打开 |
| 通知 | 群里同步开始时间、发布内容、负责人 |
| 数据库 | 结构变更已按扩/改/删拆分,本次只做兼容性变更 |
| 留观 | 至少 15~30 分钟,期间不做其他配置变更 |
回滚触发条件
| 指标 | 阈值 | 动作 |
|---|---|---|
| 5xx 比例 | 超过 1% 且持续 3 分钟 | 立即回滚,不讨论 |
| P95 延迟 | 超过基线 2 倍且持续 5 分钟 | 先看依赖是否异常,无明确外部原因就回滚 |
| 核心接口成功率 | 低于 99% | 立即回滚 |
| 业务指标 | 登录/下单量同比下跌 30% | 立即回滚并排查 |
| 日志异常量 | 新出现的异常类型持续增长 | 评估后回滚 |
回滚要写进流程,而不是临时决定:把「谁有权决定回滚」提前定好,避免出事时还在群里争论。回滚之后先保证用户可用,再慢慢查根因——顺序反了,用户会替你做出更差的选择。
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
| 软链切换后没 reload | 用户仍看到旧页面 | 切换后 nginx -t && systemctl reload nginx |
| 产物依赖服务器上的手工文件 | 切到新目录后配置丢失、功能异常 | 产物必须自包含,配置走 Git |
| 灰度的新旧版本接口不兼容 | 5% 用户报错,且回滚也救不了已写入的数据 | 数据库变更按三步法,接口保持兼容 |
| 发布和结构变更同一批 | 出问题不知道该回滚哪个 | 拆成独立变更,中间留观察期 |
| 只做备份不验证 | 真出事发现备份是空的或坏的 | 每月做一次恢复演练(见第 27 章) |
| 没有明确阈值 | 一直在「再看看」中错过回滚窗口 | 事先写好指标与阈值 |
| 回滚包被新发布覆盖 | 想回滚发现旧版本已不存在 | 每次部署到带版本号的独立目录 |
小结:发布策略的选择标准是「回滚速度」,蓝绿用两个目录加一个软链就能拿到秒级回滚,是中小项目性价比最高的方案;权重灰度用来按比例验证新版本,前提是新旧接口与数据完全兼容;数据库变更永远走「扩 → 双写 → 切读 → 删」四步,绝不与代码发布同批;发布前把备份、回滚包、监控、通知、留观五项准备好,并把回滚触发阈值写死在流程里——发布这件事,赢在不慌。