14 发布工程
概述#
SRE 视角下的发布有一个基本前提:发布即风险。每一次变更都可能引入故障——不是"可能",而是从统计学角度"一定"——拥有足够多次发布后,故障是必然事件。
发布工程的目标不是消除风险(做不到),而是用渐进式发布、自动回滚、Error Budget 决策,让每次发布的预期风险在可接受范围内。
发布工程是 SRE 和 DevOps 的交叉地带。DevOps 路线讲"怎么搭发布流水线"(工具层),SRE 路线讲"怎么用发布工具做可靠性决策"(决策层)。
蓝绿部署/金丝雀发布/滚动更新的工具层实现见 DevOps Ch17。本章聚焦 可靠性视角——"怎么判断该不该发布、什么时候必须自动回滚"。
一、发布即风险:可靠性视角#
数据说话#
Google 的统计:70% 的生产事故与配置或代码变更相关。这意味着如果你能控制发布的节奏和质量,你就能预防绝大多数事故。
这个数字的推论:不变更 = 不出事。但不变更是不可能的——业务要演进、bug 要修、安全补丁要打。所以发布工程的核心是"让变更的副作用可控"。
发布策略与风险的对应关系#
| 发布策略 | 风险等级 | 故障缩小速度 | 适用场景 |
|---|---|---|---|
| 全量发布 | 高 | 无 | 不应该用 |
| 滚动更新 | 中 | 慢(逐步扩大) | 无状态服务 |
| 蓝绿部署 | 中 | 快(切流回退) | 需要瞬间切换的场景 |
| 金丝雀发布 | 低 | 快(小流量验证) | 推荐作为默认策略 |
| Feature Flag | 最低 | 最快(开关秒级关闭) | 所有策略的补充 |
SRE 视角的核心观点#
不是"这次发布的代码有没有 bug",而是**"即使有 bug,影响面有多小、恢复有多快"**。好发布工程不保证每次发布不出问题,但保证有问题时:
- 只影响最小比例用户(金丝雀 5%)
- 能在 1 分钟内自动检测并回滚
- 不会消耗超出 Error Budget 的不可用时间
这种思路的转变是根本性的——从"防止故障"到"控制故障影响"。
二、渐进式发布#
标准流程#
Stage 1: 金丝雀 1-5% → 观察 10-30 分钟
Stage 2: 扩大 10-25% → 观察 15-30 分钟
Stage 3: 扩大 50% → 观察 10-15 分钟
Stage 4: 全量 100% → 观察期后确认完成每一步都要有明确的通过标准——不是"看看没问题就扩大",而是"指标满足 X 条件才扩大"。
每一步的观察指标#
| 指标 | 看什么 | 通过标准 | 回滚标准 |
|---|---|---|---|
| 错误率 | 5xx/超时比例 | 与上一版本持平或更优 | > 基线 3 倍 |
| 延迟 P99 | 响应时间分位数 | 与上一版本持平或更优 | > 基线 2 倍 |
| SLO Burn Rate | Error Budget 消耗速率 | < 1x | > 6x |
| 业务指标 | 订单/注册/支付成功率 | 与历史同期持平 | 下降 > 5% |
| 资源使用 | CPU/内存/连接数 | 不异常增长 | 超过水位线 |
| 日志 | 错误日志数量 | 无新增错误类型 | 新增 ERROR 日志 |
灰度策略的关键设计#
灰度必须有用户维度。不是"随机 5% 的请求"——如果同一用户不同请求落到不同版本可能出问题。按 user_id / session / cookie 固定路由到同一版本。
灰度的观察窗口必须足够长。如果延迟劣化需要 5 分钟才能稳定反映在 p99 上,那观察窗口必须 > 5 分钟。慢 SQL 类型的问题在小流量时几乎看不出。
灰度的流量需要有代表性。如果灰度 5% 恰好全是轻量请求(缓存命中高),而重量请求没命中,灰度数据就是误导。
灰度要包含高峰时段。只在低峰灰度通过的版本,高峰可能挂——流量模式不同。
灰度要测降级路径。不仅测正常请求,还要测"下游挂了"时的降级路径——新版可能改了降级逻辑。
灰度的用户选择#
按什么维度灰度?几种选择:
| 维度 | 优点 | 缺点 |
|---|---|---|
| 随机请求 | 简单 | 同一用户可能跨版本 |
| user_id 哈希 | 用户一致 | 需要改路由逻辑 |
| 内部员工优先 | 出问题先内部 | 员工流量不代表用户 |
| 新用户优先 | 影响面小 | 新用户行为不同 |
| 按地区 | 区域可控 | 地区间可能有依赖 |
推荐"内部员工 → 新用户 → 全量"的渐进路径。
Istio VirtualService 权重路由、ArgoCD Rollout 的部署配置见 DevOps Ch17。
三、自动回滚触发条件#
什么时候必须自动回滚#
| 条件 | 阈值 | 为什么 |
|---|---|---|
| 错误率突增 | 新版错误率 > 基线 3 倍 | 明确的质量退化 |
| SLO 燃烧率 | 1h burn rate > 6x | 快速消耗 Error Budget |
| 关键业务指标 | 下单成功率下降 > 5% | 直接影响收入 |
| P99 延迟劣化 | P99 > 基线 2 倍 | 用户明显感知到慢 |
| Pod 频繁重启 | 新版 Pod 5 分钟内重启 > 3 次 | 启动失败 |
| 健康检查失败 | readiness probe 失败率 > 10% | 服务不健康 |
自动回滚 vs 人工回滚#
| 自动回滚 | 人工回滚 | |
|---|---|---|
| 响应速度 | 秒级 | 分钟级(需要人看到告警→判断→执行) |
| 误判风险 | 较高(可能因非发布原因触发) | 低(有人判断) |
| 疲劳成本 | 零 | 每次回滚都需要人介入 |
| 复杂度 | 高(需要自动化) | 低(人工执行) |
| 凌晨响应 | 不受影响 | 值班人可能没醒 |
推荐策略:SLO 燃烧率 > 14.4x 自动回滚(这个速度下没时间等待人工判断),其他条件人工确认后回滚。
自动回滚的实现#
# ArgoCD Rollout 自动回滚示例
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
strategy:
canary:
steps:
- setWeight: 5
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
- setWeight: 25
- pause: { duration: 15m }
- analysis:
templates:
- templateName: success-rate
- setWeight: 50
- pause: { duration: 10m }
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 1m
successCondition: result[0] >= 0.99
failureCondition: result[0] < 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{status!~"5..",service="{{args.service-name}}"}[5m]))
/
sum(rate(http_requests_total{service="{{args.service-name}}"}[5m]))failureCondition 触发时 ArgoCD 自动回滚到上一版本。
回滚的前置条件#
- 回滚必须比发布快。如果发布要 30 分钟、回滚也要 30 分钟,那灰度期间的故障就是 30 分钟不可用。
- 回滚路径必须预演过。不是"文档上写了步骤",而是最近一次发布中实际回滚成功过。
- 数据库 Schema 变更必须向后兼容。如果新版改了数据库结构,回滚时旧版代码要能正常工作——这是 Schema 变更最容易被忽视的约束。
- 回滚的镜像必须在 registry 里。镜像被清理了就回滚不了,要保留最近 N 个版本。
- 回滚的配置必须有版本控制。ConfigMap/Secret 改动了,回滚时也要回滚配置。
回滚不能解决的场景#
- 数据库迁移已执行:加了列可以回滚,删了列回滚不了(数据丢了)
- 消息已发送:发了的消息收不回
- 缓存已失效:回滚后缓存要重建
- 外部 API 已调用:调用了支付接口,回滚不能撤销扣款
这些场景需要在发布前就设计好"前向修复"(forward fix)方案——不回滚,直接发新版本修复。
四、发布与 Error Budget 联动#
决策流程#
发布前 → 检查 Error Budget 剩余量
Budget > 50% → 正常发布,标准灰度流程
Budget 25-50% → 发布前需 SRE review,灰度每一步观察时间加倍
Budget 10-25% → 仅允许修复性发布(bug fix),新功能冻结
Budget < 10% → 完全冻结所有发布,全力修复可靠性这套流程让发布决策从"SRE 拦不拦"变成"数据说了算"。开发想发版,先看 Budget 余额——这是 Error Budget 机制的核心价值。
发布中的 Burn Rate 监控#
发布过程中持续监控 Error Budget 消耗。如果 burn rate 开始上升:
1h burn rate > 1x → 暂停扩大灰度,观察
1h burn rate > 6x → 停止灰度,评估是否需要回滚
1h burn rate > 14.4x → 立即自动回滚生产案例(摘自 A 旗舰项目)#
一次发布 v2.4.1 引入 N+1 查询问题。在灰度 50% 时出现症状,但当时没有配置 Error Budget 自动回滚,等人工决策回滚时已经从故障开始过去了 22 分钟,消耗了月度 Error Budget 的 68%。如果配了自动回滚(burn rate > 14.4x 自动触发),回滚时间可以缩短到 2 分钟内,Budget 消耗从 68% 降到 5%。
这个案例的教训:人工回滚永远比自动回滚慢。不是值班人不负责任,是"看到告警→确认是发布问题→决策回滚→执行"这个链条在压力下至少要 5-10 分钟。而 burn rate 14.4x 的情况下,每分钟都在烧预算。
发布窗口#
发布应该在"安全窗口"做:
- 工作日,非高峰:周二到周四下午 14:00-16:00
- 避免周五:周五发布出问题周末没人修
- 避免节假日前:节假日前一天禁止发布
- 避免同时发布:多个服务不要同时发布,出问题不知道是哪个
很多公司有"周五不发版"的硬规定——这是用流程约束风险。SRE 应该推动这类规定的落实。
五、Feature Flag(功能开关)#
发布 ≠ 启用#
传统: 部署代码 = 功能上线 → 风险集中
Feature Flag: 部署代码 ≠ 功能上线 → 开关控制Feature Flag 的价值在于发布与功能上线解耦:
- 代码部署到生产 → 开关关闭 → 不影响用户
- 按需逐步打开开关 → 分用户/分区域启用 → 出问题关闭开关(秒级回滚)
- 不需要重新部署 → 比传统 rollback 快 100 倍
使用场景#
| 场景 | 例子 |
|---|---|
| 逐步放量 | 新功能只向 1% 用户开放 |
| 紧急关闭 | 某个功能导致问题,关开关秒级止损 |
| A/B 实验 | 同一功能两种实现,对比效果 |
| 运维开关 | 关闭非核心功能释放资源 |
| 灰度修复 | 新功能有 bug,关开关修复后重新打开 |
Feature Flag 的实现#
# 代码示例
def get_recommendations(user_id):
if feature_flag.enabled("new_recommendation_algo", user_id):
return new_recommendation_engine.get(user_id)
else:
return old_recommendation_engine.get(user_id)Feature Flag 服务(如 LaunchDarkly、Unleash、自研)提供:
- 开关的创建和管理
- 按用户/群体/百分比控制
- 实时生效(不需要重启服务)
- 审计日志
风险#
- 开关膨胀:Flag 越来越多,代码分支复杂度爆炸。必须有 Flag 生命周期管理(定期清理已全量开启的 Flag)。
- Flag 配置变更也是发布:改 Flag 配置 ≠ 安全。需要和代码发布一样的灰度策略。
- 测试复杂度增加:每个 Flag 的 on/off 都要测,组合爆炸。
- 依赖关系:Flag A 依赖 Flag B,B 关了 A 也不能开。
Flag 生命周期管理#
每个 Flag 必须有:
- 创建日期
- 计划清理日期
- Owner
- 状态(draft / testing / partial / full / retired)
定期 review:
- 全量开启 > 2 周的 Flag → 考虑清理(代码里删掉 if 分支)
- 长期 testing 的 Flag → 评估是否要继续
- 没有 Owner 的 Flag → 清理或重新指派
六、数据库变更与发布#
数据库 Schema 变更是发布工程最复杂的部分——回滚难度大,影响持久。
向后兼容的 Schema 变更#
错误的变更顺序:
1. 代码 v2 删除 column X
2. 部署 v2 → 旧版还在跑 → 旧版查询 column X 失败
正确的变更顺序(双发布):
1. 代码 v1.1 兼容 column X 存在/不存在
2. 部署 v1.1
3. DB 变更:删除 column X
4. 代码 v2 删除对 column X 的引用
5. 部署 v2Schema 变更的分阶段#
| 阶段 | 操作 | 兼容性 |
|---|---|---|
| 1 | 加新列(nullable) | 向后兼容 |
| 2 | 代码读写新列 | 向后兼容 |
| 3 | 数据迁移到新列 | 向后兼容 |
| 4 | 代码只读写新列 | 向后兼容 |
| 5 | 删除旧列 | 不可回滚 |
阶段 5 是不可逆的——删除列后数据丢失,无法回滚到阶段 4。所以阶段 5 要谨慎,通常需要等待一段时间(如 1 个月)确认没有依赖。
大表变更的挑战#
- 加索引:百万级表加索引可能锁表数小时
- 加列:MySQL 8.0 之前加列要重建表
- 数据类型变更:可能触发全表数据重写
解决方案:
- 用 pt-online-schema-change / gh-ost 等工具在线变更
- 在低峰期做
- 分批变更(先变更从库,再切换主库)
七、发布工程的组织维度#
发布权限#
谁有权发布到生产?
- 开发无权直接发布:必须通过 CI/CD 流水线
- SRE 有权紧急回滚:但要有审计
- 发布需要审批:根据风险等级决定审批层级
发布日历#
- 维护发布日历,记录计划发布
- 高风险发布需要提前公告
- 同一服务不要同时多个发布
发布复盘#
每次重大发布后做简短复盘:
- 发布是否按计划完成
- 有没有出现意外
- 灰度指标是否准确
- 回滚是否触发(如果触发,为什么)
实战要点#
- 金丝雀发布是默认策略。全量发布只应在内部工具等低风险场景使用。
- 回滚速度必须快于发布速度。做不到就要重新设计发布流水线。
- Error Budget 是发布的决策依据,不是 SRE 的心情。
- 灰度不等于观察。必须设具体的观察指标和通过标准,不能"看看再说"。
- 数据库变更与代码变更必须协调——向后兼容的 Schema 变更是安全回滚的前提。
- Feature Flag 是发布的补充,不是替代。开关可以让发布更安全,但不能替代灰度。
- 自动回滚要有但不要过度。14.4x 自动回滚是底线,其他条件人工判断。
- 周五不发版。用流程约束风险,比技术手段更有效。
- 发布窗口要明确。所有人都知道"什么时候可以发,什么时候不能发"。
- 工具层部署(ArgoCD Rollout / Istio 流量路由 / CI/CD Pipeline)见 DevOps Ch17。
小结#
发布工程从可靠性角度的核心公式:渐进式发布 + 自动回滚阈值 + Error Budget 决策。不是"发布了就别出错",而是"出错了能在 2 分钟内自动回滚、只影响 5% 用户、Error Budget 消耗在可接受范围内"。这个思维转变就是从 DevOps 到 SRE 的关键一步。
判断一个团队的发布工程是否成熟,看三件事:核心服务都用金丝雀发布、有自动回滚机制、发布决策基于 Error Budget。三件事都做到是成熟团队;只做到一两件是"在改进中";一件都没做,每次发布都是"赌博"——运气好不出事,运气不好就是 SEV-1。
发布工程的最高境界是"无聊"——每次发布都按流程走,没惊喜、没事故、没加班。无聊的发布就是好的发布。