Skip to content

灰度与回滚:蓝绿、金丝雀与数据兼容 ​

发布的核心不是"怎么发上去",是出问题时怎么快速退回来。代码能回滚,数据库往往不能——这才是真正的难点。

一、蓝绿部署 ​

两套完整环境,流量只指向其中一套。切换就是改一次 upstream。

nginx
# /etc/nginx/conf.d/upstreams.conf
upstream app_blue  { server 10.0.0.41:8080; }
upstream app_green { server 10.0.0.42:8080; }

# 当前生效的池(由脚本改写后 reload)
include /etc/nginx/conf.d/active.conf;
ini
# /etc/nginx/conf.d/active.conf
upstream app_active { server 10.0.0.41:8080; }
nginx
server {
    listen 80;
    server_name api.example.com;
    location / {
        proxy_pass http://app_active;
        proxy_set_header Host $host;
    }
}

切换:

bash
cat > /etc/nginx/conf.d/active.conf <<'EOF'
upstream app_active { server 10.0.0.42:8080; }
EOF

nginx -t && nginx -s reload
text
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

回滚就是把文件改回去再 reload,几秒钟。

优点缺点
切换和回滚都极快需要双倍资源
新版本可以充分预热数据库要兼容两个版本
出问题影响面清晰长连接会断(reload 时旧 worker 会优雅退出)

reload 不会中断已有请求

nginx -s reload 会让旧 worker 处理完当前请求再退出,所以长轮询/WebSocket 连接可能保持几分钟。切换后要观察 nginx_status 的 Active connections 是否真的转移了。

二、金丝雀发布 ​

只放一小部分流量到新版本,观察指标后再逐步放大。

nginx
split_clients "${remote_addr}AAA" $app_pool {
    5%      app_canary;
    *       app_stable;
}

upstream app_stable { server 10.0.0.41:8080; }
upstream app_canary { server 10.0.0.42:8080; }

server {
    location / {
        proxy_pass http://$app_pool;
    }
}

${remote_addr}AAA 里的 AAA 是盐,保证同一个客户端始终命中同一池(否则用户会在两个版本间跳来跳去)。

放大比例:

text
5% → 观察 30 分钟 → 20% → 观察 → 50% → 100%

观察指标:

指标阈值
错误率(5xx)不超过基线 +0.5%
P99 延迟不超过基线 1.2 倍
CPU/内存不异常突增
业务指标(下单成功率)不低于基线

任一项超标:把比例改回 0,reload。

三、K8s 里的灰度 ​

滚动更新(默认) ​

yaml
strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0

maxUnavailable: 0 保证先起新的、就绪后再停旧的,容量不下降。

双 Deployment 按副本比例 ​

yaml
# stable: replicas 9
# canary: replicas 1
# 两者 label 相同,Service 同时选中 → 约 10% 流量

简单粗暴,但比例只能按副本数近似。

Ingress 按权重(ingress-nginx) ​

yaml
metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "5"

先把 canary-weight 设 5,观察后改成 50、100,再把 stable 缩容。

四、数据库是回滚的真正障碍 ​

代码可以瞬间回滚,但数据库变更不可逆。典型翻车:

sql
-- 新版本发布的 DDL
ALTER TABLE orders DROP COLUMN old_status;

发布后发现问题要回滚代码,但旧代码还在读 old_status —— 直接报错。

expand-contract 三段式 ​

第一步:expand(只加不改)

sql
ALTER TABLE orders ADD COLUMN status_v2 VARCHAR(32) NULL;

新代码双写两个字段,读还是读旧的。此时新旧版本都能跑。

第二步:migrate(回填 + 切读)

sql
UPDATE orders SET status_v2 = status WHERE status_v2 IS NULL;

分批更新(每批几千行),避免长事务。然后新代码改成读 status_v2。

第三步:contract(确认无旧版本后再删)

sql
-- 至少等一个完整的发布周期(比如 2 周)
ALTER TABLE orders DROP COLUMN status;

删列/改类型/重命名 必须跨版本

单次发布里同时"改代码 + 改表结构"是最危险的操作。任何一步出问题,回滚都会卡在数据不兼容上。

原则:让 N 和 N-1 两个版本能同时跑在同一份数据上。

其他不兼容变更 ​

变更安全做法
改字段类型加新列 → 双写 → 回填 → 切读 → 删旧列
重命名表新建表 + 双写 + 迁移,或建视图过渡
加非空列先加可空列,回填后再加约束
加唯一索引先加普通索引,确认无重复再改唯一
删除字段先让代码停止读写,跨版本后再删

五、回滚清单 ​

出事时按顺序执行,每步确认后再下一步:

text
1. 停止流量入口(或把灰度比例调 0)
   - nginx: 改 upstream 回旧版本 + reload
   - K8s: kubectl rollout undo

2. 确认旧版本进程健康
   - systemctl status / kubectl get pods
   - curl health

3. 恢复流量(10% → 50% → 100%)

4. 检查数据是否被新版本写坏
   - 对比新旧字段
   - 查异常日志

5. 保留现场
   - 新版本的日志、dump、监控截图
   
6. 复盘(不要跳过)
bash
# nginx 回滚
sed -i 's/10.0.0.42:8080/10.0.0.41:8080/' /etc/nginx/conf.d/active.conf
nginx -t && nginx -s reload

# K8s 回滚
kubectl -n prod rollout undo deploy/order-api
kubectl -n prod rollout status deploy/order-api --timeout=180s

# 软链回滚
/opt/order-api/bin/rollback.sh

六、演练 ​

不演练的回滚方案等于没有方案。

演练项频率通过标准
软链回滚每次发布后一条命令、60 秒内恢复
蓝绿切换每月reload 后 10 秒内流量转移
K8s rollout undo每月3 分钟内所有 Pod 就绪
数据库回滚每季度旧版本能正常读写新数据
备份恢复每季度从备份恢复到独立实例并验证数据
全链路故障演练每半年按预案执行,无人工临时决策
bash
# 演练脚本示例:发布后立刻回滚,验证可恢复性
./deploy.sh /tmp/app-new.jar
sleep 10
./rollback.sh
curl -fsS http://127.0.0.1:8080/actuator/health && echo "rollback verified"

七、踩过的坑 ​

现象根因处置
回滚后接口报字段不存在新版本删了列expand-contract,跨版本删
蓝绿切换后会话丢失session 存在本地内存会话外置到 Redis
灰度用户一会儿新一会儿旧split_clients 没加盐用 remote_addr + 固定盐
reload 后旧连接还在长连接没断观察 Active connections,必要时重启 worker
K8s 回滚了但 Pod 还是新的回滚的是 ReplicaSet,镜像 tag 没变确认 image 确实变了
回滚后缓存里是新格式数据缓存没清发布时带版本前缀,或清相关 key
双写失败导致数据不一致没做补偿双写失败要报警 + 对账任务
灰度比例靠副本数,实际不均Pod 分布不均用 Ingress 权重或服务网格

发布前的三个问题

  1. 这个版本能被回滚吗?(数据兼容)
  2. 回滚需要多久?(命令是否现成)
  3. 谁有权执行回滚?(别等到出事才找人)