有人花两天做了一次"缓存优化实测",最后写出的结论是"新方案比旧方案快"。没人能判断这句话有什么用——因为不知道"快"是指吞吐、延迟,还是 CPU 占用;也不知道"在什么条件下快"。
实测的最大浪费不是测错,而是测完之后发现"这个问题当初就没定义清楚"。这个成本差有个"复利"效果:十分钟的定义能省下两天,而那两天本来可以用于"第二次、第三次测量"。所以"先定义"不是为了严谨,而是为了让后面的时间花在能产出结论的地方。
而定义问题的成本只有十分钟,它决定了后面两天的活是否能产出可用的结论。
1. 问题定义的四个要素
| 要素 | 例子 |
|---|---|
| 比较对象 | A 方案 vs B 方案 |
| 指标 | P99 延迟、吞吐、成本 |
| 条件 | 数据量、并发、硬件 |
| 判定 | 快多少算"更好" |
第四项最常缺:没有判定标准,"快 12%" 与 "快 3%" 都被写成"更快"第一项(比较对象)也会含糊:"新方案 vs 旧方案"里,"旧方案"是哪一版? 若旧方案本身刚改过,比较就失去基准,而这在快速迭代的团队里很常见。
,于是结论无法支撑决策。
第二项要具体到"哪个分位":P50 改善而 P99 变差是常见情形,只说"延迟"会掩盖它。
2. 一个好问题的三个特征
| 特征 | 反例 |
|---|---|
| 有明确的二选一 | "评估一下缓存" |
| 结论能改变一个决定 | "了解一下性能" |
| 失败也有信息量 | "看看能不能更快" |
第二项的判据可以写得更直接:"测完之后,我会因为哪个结果而改变什么"。答不出,就回到定义阶段,而不是开始跑。
第二项最实用:如果结论"无论好坏都不改变任何决定",那这次实测不该做。而这类"为了了解"的测速,往往占了一半以上的工作量。
第三项要求"预设判据":"如果 P99 没降到 80ms 以下,就不换"——这样"没达标"也是一个明确结论。
// 两种问题定义的对比
差:"评估新缓存方案的效果"
好:"在 3000 QPS、100 万行数据的条件下,
新方案能否把 P99 从 120ms 降到 80ms 以下;
若不能,维持旧方案"
// 差别:后者有对象、指标、条件、判定
问题定义写下来之后要"评审一次",成本两分钟,能省掉大部分"测完发现没回答":
// 两分钟自检(五项)
1 对象:A 与 B 具体是什么版本/配置
2 指标:主指标是哪个;约束是哪些
3 条件:数据/并发/硬件/参数/轮数
4 判定:什么结果算"更好"(数字)
5 会改变什么决定:若结论相反,我会做什么
// 任一项答不出 → 回到定义,不要开跑
3. 指标该怎么选
| 场景 | 主指标 |
|---|---|
| 交互式接口 | P99 延迟 |
| 批处理 | 总耗时 |
| 服务容量 | 吞吐(QPS) |
| 成本优化 | 单位成本 |
| 稳定性 | 错误率、超时率 |
第一项与第三项常冲突:吞吐高的方案可能 P99 更差。所以"主指标"要选一个,其余作为约束(如"P99 不退化超过 10%")。第二项(批处理)的"总耗时"要注意"分片":10 个任务并行跑完用 30 分钟,与串行跑完用 300 分钟,是两种完全不同的系统。所以"总耗时"必须配"并发度"一起写。
第五项最容易被忽略:"更快但错误率上升"不是优化。所以任何性能实测都应同时记录错误率。
4. 条件的写法
| 条件 | 必须写 |
|---|---|
| 数据规模 | 行数、平均大小 |
| 并发 | 客户端数、是否闭环 |
| 硬件 | 核数、磁盘类型 |
| 参数 | 非默认项 |
| 时长 | 每轮多久、跑几轮 |
第二项"闭环"很关键:闭环压测自带背压,它的吞吐上限不等于"能扛住的流量"。这一点不写清,结论会被误用。
第四项(非默认参数)最容易被省略,而它常是差异的真正来源:"新方案快 20%"可能是因为新方案用了更大的连接池,而这一点在参数块里没写。
第五项决定"结论是否稳定":单轮 10 秒的结果,噪声可能比差异还大。所以"跑 5 轮取中位数"应当写进方法。
// 条件块的标准写法(五行)
data: 1,000,000 rows, avg 420B, 3% 倾斜
load: 50 clients closed-loop, no think time
hw: 8 vCPU / 32GB / NVMe, 同机部署
params: pool=20, cache=on(其它默认)
runs: 5 轮 × 30s,取中位数与 P99
还有一个"最小可测"的原则:问题定义应当窄到"一次实验能回答"。如果它需要三次实验才能回答,那它其实是一组问题,应当拆开——否则每次实验都在"部分回答",而结论永远是"还要再看"。
// 一个过宽的问题 vs 拆开后的三个
宽:"新架构是否更好"
拆:① 单查询 P99 是否降到 80ms 以下
② 3000 QPS 下错误率是否 ≤0.1%
③ 单位成本是否下降 20% 以上
// 每个都能独立回答;三个都答"是"才换架构
5. 判定标准怎么定
| 类型 | 例子 |
|---|---|
| 阈值型 | "P99 < 80ms" |
| 相对型 | "比现状快 20% 以上" |
| 代价型 | "快 20% 但成本不增" |
| 排除型 | "只要不退化就换" |
第一项(阈值型)的"来源"要写清:"来自产品要求的 200ms 首屏"比"来自我们的经验"更可追责。因为阈值会被质疑,而"来源"是它的依据。
第一项最清晰,但需要"知道目标从哪来":通常来自 SLA 或用户体验阈值,而不是拍脑袋。
第三项在"优化"类项目里最实用:"更快但更贵"通常不是优化,而是换了一种代价。第四项(排除型)看着宽松,其实最实用:"只要不退化就换" 适合"新方案有其它好处(可维护性、生态)"的场景——此时性能不是主目标,而是"不能变差"的约束。
最后一项"事前记录"值得单独做:在开始测之前,把"我的预期"写下来。这一步的价值是"防止事后合理化"——人很容易在看到数据后解释成"本来就该这样",而写下的预期能让"意外"被看见。
// 事前预期(三行,写进记录顶部)
预期:新方案 P99 从 120 → 60ms,吞吐不变
理论依据:去掉了两次序列化
可能的意外:CPU 上升(因为压缩),或长尾变差(因为批量)
// 事后对照:命中/未命中都要写;未命中的部分才是新知识
6. 排错:五种"测完没用"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 结论无法支撑决策 | 无判定标准 | 先写判据 |
| 两个方案"各有优劣" | 主指标没选 | 定一个主指标 |
| 复现不出来 | 条件没写 | 补条件块 |
| 结论被质疑 | 只有单轮 | 多轮取中位 |
| 测完发现问错问题 | 无问题定义 | 先花十分钟 |
第三项(复现不出来)有个"隐性成本":它会让结论被贴上"不可信"的标签,于是后续所有相关结论都要重新验证。而这本可以通过"条件块"避免。
第五项是最大浪费:两天的测量,回答了一个"没人在问"的问题。而它本来可以在十分钟的定义阶段被发现。
7. 边界:什么情况不必写问题定义
| 场景 | 为什么 | 做法 |
|---|---|---|
| 探索性测量("看看什么慢") | 目的就是发现 | 先测再定 |
| 已知数量级差异 | 无需精确 | 粗测即可 |
| 只为自己判断 | 不被引用 | 随意 |
| 复现别人的已知结论 | 问题已被定义 | 照抄其定义 |
判断口径一句话:这个结论会不会被别人拿去支撑一个决定。最后一条与"文档"有关的边界:问题定义应当和结果写在同一份记录里,而不是分开。因为分开后,"结论"会被单独引用,而"条件与判定"留在另一份没人看的文档里。
会,就必须先写问题定义(对象、指标、条件、判定);只为自己看一眼,粗测就行。
8. 自测
- 问题定义的四个要素里,哪一个最常缺?没有它会产生什么后果?
- 一个好问题为什么要求"结论能改变一个决定"?"为了了解"的测速问题在哪?
- 为什么"吞吐高但 P99 更差"需要先定主指标?主指标与约束的关系是什么?
- 条件里"闭环/开环"为什么必须写?"跑几轮"影响什么?
- 四种判定标准里,哪一种在"优化"类项目里最实用?为什么"更快但更贵"通常不算优化?
- 判断"是否需要写问题定义"的那个问题是什么?