灰度的价值不是「更稳」,是「出错时影响的人少一个量级」。 分流要按用户 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 秒实验
- 查一下你们的分流是按用户还是按请求。 如果按请求,找一次线上问题看看它是不是「复现不了」的那种。
- 算一下你们现在全量发布一次,出错的话会影响多少用户。 这个数除以十,就是加一档灰度能省下的量。
- 写下你们三档观察窗口分别要看什么指标。 写不出来的那一档,就是你们实际上没在观察的那一档。
常见误区
- 「灰度就是风险控制,稳一点而已。」 它管的是影响面,不是稳定性。 bug 该出还是出,只是碰到它的人少一个量级。
- 「按请求随机分流更公平。」 公平的是比例,不公平的是体验。 同一个用户来回跳,报上来的 bug 复现不了。
- 「部署快,不需要开关。」 回滚速度和部署速度不是一回事。 回滚要在压力下往回走,那一刻没有「很快」这种东西。
自测三问(点开对答案)
1. 灰度到底解决了什么? —— **影响面。** **bug 该出还是出,只是碰到它的人从全部变成一小部分。** 2. 为什么分流要按用户 ID 哈希? —— **为了让同一个用户始终在同一侧。** **随机会让他来回跳,报上来的 bug 复现不了。** 3. 为什么观察窗口要分三档? —— **因为三类错误的显形时间不一样:崩溃立刻,功能性十分钟,性能和数据问题要等一个业务周期。**最后一篇回到一个数:一次通过率,以及「信息对齐」到底是不是核心。
- ☐ (本篇完,下一篇 11.5 一次通过率:信息对齐是不是核心)