外观
灰度与回滚:蓝绿、金丝雀与数据兼容
发布的核心不是"怎么发上去",是出问题时怎么快速退回来。代码能回滚,数据库往往不能——这才是真正的难点。
一、蓝绿部署
两套完整环境,流量只指向其中一套。切换就是改一次 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 reloadtext
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: 0maxUnavailable: 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 权重或服务网格 |
发布前的三个问题
- 这个版本能被回滚吗?(数据兼容)
- 回滚需要多久?(命令是否现成)
- 谁有权执行回滚?(别等到出事才找人)