🛠️ 第11章 场景三:跨前后端功能开发
📄 一句话概括

灰度的价值不是「更稳」,是「出错时影响的人少一个量级」。 分流要按用户 ID 哈希,不是每次请求随机——随机会让同一个用户来回跳。 开关、灰度、回滚三件套,最容易被省掉的是开关。

📝 本章和前面几章的关系

上一篇把库改对了。这一篇把代码放出去。 「放出去」这件事,一次全量和五步灰度,区别不在勇气。 Demo 先量两种分流方式的稳定性:哈希分流同一用户百分之百同侧,随机分流只有不到六成。 再跑五步灰度,看整体错误率什么时候越过百分之一的阈值。

一、开关、灰度、回滚三件套

组件 作用 没有它会怎样
开关 不部署就能关掉新逻辑 回滚要重新发版
灰度 按比例放给一部分人 开关有,但一开就是全量
回滚 出问题时快速退回 只能靠人手动改

三样里最容易被省掉的是开关。 理由通常是「我们部署很快」—— 但回滚速度和部署速度不是一回事:部署是往前走,回滚要在压力下往回走。 Demo 里那套灰度是纯计算:给定新版本错误率和放量比例,算整体错误率。 放到百分之二十五时,整体错误率已经是基线的四倍多,但还没越过阈值。 放到百分之五十才越线。 这个细节值得停一下:如果用「相对基线涨了多少」当判据,你在百分之二十五就该停了; 用「绝对值超过百分之一」当判据,你能撑到百分之五十。 两个判据都能用,但要事先说清是哪一个。

二、一次只放一类人

分流方式有两种,差别很大。

方式 同一用户稳定性 问题
按用户 ID 哈希 始终同侧 —
每次请求随机 来回跳 bug 复现不了

Demo 里量的是「五次请求都落在同一侧的用户占比」:哈希分流百分之百,随机分流不到六成。 随机分流看起来更公平,实际最糟。 同一个用户第一次看到新版、第二次看到旧版,他报上来的 bug 你复现不了—— 因为你不确定他当时在哪一侧。 哈希分流的好处是可复现:用户 ID 一亮,他就永远在同一个桶里。 排查时可以精确地说「这个用户在新版本上」,不用猜。 「一次只放一类人」的另一种实现是放内部员工。 他们能忍 bug,也能说清现象,而且比例天然很小。 这是最便宜的第一档灰度。

三、观察窗口

观察多久 能看到什么
立刻 崩溃和 500
十分钟 大部分功能性错误
一天 跟时段相关的错误和慢查询

三档对应三种错误,只观察立刻那一档,等于只防住了最响的那类。 「响」和「严重」不是一回事。 崩溃很响,但影响明确;慢查询不响,但它会拖垮整个库。 所以观察窗口要跟错误类型配: 功能性错误十分钟能看出来,性能问题要等一个业务高峰,数据一致性问题可能要等一次对账。 一个实用做法:放量之后不要急着下一步,先等一个完整的业务周期。 如果你们的业务有明显早晚高峰,那「等一天」不是保守,是必要。

四、动手 Demo:渐进式交付:开关、灰度、回滚

先量两种分流方式的稳定性,再跑一次五步灰度,看错误率什么时候越线。 两千个用户,哈希分流用 crc32 取模,随机分流用 random——两边都真的跑一遍, 数出「五次请求都同侧」的用户占比。

def bucket(uid):
    return zlib.crc32(str(uid).encode("utf-8")) % 100

# 五步灰度:给定新版本错误率,算整体错误率
NEW_ERR, OLD_ERR = 0.03, 0.002
for pct in (1, 5, 25, 50, 100):
    n_new = USERS * pct // 100
    rate = (NEW_ERR * n_new + OLD_ERR * (USERS - n_new)) / USERS
    print(pct, "%d 人" % n_new, "%.2f%%" % (rate * 100),
          "回滚" if rate > 0.01 else "放行")

实跑输出:

--------------------------------------------------------------------
渐进式交付:开关、灰度、回滚
--------------------------------------------------------------------

  一次全量发布和五步灰度,差别不在勇气,在「出错时影响多少人」。

  ① 先看分流稳不稳
  --------------------------------------------------------------------
| 分流方式 | 五次请求都同侧的用户占比 | 问题 |
|---|---:|---|
| 按用户 ID 哈希 | 100.0% | 同一用户始终同侧 |
| 每次请求随机 | 59.2% | 同一用户来回跳 |

  ! 随机分流看起来更公平,实际最糟:同一个用户一会儿看到新版一会儿看到旧版,
  报上来的 bug 复现不了。
  哈希分流的好处是可复现——用户 ID 一亮,他就永远在同一个桶里。

  ② 五步灰度,看错误率什么时候越过阈值
  --------------------------------------------------------------------
| 放量 | 新版本用户 | 整体错误率 | 动作 |
|---|---:|---|---|
| 1% | 20 | 0.23% | 放行 |
| 5% | 100 | 0.34% | 放行 |
| 25% | 500 | 0.90% | 放行 |
| 50% | 1000 | 1.60% | 超阈值,回滚 |
| 100% | 2000 | 3.00% | 超阈值,回滚 |

  放到 50% 时越过百分之一的阈值,触发回滚。此时受影响的是 1000 人,不是 2000 人。
  ! 这就是灰度的全部价值:把一次可能的全量事故,换成一次只影响 50% 用户的事故。
  注意第三步那组:百分之二十五放量时错误率已经涨到基线的四倍多,但还没越过阈值。
  所以阈值定多少是个取舍:定得低,误回滚多;定得高,等它越过时已经放出去一大片了。

  ③ 三件套缺一不可
  --------------------------------------------------------------------
| 组件 | 没有它会怎样 |
|---|---|
| 开关 | 回滚要重新部署,十几分钟 |
| 灰度 | 开关有,但一开就是全量 |
| 回滚 | 发现问题时只能靠人手动改回去 |

  三样里最容易被省掉的是开关。
  因为「我们的部署很快」听起来像理由,但回滚速度和部署速度不是一回事——部署是往前走,
  回滚要在压力下往回走。

  ④ 观察窗口
  --------------------------------------------------------------------
| 观察多久 | 能看到什么 |
|---|---|
| 立刻 | 崩溃和 500 |
| 十分钟 | 大部分功能性错误 |
| 一天 | 跟时段相关的错误和慢查询 |

  三档对应三种错误。只观察立刻那一档,等于只防住了最响的那类问题。

那张分流表里,随机分流的「不到六成」是个可以算出来的数: 五次都落同侧的概率是 0.1 的五次方加 0.9 的五次方,约等于零点五九。 换句话说,四分之一的用户在这五次请求里会来回跳。 灰度的数字要连着看:1% 到 25% 一路放行,50% 越线。 如果只看「放了几步」,会觉得走到第四步才出事; 但看错误率那一列,第三步的时候已经是基线的四倍多了。 问题早就在,只是没越过你定的那条线。 「受影响的是 1000 人,不是 2000 人」这句是全篇的落点。 灰度不减少 bug,它减少的是「有多少人碰到这个 bug」。 最后那张观察窗口表要看第三行。 「一天」这一档最贵,但它对应的是最难查的那类问题。 三档里省掉哪一档,都有一类问题会漏过去。

30 秒实验

  1. 查一下你们的分流是按用户还是按请求。 如果按请求,找一次线上问题看看它是不是「复现不了」的那种。
  2. 算一下你们现在全量发布一次,出错的话会影响多少用户。 这个数除以十,就是加一档灰度能省下的量。
  3. 写下你们三档观察窗口分别要看什么指标。 写不出来的那一档,就是你们实际上没在观察的那一档。

常见误区

  1. 「灰度就是风险控制,稳一点而已。」 它管的是影响面,不是稳定性。 bug 该出还是出,只是碰到它的人少一个量级。
  2. 「按请求随机分流更公平。」 公平的是比例,不公平的是体验。 同一个用户来回跳,报上来的 bug 复现不了。
  3. 「部署快,不需要开关。」 回滚速度和部署速度不是一回事。 回滚要在压力下往回走,那一刻没有「很快」这种东西。
自测三问(点开对答案) 1. 灰度到底解决了什么? —— **影响面。** **bug 该出还是出,只是碰到它的人从全部变成一小部分。** 2. 为什么分流要按用户 ID 哈希? —— **为了让同一个用户始终在同一侧。** **随机会让他来回跳,报上来的 bug 复现不了。** 3. 为什么观察窗口要分三档? —— **因为三类错误的显形时间不一样:崩溃立刻,功能性十分钟,性能和数据问题要等一个业务周期。**

最后一篇回到一个数:一次通过率,以及「信息对齐」到底是不是核心。

相关笔记