路线图

14 发布工程

星辉 2026-07-02 阅读 4 min 836 字 路线图
14 发布工程 封面

概述#

SRE 视角下的发布有一个基本前提:发布即风险。每一次变更都可能引入故障——不是"可能",而是从统计学角度"一定"——拥有足够多次发布后,故障是必然事件。

发布工程的目标不是消除风险(做不到),而是用渐进式发布、自动回滚、Error Budget 决策,让每次发布的预期风险在可接受范围内

发布工程是 SRE 和 DevOps 的交叉地带。DevOps 路线讲"怎么搭发布流水线"(工具层),SRE 路线讲"怎么用发布工具做可靠性决策"(决策层)。

蓝绿部署/金丝雀发布/滚动更新的工具层实现见 DevOps Ch17。本章聚焦 可靠性视角——"怎么判断该不该发布、什么时候必须自动回滚"。

一、发布即风险:可靠性视角#

数据说话#

Google 的统计:70% 的生产事故与配置或代码变更相关。这意味着如果你能控制发布的节奏和质量,你就能预防绝大多数事故。

这个数字的推论:不变更 = 不出事。但不变更是不可能的——业务要演进、bug 要修、安全补丁要打。所以发布工程的核心是"让变更的副作用可控"。

发布策略与风险的对应关系#

发布策略风险等级故障缩小速度适用场景
全量发布不应该用
滚动更新慢(逐步扩大)无状态服务
蓝绿部署快(切流回退)需要瞬间切换的场景
金丝雀发布快(小流量验证)推荐作为默认策略
Feature Flag最低最快(开关秒级关闭)所有策略的补充

SRE 视角的核心观点#

不是"这次发布的代码有没有 bug",而是**"即使有 bug,影响面有多小、恢复有多快"**。好发布工程不保证每次发布不出问题,但保证有问题时:

  • 只影响最小比例用户(金丝雀 5%)
  • 能在 1 分钟内自动检测并回滚
  • 不会消耗超出 Error Budget 的不可用时间

这种思路的转变是根本性的——从"防止故障"到"控制故障影响"。

二、渐进式发布#

标准流程#

text
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 RateError Budget 消耗速率< 1x> 6x
业务指标订单/注册/支付成功率与历史同期持平下降 > 5%
资源使用CPU/内存/连接数不异常增长超过水位线
日志错误日志数量无新增错误类型新增 ERROR 日志

灰度策略的关键设计#

  1. 灰度必须有用户维度。不是"随机 5% 的请求"——如果同一用户不同请求落到不同版本可能出问题。按 user_id / session / cookie 固定路由到同一版本。

  2. 灰度的观察窗口必须足够长。如果延迟劣化需要 5 分钟才能稳定反映在 p99 上,那观察窗口必须 > 5 分钟。慢 SQL 类型的问题在小流量时几乎看不出。

  3. 灰度的流量需要有代表性。如果灰度 5% 恰好全是轻量请求(缓存命中高),而重量请求没命中,灰度数据就是误导。

  4. 灰度要包含高峰时段。只在低峰灰度通过的版本,高峰可能挂——流量模式不同。

  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 自动回滚(这个速度下没时间等待人工判断),其他条件人工确认后回滚。

自动回滚的实现#

yaml
# 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 自动回滚到上一版本。

回滚的前置条件#

  1. 回滚必须比发布快。如果发布要 30 分钟、回滚也要 30 分钟,那灰度期间的故障就是 30 分钟不可用。
  2. 回滚路径必须预演过。不是"文档上写了步骤",而是最近一次发布中实际回滚成功过。
  3. 数据库 Schema 变更必须向后兼容。如果新版改了数据库结构,回滚时旧版代码要能正常工作——这是 Schema 变更最容易被忽视的约束。
  4. 回滚的镜像必须在 registry 里。镜像被清理了就回滚不了,要保留最近 N 个版本。
  5. 回滚的配置必须有版本控制。ConfigMap/Secret 改动了,回滚时也要回滚配置。

回滚不能解决的场景#

  • 数据库迁移已执行:加了列可以回滚,删了列回滚不了(数据丢了)
  • 消息已发送:发了的消息收不回
  • 缓存已失效:回滚后缓存要重建
  • 外部 API 已调用:调用了支付接口,回滚不能撤销扣款

这些场景需要在发布前就设计好"前向修复"(forward fix)方案——不回滚,直接发新版本修复。

四、发布与 Error Budget 联动#

决策流程#

text
发布前 → 检查 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 开始上升:

text
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(功能开关)#

发布 ≠ 启用#

text
传统: 部署代码 = 功能上线 → 风险集中
Feature Flag: 部署代码 ≠ 功能上线 → 开关控制

Feature Flag 的价值在于发布与功能上线解耦

  • 代码部署到生产 → 开关关闭 → 不影响用户
  • 按需逐步打开开关 → 分用户/分区域启用 → 出问题关闭开关(秒级回滚)
  • 不需要重新部署 → 比传统 rollback 快 100 倍

使用场景#

场景例子
逐步放量新功能只向 1% 用户开放
紧急关闭某个功能导致问题,关开关秒级止损
A/B 实验同一功能两种实现,对比效果
运维开关关闭非核心功能释放资源
灰度修复新功能有 bug,关开关修复后重新打开

Feature Flag 的实现#

python
# 代码示例
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 变更#

text
错误的变更顺序:
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. 部署 v2

Schema 变更的分阶段#

阶段操作兼容性
1加新列(nullable)向后兼容
2代码读写新列向后兼容
3数据迁移到新列向后兼容
4代码只读写新列向后兼容
5删除旧列不可回滚

阶段 5 是不可逆的——删除列后数据丢失,无法回滚到阶段 4。所以阶段 5 要谨慎,通常需要等待一段时间(如 1 个月)确认没有依赖。

大表变更的挑战#

  • 加索引:百万级表加索引可能锁表数小时
  • 加列:MySQL 8.0 之前加列要重建表
  • 数据类型变更:可能触发全表数据重写

解决方案:

  • 用 pt-online-schema-change / gh-ost 等工具在线变更
  • 在低峰期做
  • 分批变更(先变更从库,再切换主库)

七、发布工程的组织维度#

发布权限#

谁有权发布到生产?

  • 开发无权直接发布:必须通过 CI/CD 流水线
  • SRE 有权紧急回滚:但要有审计
  • 发布需要审批:根据风险等级决定审批层级

发布日历#

  • 维护发布日历,记录计划发布
  • 高风险发布需要提前公告
  • 同一服务不要同时多个发布

发布复盘#

每次重大发布后做简短复盘:

  • 发布是否按计划完成
  • 有没有出现意外
  • 灰度指标是否准确
  • 回滚是否触发(如果触发,为什么)

实战要点#

  1. 金丝雀发布是默认策略。全量发布只应在内部工具等低风险场景使用。
  2. 回滚速度必须快于发布速度。做不到就要重新设计发布流水线。
  3. Error Budget 是发布的决策依据,不是 SRE 的心情。
  4. 灰度不等于观察。必须设具体的观察指标和通过标准,不能"看看再说"。
  5. 数据库变更与代码变更必须协调——向后兼容的 Schema 变更是安全回滚的前提。
  6. Feature Flag 是发布的补充,不是替代。开关可以让发布更安全,但不能替代灰度。
  7. 自动回滚要有但不要过度。14.4x 自动回滚是底线,其他条件人工判断。
  8. 周五不发版。用流程约束风险,比技术手段更有效。
  9. 发布窗口要明确。所有人都知道"什么时候可以发,什么时候不能发"。
  10. 工具层部署(ArgoCD Rollout / Istio 流量路由 / CI/CD Pipeline)见 DevOps Ch17。

小结#

发布工程从可靠性角度的核心公式:渐进式发布 + 自动回滚阈值 + Error Budget 决策。不是"发布了就别出错",而是"出错了能在 2 分钟内自动回滚、只影响 5% 用户、Error Budget 消耗在可接受范围内"。这个思维转变就是从 DevOps 到 SRE 的关键一步。

判断一个团队的发布工程是否成熟,看三件事:核心服务都用金丝雀发布、有自动回滚机制、发布决策基于 Error Budget。三件事都做到是成熟团队;只做到一两件是"在改进中";一件都没做,每次发布都是"赌博"——运气好不出事,运气不好就是 SEV-1。

发布工程的最高境界是"无聊"——每次发布都按流程走,没惊喜、没事故、没加班。无聊的发布就是好的发布。