🏠 总目录📚 本教程 03 · 灰度、影子与回滚
📑 本页目录(点开跳转)

03 · 灰度、影子与回滚

42 分钟 | ⭐ 让「出错」变成一件便宜的事


🎯 一句话

你不可能保证不出错,但你可以保证「出错时只有 1% 的人受影响,而且五分钟内能撤回」。 这一章讲的三个机制,本质上都是在降低犯错的单价


🎚️ 一、三种放量方式,解决不同的问题

方式 新模型拿到什么 用户受影响吗 解决什么
影子流量(shadow) 真实请求的副本 ❌ 完全不影响 ⭐ 验证工程正确性
灰度(canary) 真实流量的一小部分 ✅ 少量人 验证真实效果
A/B 实验 严格随机分流 量化效果差异
正确的顺序(每一步过了才进下一步) 影子 灰度 1% 灰度 10% A/B 50/50 全量 拿到可信的效果数字(第 12 章) 看有没有长尾问题、罕见 case 看真实用户反应,代价可控 看会不会崩、延迟够不够、输出合不合理
右边那四行小字才是重点:每一档回答的是不同的问题,所以顺序不能跳、也不能合并 —— 跳过哪一档,那一类问题就只能等到流量更大时才暴露。

最常见的错误是跳过影子直接灰度。 灰度虽然只影响 1%,但如果模型有工程性 bug(特征算错、维度不对), 那 1% 的人拿到的是彻底错误的结果 —— 而这类 bug 影子流量能 100% 提前发现。


👻 二、影子流量:最被低估的一步

   用户请求 ──┬──→ 旧模型 ──→ 返回给用户  ✅
              │
              └──→ 新模型 ──→ 【只记录,不返回】 ⭐
                              ↓
                        对比两边的输出

它能发现什么(这些都是灰度发现不了或代价更大的):

能发现 说明
崩溃 / 异常 新模型遇到线上真实数据会不会挂
延迟不达标 离线测的延迟和线上完全不是一回事
输出分布异常 新模型的预测分数分布和旧的差太多 = 警报
训练/推理不一致 ⭐⭐ 对同一批请求,离线重算一遍特征,和线上比对(第 4 章)

💡 影子流量最有价值的用法

   不只是「看新模型会不会崩」,而是:

   ① 记录线上实际用的特征值
   ② 用同一批样本,走离线管道再算一遍特征
   ③ 逐字段比对

   ⭐ 这是发现「训练/推理不一致」唯一可靠的办法
      —— 而那是线上掉点最常见的原因(第 4 章)

⚠️ 影子流量的三个成本(要提前想好):

成本 应对
算力翻倍 只影子采样 5–10% 的流量就够
有副作用的操作会执行两次 影子链路必须禁写:不发消息、不扣库存、不写业务库 💀
下游系统压力 影子调用要走独立配额,别把下游打挂

💀 第二条出过真事故:影子链路里的推荐模型会写「已曝光」表用于去重, 结果同一个物品被记了两次曝光,导致真实推荐里它被错误地过滤掉。 规则:影子链路只读,任何写操作都要显式关掉。


🐤 三、灰度:小步放量

   1% → 5% → 20% → 50% → 100%
   每一档观察足够长时间再往下走

「足够长」是多久

   ⭐ 至少覆盖一个完整的业务周期

   电商/内容:24 小时(早晚高峰、上下班行为完全不同)
   To B 工具:一周(工作日 vs 周末)
   有月度周期的(如报销、结算):一个月

   ⚠️ 只跑 2 小时就全量,是在赌「白天的表现代表全天」

灰度分流怎么切(很关键)

切法 问题
❌ 按请求随机 同一用户一会儿新一会儿旧,体验割裂,指标也没法解读
按用户 ID 哈希 同一用户始终落在同一侧,可比
✅ 按设备 / 会话 无登录场景用这个

⚠️ 哈希要加盐

   如果多个实验都用 hash(user_id) % 100 < 1
   → 永远是【同一批用户】被选中做小流量实验  💀
   → 这批人的体验被反复折腾,而且实验之间互相污染

   ⭐ 正确做法:hash(user_id + 实验名) —— 每个实验的分流互相独立

⏮️ 四、回滚:唯一真正的安全网

能回滚,一切错误都是可修复的;不能回滚,任何错误都是事故。

回滚要满足三个条件

   ① 快      —— 目标是 5 分钟内完成,最好是改个配置就行
   ② 完整    —— 模型 + 特征逻辑 + 配置要一起回,只回模型没用 ⭐
   ③ 可验证  —— 回滚后要能确认真的回去了(看版本号、看指标恢复)

⭐ 第 ② 条是最容易翻车的地方

   新模型上线时,你可能同时改了:
   · 模型文件
   · 特征计算逻辑(加了个新特征)
   · 配置(阈值、截断长度)
   · 上游数据表结构

   只回滚模型 → 新特征逻辑还在 → 旧模型拿到它没见过的特征  💀
   → 比不回滚还糟

🔑 解法:把这四样绑成一个「发布单元」,版本号统一,回滚是整体回滚。 🔗 第 17 章会讲具体怎么组织。

回滚触发条件要预先写死

   ❌ 「感觉不太对,讨论一下要不要回」   → 讨论时损失在扩大
   ✅ 预先定好自动触发线:

      · 错误率 > 基线 3 倍         → 自动回滚
      · P99 延迟 > SLA            → 自动回滚
      · 核心业务指标跌破阈值        → 告警 + 人工确认后回滚
      · 模型输出分布突变           → 告警

💡 为什么业务指标不自动回滚:它有正常波动,也可能因为外部原因(大促、竞品)变化。 工程指标客观,可以自动;业务指标要人看一眼。


🧯 五、降级:模型挂了返回什么

必须在上线前就想清楚

降级链路(从好到差,每层都要能独立工作) 新模型 旧模型 简单规则 静态兜底 如「按热度排序」 如编辑精选榜单 ⭐ 最后一层【必须不依赖任何模型和实时计算】
该盯的不是前三层,而是最后一格:只要它还需要模型或实时计算,那条链就有可能整条一起挂 —— 而且降级本身不报错,没人会告诉你已经退到这儿了。

⚠️ 降级最容易漏的一点降级本身要有监控和告警。 很多系统降级了几个月都没人知道 —— 因为降级路径不报错,它「正常工作」着。

🔗 推荐算法 16 实战项目的检查点里问过 「你的系统在什么情况下会降级、降级到什么」—— 就是这件事。


💀 六、一个专门讲"回滚失败"的事故

前面说回滚是安全网。但没演练过的安全网,出事时经常是破的。

   某风控模型 v7 上线两小时,拒绝率从 12% 飙到 31%
   → 值班同学按手册回滚到 v6,用时 4 分钟   ✅ 看起来很顺利

   回滚后 20 分钟:
   拒绝率没回到 12%,反而变成 38%          💀 比不回滚还糟

   原因:
   v7 上线时顺手改了特征表 —— 加了 3 列、把 device_score 从 int 改成 float
   v6 的模型文件里存的特征 schema 还是旧的
   → 按位置取特征 → 从第 12 列开始【整体错位】
   → v6 拿到的每一个特征值都是错的,但维度是对的,不报错

   真正恢复:6 小时后(回滚特征表 ETL + 等一次重跑)
环节 声称的时间 实际时间
模型回滚 5 分钟 4 分钟
发现回滚没生效 20 分钟
定位到 schema 变更 90 分钟
特征表回滚 + 重跑 4 小时
端到端 MTTR 5 分钟 6 小时 💀

这张表是本章最该记住的东西「我们有回滚方案」和「我们的 MTTR 是 5 分钟」是两回事。 前者是文档,后者是演练出来的数字

🏋️ 回滚演练(game day):怎么做

   ① 挑一个正常的工作日下午(不是深夜,要让人清醒)
   ② 不预告具体时间,只告诉团队"本周会有一次"
   ③ 在灰度环境把当前模型故意换成上一个版本
   ④ 计时:从触发到【指标确认恢复】用了多久
   ⑤ 记下每一个卡住的地方

   ⭐ 关注的不是"能不能回",是"卡在哪、卡了多久"

演练里最常暴露的五件事

暴露的问题 典型表现
回滚只回了模型 ⭐⭐ 特征逻辑 / schema / 配置还是新的
缺权限 值班的人没有改配置的权限,要找人审批
没有"确认已恢复"的手段 回滚完了不知道到底生没生效
手册过期 里面写的控制台路径半年前就改了
依赖某个具体的人 只有写这个服务的人知道怎么回

一个可以当 KPI 的数字端到端 MTTR(从决定回滚到指标确认恢复)。 每季度演练一次,把这个数字记下来。它比"我们有没有回滚方案"有意义得多。 🔗 和第 8 章的「发现延迟」是一对: 发现延迟 + MTTR = 一次事故的实际影响时长。


🎲 七、分流:怎么切、怎么验、需要多少流量

🔨 带盐的哈希分流(可以直接抄)

import hashlib

def bucket(unit_id: str, exp_name: str, buckets: int = 100) -> int:
    """unit_id: 用户ID/设备ID;exp_name: 实验名(就是那把盐)⭐"""
    key = f"{exp_name}:{unit_id}".encode()           # ⭐ 盐必须参与哈希,不能只当前缀存
    h = hashlib.md5(key).digest()                     # ⭐ 用 md5/sha 而不是 Python 的 hash()
    return int.from_bytes(h[:8], "big") % buckets     #    hash() 每次进程重启结果会变 💀


def in_treatment(unit_id, exp_name, pct):
    return bucket(unit_id, exp_name) < pct

💀 注释里那条坑值得单独说Python 内置 hash() 对字符串是加盐随机的, 进程重启后同一个用户会落到不同桶。 线上表现是:服务每次发版,实验分组就洗一次牌 —— 而且它不报错。 规则:分流哈希只用 md5 / sha1 / murmur 这类确定性哈希。

✅ 分流上线前必须验的三件事

import numpy as np
from scipy import stats

def check_split(ids, exp_name, pct, feats=None):
    """feats: DataFrame,分流单元的历史特征(用于 A/A 检验)"""
    flag = np.array([in_treatment(i, exp_name, pct) for i in ids])

    # ⭐ ① 比例对不对(大流量下应该非常接近 pct%)
    print(f"实际实验组占比 {flag.mean():.4%}(目标 {pct}%)")

    # ⭐ ② 分桶均匀吗——卡方检验,p 太小说明哈希有问题
    b = np.array([bucket(i, exp_name) for i in ids])
    cnt = np.bincount(b, minlength=100)
    print("分桶均匀性 chi2 p =", stats.chisquare(cnt).pvalue)

    # ⭐⭐ ③ A/A 检验:分流【之前】的历史指标两组应该没差异
    if feats is not None:
        for c in feats.columns:
            p = stats.ttest_ind(feats[c][flag], feats[c][~flag]).pvalue
            if p < 0.01:
                print(f"⚠️ 分流前 {c} 就已经有显著差异(p={p:.4f})—— 分流不可信")

第 ③ 条(A/A 检验)是最值钱的一步用分流之前的历史数据比较两组 —— 如果那时候就有显著差异, 说明分流本身有问题,之后所有实验结论都是假的。 🔗 第 12 章会详细讲 A/A。

📊 1% 流量到底够不够

一个常被跳过的算术(详细公式在第 12 章):

基线 CTR 想检出的相对提升 每组需要的样本量 1% 流量、日均 100 万请求时要跑多久
4% +5% ≈ 75 万 150 天 💀
4% +10% ≈ 19 万 38 天
4% +20% ≈ 4.8 万 10 天

结论很反直觉但很重要1% 灰度看不出效果 —— 它压根不是用来看效果的。 1% 灰度是用来看「有没有崩、有没有异常」的; 想看效果,要么放大到 10–50%,要么接受只能检出很大的提升。

⚠️ 由此产生的头号误用在 1% 灰度上看到 CTR "涨了 3%" 就宣布成功。 那个 3% 几乎完全是噪声 —— 这个样本量下 ±8% 的波动都是正常的。

⚠️ 灰度阶段的五个坑

后果 怎么修
只看均值不看分位数 P99 延迟翻倍但均值只涨 5%,被忽略 每档都看 P50/P95/P99
灰度组和对照组不可比 灰度先给了内部员工/新用户 严格随机,做 A/A 检验 ⭐
放量太快 1%→100% 一步到位,出事影响全量 每档至少一个业务周期
多个改动同时灰度 出事了不知道是哪个引起的 一次只放一个变量
灰度期间改了代码 前后半段的"新模型"不是同一个东西 灰度期间代码冻结 💀

🔗 八、和站内其他章的关系

相关的地方 和这一章的关系
推荐算法 15工程落地 推荐场景的降级与放量
智能体工程 16 Agent 的灰度与回滚
Kaggle 22 shake-up 「小流量结论不稳」是同一个统计问题

✅ 检查点

  1. 影子、灰度、A/B 各解决什么问题?正确顺序是什么?
  2. 为什么不能跳过影子直接灰度?
  3. 影子流量最有价值的用法是什么?
  4. 影子链路必须注意什么?举那个真实事故。
  5. 灰度每一档要观察多久?依据是什么?
  6. 灰度分流为什么要按用户哈希、且要加盐?
  7. 回滚的三个条件是什么?哪条最容易翻车?
  8. 为什么工程指标可以自动回滚、业务指标不行?
  9. 降级最容易漏的一点是什么?
  10. 「回滚失败」那个事故里,端到端 MTTR 是多少?和声称的差多少?根本原因是什么?
  11. 回滚演练怎么做?它最常暴露哪五个问题?
  12. 为什么分流哈希不能用 Python 内置的 hash()?线上会表现成什么样?
  13. A/A 检验是什么?为什么说它是分流验证里最值钱的一步?
  14. 1% 灰度能不能看出效果?为什么?它到底是用来干什么的?
👀 答案
  1. 影子验证工程正确性(不影响用户)、灰度验证真实效果(少量用户)、A/B 量化效果差异。顺序:影子 → 灰度 1% → 10% → A/B → 全量。
  2. 因为灰度虽只影响 1%,但如果有工程性 bug(特征算错、维度不对),那 1% 拿到的是彻底错误的结果;而这类 bug 影子能 100% 提前发现
  3. 不只是看会不会崩,而是:记录线上实际用的特征值 → 用同一批样本走离线管道再算一遍 → 逐字段比对。这是发现「训练/推理不一致」唯一可靠的办法。
  4. 影子链路必须禁写。真实事故:影子里的推荐模型写了「已曝光」表,同一物品被记两次曝光,导致真实推荐里被错误过滤掉。
  5. 至少覆盖一个完整业务周期:电商/内容 24 小时(早晚高峰行为完全不同)、To B 一周、有月度周期的一个月。
  6. 按请求随机会让同一用户一会儿新一会儿旧,体验割裂且指标没法解读。加盐是因为多个实验都用 hash(uid)%100永远选中同一批用户,实验之间互相污染 → 用 hash(uid + 实验名)
  7. ①快(5 分钟内)②完整(模型+特征逻辑+配置一起回)③可验证。第②条最容易翻车:只回模型不回特征逻辑,旧模型会拿到它没见过的特征,比不回滚还糟
  8. 工程指标客观(错误率、延迟),可以自动;业务指标有正常波动,也可能因大促/竞品变化,要人看一眼。
  9. 降级本身要有监控和告警。很多系统降级几个月没人知道,因为降级路径不报错、它「正常工作」着。
  10. 声称 5 分钟,实际端到端 MTTR 是 6 小时(模型回滚 4 分钟 + 发现没生效 20 分钟 + 定位 90 分钟 + 特征表回滚重跑 4 小时)。根本原因:v7 顺手改了特征表 schema(加 3 列、改了一列类型),只回模型不回 schema → 特征整体错位,维度对、不报错,回滚后拒绝率反而从 31% 变 38%。
  11. 挑正常工作日下午、不预告具体时间、在灰度环境故意换回上一版、计时到"指标确认恢复"、记下卡住的地方。五个常见暴露:回滚只回了模型、缺权限、没有确认已恢复的手段、手册过期、依赖某个具体的人。
  12. 因为 Python 内置 hash() 对字符串是加盐随机的,进程重启后同一用户会落到不同桶。线上表现是服务每次发版实验分组就洗一次牌,而且不报错。只能用 md5/sha1/murmur 这类确定性哈希。
  13. 用分流之前的历史指标比较两组,如果那时候就有显著差异,说明分流本身有问题。值钱是因为它一票否决——分流坏了的话,之后所有实验结论都是假的。
  14. 看不出。1% 流量、日均 100 万请求、基线 CTR 4% 时,检出 +5% 的相对提升要跑 150 天1% 灰度是用来看"有没有崩、有没有异常"的,不是用来看效果的;这个样本量下 ±8% 的波动都正常,看到"涨了 3%"基本是噪声。

🛑 可以停在这里

走神救援

三个机制本质都是在降低犯错的单价。顺序:影子 → 灰度1% → 10% → A/B → 全量。⭐最常见的错误是跳过影子——灰度只影响1%,但有工程bug时那1%拿到的是彻底错误的结果,而这类bug影子能100%提前发现。⭐⭐影子最有价值的用法不是看崩不崩,是:记录线上实际特征值 → 走离线管道再算一遍 → 逐字段比对,这是发现「训练/推理不一致」唯一可靠的办法。💀影子链路必须禁写(真实事故:影子写了"已曝光"表,同一物品记两次曝光,真实推荐里被错误过滤)。灰度每档要覆盖一个完整业务周期(电商24小时,因为早晚高峰行为完全不同)。分流按用户哈希且要加盐hash(uid+实验名)),否则多个实验永远选中同一批人、互相污染。回滚三条件:快、完整、可验证;⭐最容易翻车是"完整"——只回模型不回特征逻辑,旧模型拿到没见过的特征,比不回滚还糟 → 把模型+特征+配置绑成一个发布单元。工程指标可自动回滚,业务指标要人看(有正常波动)。⚠️降级本身要有监控——很多系统降级几个月没人知道,因为它不报错、"正常工作"着。💀回滚失败的真事故:模型 4 分钟就回去了,但 v7 顺手改过特征表 schema,只回模型不回 schema → 特征整体错位,拒绝率从 31% 变成 38%(比不回还糟),端到端 MTTR 是 6 小时而不是声称的 5 分钟。⭐「我们有回滚方案」和「我们的 MTTR 是 5 分钟」是两回事——后者只能靠演练得出;演练最常暴露:只回了模型、缺权限、没有确认已恢复的手段、手册过期、依赖某个具体的人。⭐发现延迟 + MTTR = 一次事故的实际影响时长。分流:💀绝不能用 Python 内置 hash()(对字符串加盐随机,每次发版实验分组就洗一次牌,且不报错),只用 md5/sha1/murmur;⭐⭐上线前做 A/A 检验——用分流之前的历史指标比两组,那时就有显著差异说明分流本身坏了,之后所有结论都是假的。⭐反直觉但重要:1% 灰度看不出效果(基线 CTR 4%、日均百万请求,检出 +5% 相对提升要 150 天),它是用来看崩不崩的;在 1% 上看到"涨了 3%"基本全是噪声。灰度五坑:只看均值不看分位、灰度组不可比、放量太快、多个改动一起灰度灰度期间改代码

下一节 👉 04-训练推理一致性.md

打卡记录保存在你的浏览器里,首页能看到总进度