7.8 优化的三问纪律与收益报告
上一节:7.7 JVM 调优 | 下一节:7.9 Lab 7 配套代码:07-optimization-and-validation/08-optimization-discipline
一句话结论
每一项优化都必须回答三个问题:提升多少?代价是什么?回归验证了吗?——答不出第三个问题的优化,本质上是技术债,而不是优化。
一、用「报销凭证」理解三问
公司报销,你需要三样东西:
| 报销 | 优化 |
|---|---|
| 发票(花了多少) | 提升多少(before/after 数据) |
| 用途说明(为什么花) | 代价是什么(复杂度、风险、成本) |
| 审批记录(谁批的) | 回归验证了吗(功能 + 性能 + 长稳) |
缺一样都不能报销——性能优化也一样:缺了"回归验证",这次改动就是一笔没有凭证的开销。
二、三问详解
第一问:提升多少?
必须给出:
| 指标 | before | after | 变化 | 显著性 |
| --- | --- | --- | --- | --- |
| P50 | 20.1 ms | 8.3 ms | -58.7% | p=0.008 ✅ |
| P99 | 210.4 ms | 95.2 ms | -54.7% | p=0.008 ✅ |
| QPS | 498 | 499 | +0.2% | 不显著 |
| 错误率 | 0.05% | 0.03% | -0.02pp | — |
环境噪声底线:P99 ±13.2%,MDD ±26.4%
结论:改善 54.7%,远超 MDD,可信
三个要点:
| 要点 | 为什么 |
|---|---|
| 给多个指标(P50/P99/QPS/错误率) | 只看一个会被误导(比如 P50 改善但 P99 恶化) |
| 给显著性(p 值或置信区间) | 证明差异不是噪声(第 5 章 5.6) |
| 与噪声底线/MDD 对比 | 证明幅度足够大(第 5 章 5.7) |
第二问:代价是什么?
必须显式列出——因为代价不会自己消失:
| 代价类型 | 例子 | 谁来承担 |
|---|---|---|
| 复杂度 | 新增缓存组件、失效逻辑 | 后续维护者 |
| 一致性 | 缓存导致的数据延迟可见 | 用户/业务 |
| 资源 | 内存、额外机器、GPU | 成本预算 |
| 可读性 | IntArray 代替 List、内联优化 |
代码 review |
| 风险 | 新的失败模式(缓存挂了怎么办) | 值班同学 |
| 耦合 | 引入对某个中间件的依赖 | 架构 |
写法示例:
### 代价
1. **新增组件**:引入了 Redis 作为二级缓存(运维成本 + 一个故障点)
2. **一致性**:数据最多延迟 10 秒可见(业务已确认可接受)
3. **内存**:本地缓存占约 200 MB 堆(占堆上限的 10%)
4. **降级方案**:Redis 不可用时自动跳过缓存直查数据库(P99 会退化到 180 ms)
注意第 4 条:每个优化都要有降级方案——否则新引入的组件会成为新的单点。
第三问:回归验证了吗?
三种回归,一个都不能少:
| 回归类型 | 怎么验证 | 漏了会怎样 |
|---|---|---|
| 功能正确性 | 单元测试、集成测试、手工验证关键路径 | 优化引入了 bug |
| 性能回归 | 用同一套脚本复跑基线场景 | 优化 A 导致 B 变慢 |
| 长稳(浸泡) | 30 分钟–1 小时的浸泡测试 | 优化引入了缓慢泄漏 |
特别注意第二种:优化 A 可能让 B 变慢。
例:
给接口 X 加缓存后,X 的 P99 从 200ms 降到 50ms
但缓存占用了内存 → GC 频率上升 → 接口 Y 的 P99 从 80ms 涨到 110ms
→ 如果不做"整体性能回归",你只会看到 X 的改善
验证清单:
### 回归验证
- [ ] 功能:单元测试全通过;手工验证了 X 个关键路径
- [ ] 性能:复跑了基线场景,其他接口无退化(P99 变化 < 噪声底线)
- [ ] 长稳:跑了 40 分钟浸泡测试,内存与连接数稳定
- [ ] 降级:模拟了新组件的失败,服务能降级运行
三、边际效应:什么时候该停
收益递减是常态。当优化从「消除浪费」进入「压榨常数」阶段:
| 阶段 | 典型收益 | 该不该继续 |
|---|---|---|
| 消除浪费(N+1、批处理) | 数倍 | ✅ 继续 |
| 架构调整(并发、隔离) | 数倍 | ✅ 继续 |
| 缓存 | 中(P50 明显) | ⚠️ 评估 P99 是否改善 |
| 微观常数 | 个位数 % | ⚠️ 看是否有更大目标 |
| JVM 参数 | 个位数 % | ⚠️ 通常该停 |
判断「该不该继续」的四个问题
① 用户能感知吗?(P99 从 210 → 205 ms 通常无感)
② 是否解决了 SLO 违约?(还是只是让数字更好看)
③ 引入的复杂度由谁维护?
④ 有没有更上游、更简单的解法?(改需求、改产品策略、降数据量)
第四个问题最容易被忽略:
❌ 「我们优化了三个月,把报表生成从 30 秒降到 8 秒」
✅ 「我们问业务方:这个报表真的需要实时吗?改成每天凌晨生成,
用户第二天早上看,结果 0 秒等待」
「不做这个功能」往往是最优解。
四、主动否决:一种被低估的能力
一份合格的优化报告,应该包含「被否决的方案」。
### 被否决的方案
| 方案 | 预期收益 | 否决理由 |
| --- | --- | --- |
| 加 Redis 缓存 | P50 -70% | 命中率预估仅 40%(key 太分散),P99 无改善;引入一致性风险 |
| 换 ZGC | P99 -5% | 收益小于噪声底线;吞吐可能下降 10%;验证成本高 |
| 手写无锁队列 | 不确定 | 现成的 LongAdder/ConcurrentLinkedQueue 已够用;正确性风险高 |
| 加机器(3→6 台) | 吞吐 ×1.6 | 瓶颈是共享数据库,加副本无效(第 3 章 3.4 的非线性因素) |
为什么这很重要:
| 价值 | 说明 |
|---|---|
| 证明你考虑过其他方案 | 而不是"碰巧找到一个" |
| 省下未来的重复试探 | 别人不会再提同样的方案 |
| 展示判断力 | 「知道什么不该做」比「知道什么能做」更难 |
| 记录否决的依据 | 当条件变化时(比如数据量涨了 10 倍),可以重新评估 |
最后一行特别重要:否决不是永恒的。今天的"不值得"可能是明天的"必须做"——所以要把否决依据记下来,条件变了就重新评估。
五、优化收益报告模板
# 优化收益报告:<标题>
> 关联实验:<EXP_ID> | 日期 | 作者
## 1. 问题与根因
(引用第 6 章的根因结论,含贡献占比)
## 2. 优化措施
### 措施 1:<简述>
- **层级**:金字塔的哪一级(第 7.1 节)
- **改动**:<具体代码/配置改动>
- **原理**:<为什么这样能改善>
## 3. 收益(第一问)
| 指标 | before | after | 变化 | 显著性 |
| --- | --- | --- | --- | --- |
| P50 | | | | |
| P99 | | | | |
| QPS | | | | |
| 错误率 | | | | |
- 环境噪声底线:±__%
- 最小可检测差异:±__%
- 统计检验:Mann-Whitney U,p = ___
- **结论**:改善 __%,☐ 超过 MDD ☐ 未超过
## 4. 代价(第二问)
1. **复杂度**:
2. **一致性**:
3. **资源**:
4. **风险与降级方案**:
## 5. 回归验证(第三问)
- [ ] 功能正确性:<怎么验证的>
- [ ] 性能回归:<其他接口无退化>
- [ ] 长稳:<浸泡测试结果>
- [ ] 降级:<新组件失败时的行为>
## 6. 被否决的方案
| 方案 | 预期收益 | 否决理由 |
| --- | --- | --- |
| | | |
## 7. 未覆盖的场景
- 冷启动路径
- 数据量增长后的表现
- <其他>
## 8. 后续建议
- <下一步优化方向>
- <需要监控的指标>
六、四个常见的不合格写法
| 写法 | 问题 |
|---|---|
| 「优化后性能提升了 20%」 | 没说是哪个指标、没给 before/after、没给显著性 |
| 「加了个缓存,应该快了不少」 | 「应该」+ 没有数据 |
| 「测试通过了」 | 只验证了功能,没验证性能回归与长稳 |
| 「这个方案最优雅」 | 优雅不是收益;没有数据支撑 |
共同点:缺少可验证的数据。
七、本节小结
- 优化的三问:提升多少?代价是什么?回归验证了吗?
- 第一问要有三个要素:多个指标、显著性、与噪声底线/MDD 的对比。
- 第二问要显式列出代价(复杂度、一致性、资源、风险),并且每个优化都要有降级方案。
- 第三问要三种回归:功能正确性、性能回归(A 的优化可能让 B 变慢)、长稳。
- 边际效应:收益递减是常态;用四个问题判断该不该继续,其中「有没有更上游的解法」最容易被忽略。
- 主动否决是一种被低估的能力:报告里应该有「被否决的方案」及其依据——否决不是永恒的,条件变了要重新评估。
- 不合格写法的共同点:缺少可验证的数据。
八、自测
- 一个优化让接口 X 的 P99 从 200 ms 降到 50 ms,但你知道它引入了缓存。请说出你必须补充验证的三件事。
- 「我们优化了三个月,把报表生成从 30 秒降到 8 秒。」请提出一个可能更有效的方向,并说明为什么。
- 为什么「被否决的方案」应该写进报告?请说出至少三个价值。
- 必须补充验证的三件事:① 性能回归(其他接口是否受影响)——缓存占用了内存,可能导致 GC 频率上升,其他接口的 P99 可能变差;需要用同一套压测脚本复跑基线场景,确认其他接口无退化(变化小于噪声底线)。② 一致性行为——缓存的更新策略是什么?数据最多延迟多久可见?如果 Redis 不可用,服务是否降级到直查数据库?必须模拟缓存故障,确认服务仍可用(哪怕退化到 180 ms)。③ 长稳与内存——缓存的
maximumSize是多少?跑了 30 分钟以上的浸泡测试后,内存、缓存大小、命中率是否稳定?(防止缓存无界增长导致 OOM,第 6.6 节模式八)。第四件也值得做:未命中时的延迟——因为 P99 可能落在未命中的那部分请求上(第 7.4 节的数学原理)。 - 更有效的方向:**问业务方「这个报表需要实时吗?」**如果业务上可以接受「每天凌晨生成、早上查看」,那么:① 用户等待时间从 30 秒降到 0(打开就是现成的);② 不需要优化计算逻辑(省下三个月的工程投入);③ 生成过程可以放在低峰期,不影响在线服务;④ 甚至可以做得更"重"(比如生成更完整的报表、更多维度),而不必担心延迟。为什么这更有效:① 它属于金字塔的 ① 级(先问要不要做)——收益最大、成本最低;② 优化计算逻辑属于 ② 级,收益是数倍但需要持续投入;③ 「不做」或「换个时间做」常常是最优解——而工程师容易忽略这个方向,因为它不是"技术工作"。注意:这个方向需要和业务方一起决定,属于产品决策而非技术决策——但它应该是性能工作的第一选项。
- 三个价值:① 证明你考虑过其他方案——报告里只有"我做了 X",读者无法判断 X 是不是最好的选择;有了"我否决了 Y 和 Z 及其理由",读者能看出你做过比较。② 省下未来的重复试探——同事(或半年后的你)不会再提同样的方案;这是团队层面的知识积累。③ 展示判断力——「知道什么不该做」比「知道什么能做」更难,也更有价值;它说明你不是"碰巧找到一个看起来像的原因"。④ 第四个价值(很重要):否决是有条件的——记录了「因为命中率预估只有 40% 而否决 Redis 缓存」之后,当数据分布变化(命中率可能升到 90%)时,你可以重新评估这个否决是否还成立。没有记录依据的否决,将来无法判断是否该翻案。