一次"加了缓存"的实测显示 P99 从 210ms 降到 90ms,团队据此认为"缓存有效"。而两周后另一个团队在同一系统上只测出 30ms 的改善——因为那两周里还上了"索引优化",而它贡献了一半。
实测最常见的逻辑错误不是"测错数",而是"没有对照组,于是把"所有变化"归给"我做的那一个变化"。这个判断有点反直觉:测量精度提高 10% 的收益,远小于"加一个对照组"的收益。因为前者只是让数字更准,后者决定"这个数字指向谁"。
而对照组的设计比测量本身更决定结论的有效性。
1. 三种"对照"的强度
| 设计 | 强度 |
|---|---|
| 前后对比(同一环境,改前/改后) | 弱(时间带来的变化混进来) |
| A/B(同时跑两种,只差一个变量) | 强 |
| 交替(A/B 交替跑,摊平漂移) | 最强(成本高) |
第一项最常用也最弱:"改之前"与"改之后"之间,还发生了很多事第一项的"弱"不在"不严谨",而在"它无法排除":哪怕你确信"只有我改了",也无法排除"数据增长"与"流量变化"——而这两项在真实系统里永远在变。
(其它发布、数据增长、外部流量变化)。
第二项是"能证明因果"的最低要求:两组之间只差"那一个变量"。
2. 前后对比为什么不可靠
| 混入因素 | 影响 |
|---|---|
| 同时上线的其它改动 | 归因错误 |
| 数据量增长 | 前后不可比 |
| 外部流量结构变化 | 负载不同 |
| 环境漂移(温度、缓存) | 噪声变化 |
第一项的"叙事偏差"值得单独说:人倾向于把好结果归给自己熟悉的那个改动,而"最有名"往往等于"写代码的人最有话语权",于是归因变成了权力问题。
第一项在"密集发布"的团队里几乎必然发生:两周内合了 20 个 PR,而"改善"被归给了最有名的那一个。
第三项在"大促前后"最明显:改前是平常流量,改后是大促流量,于是"变慢"可能完全是流量导致的。第三项还有一个"时序"陷阱:改动的上线时间恰好与"某季节性事件"重合(月末结算、开学、节假日),于是"改前 vs 改后"实际是"淡季 vs 旺季"。
3. A/B 的三种落地方式
| 方式 | 说明 |
|---|---|
| 双跑(同一请求跑两条路径) | 精确,但成本翻倍 |
| 灰度分流(按用户/流量) | 真实,但需观测对齐 |
| 影子流量(复制请求到新路径) | 真实且安全,需额外容量 |
第二项的"10%"不是随便取的:太小(1%)样本不足,太大(50%)风险过高。经验值是"5%–20%",且要跑够"至少覆盖一个业务周期"(如完整一天)。
第二项最实用:10% 流量走新路径、90% 走旧路径,同时对比两组指标。它需要"两组流量的构成相似"(随机分流,不是按用户 ID 尾号)。
第三项在"新路径有风险"时用:复制真实请求到新实现,只比对结果不返回。代价是双倍计算。第三项的独特价值是"能比对结果":它不仅比"快慢",还能比"新旧实现的输出是否一致"——于是它能同时发现"性能改善"与"语义偏差"。
// 灰度分流的三个前提(缺一项结论就不可靠)
1 随机分流(按 hash(uid) 取模,而不是"先来的走 A")
2 两组同时段对比(不是"上周 vs 本周")
3 指标口径一致(同一采集点、同一分位)
"分流"必须按"不可伪造的键"做,否则两组会不可比:
// 三种分流键的可用性
hash(uid) % 100 < 10 → 同一用户始终在 B 组(体验一致,样本独立)推荐
hash(request_id) % 100 → 同一用户会在两组间跳,指标被"平均"(不可用)
按时间切(前 10 分钟 B) → 与"流量结构随时间变化"混淆(不可用)
// 判据:分流要"与人绑定",而不是"与请求绑定"
4. "只差一个变量"怎么保证
| 做法 | 效果 |
|---|---|
| 一次只改一个 | 需要纪律 |
| 特性开关(可分别开关) | 最有效 |
| 分支对比(两套部署) | 强,但成本高 |
第二项的落地细节是"开关的粒度与"可观测"对齐":一个开关应当对应"一个能被单独观测的改动",否则"开了但分不清谁起作用"。
第二项是"工程化的对照":把每个改动放在一个独立开关后面,于是"只开 A"与"只开 B"可以分别测。它把"对照"从"排期问题"变成"配置问题"。
代价是"代码里多分支",所以"开关要有到期时间"(与前面的开关台账一致)。
5. 怎么报"贡献度"
| 内容 | 必须写 |
|---|---|
| 对照组设置 | 谁与谁比 |
| 单变量是什么 | 唯一差异 |
| 各自绝对值 | 不只有"改善%" |
| 是否有交互 | 两者一起上是否更差 |
第四项最容易被忽略:两个优化同时上,可能互相抵消第四项的典型例子:"加缓存"减少了数据库压力,于是"加索引"的收益看起来变小——两者都有效,但"合并收益"远小于"各自收益之和"。
(如"加缓存"与"改批量"都改了同一段路径)。所以"分别测"之后还要"合起来测一次"。
// 一次合格的归因报告(四行)
对照:A 组(旧路径,90% 流量)vs B 组(仅开缓存,10% 流量)
单变量:只开缓存开关,索引与批量均未改
结果:P99 210ms → 148ms(-30%),P50 基本不变
交互:A+B 同开时 P99 = 132ms(叠加有效,无抵消)
"混入因素"可以先用"排除法"清一遍,比"加对照"更便宜:
// 四问排除(每条一分钟)
1 同期有没有别的发布? 有 → 归因不可靠
2 数据量有没有变? 变 → 前后不可比
3 流量结构有没有变(渠道/时段)?变 → 同上
4 环境有没有变(机器/参数)? 变 → 噪声变了
// 任一为"是" → 必须做 A/B,而不是前后对比
6. 排错:五种"归因错误"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 改善比预期大很多 | 混入其它改动 | 用 A/B 重测 |
| 别人复现不出 | 环境/时段不同 | 固定对照 |
| 两项优化"各有效"但合起来更差 | 交互 | 加"合起来测一次" |
| 只有"改善%"没有绝对值 | 报告不全 | 补绝对值 |
| 小样本上"显著" | 噪声 | 先测噪声幅度 |
第四项(只有改善%没有绝对值)会让"结论强度"不可判断:"改善 30%"可能是 210→147ms,也可能是 3→2.1ms——而后者对体验毫无意义。
第五项与"样本量"是同一个问题:差异小于噪声时,"显著改善"只是巧合。
最后一个"归因记录"的模板:把"对照设置"写进实测记录的固定字段,于是"能不能信"变成"读一下就知道",而不是"问作者"。
// 实测记录里的归因块(四行)
control: "A 组旧路径 90% vs B 组仅开缓存 10%(按 hash(uid) 分流)"
single_variable: "只有 cache.enabled 不同"
result: { p99_a: 210, p99_b: 148, p50_a: 62, p50_b: 60 }
interaction: "A+B 同开 p99=132(叠加,无抵消)"
7. 边界:什么情况前后对比就够
| 场景 | 为什么 | 做法 |
|---|---|---|
| 改动极大(10 倍级) | 混入因素影响小 | 前后对比 |
| 独立环境(无其它发布) | 无混入 | 前后对比 |
| 只判断"方向"不判断"幅度" | 不需要精确 | 前后对比 |
| 一次性验证(不对外报数) | 结论不被引用 | 前后对比 |
判断口径一句话:这个结论会不会被别人拿去支撑一个决定。最后一条与"团队"有关的边界:对照组是"工程纪律",而不是"额外工作"。做法可以很轻:上线时"只开一半流量",观察一小时再全量——这个动作顺便就产生了对照。
会,就必须有对照(A/B 或交替);只自己看一眼,前后对比可接受。
8. 自测
- 三种对照设计的强度差别是什么?为什么"前后对比"最弱?
- "前后对比"会混入哪四类因素?为什么密集发布时几乎必然出错?
- A/B 的三种落地方式里,哪一种最实用?它的三个前提是什么?
- "特性开关"为什么能把"对照"从排期问题变成配置问题?它的代价是什么?
- 为什么"两项优化分别有效"还要"合起来测一次"?
- 判断"是否需要对照"的那个问题是什么?