文档目录

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、没给显著性
「加了个缓存,应该快了不少」 「应该」+ 没有数据
「测试通过了」 只验证了功能,没验证性能回归与长稳
「这个方案最优雅」 优雅不是收益;没有数据支撑

共同点:缺少可验证的数据。


七、本节小结

  1. 优化的三问:提升多少?代价是什么?回归验证了吗?
  2. 第一问要有三个要素:多个指标、显著性、与噪声底线/MDD 的对比。
  3. 第二问要显式列出代价(复杂度、一致性、资源、风险),并且每个优化都要有降级方案。
  4. 第三问要三种回归:功能正确性、性能回归(A 的优化可能让 B 变慢)、长稳。
  5. 边际效应:收益递减是常态;用四个问题判断该不该继续,其中「有没有更上游的解法」最容易被忽略。
  6. 主动否决是一种被低估的能力:报告里应该有「被否决的方案」及其依据——否决不是永恒的,条件变了要重新评估。
  7. 不合格写法的共同点:缺少可验证的数据。

八、自测

  1. 一个优化让接口 X 的 P99 从 200 ms 降到 50 ms,但你知道它引入了缓存。请说出你必须补充验证的三件事。
  2. 「我们优化了三个月,把报表生成从 30 秒降到 8 秒。」请提出一个可能更有效的方向,并说明为什么。
  3. 为什么「被否决的方案」应该写进报告?请说出至少三个价值。
  1. 必须补充验证的三件事:① 性能回归(其他接口是否受影响)——缓存占用了内存,可能导致 GC 频率上升,其他接口的 P99 可能变差;需要用同一套压测脚本复跑基线场景,确认其他接口无退化(变化小于噪声底线)。② 一致性行为——缓存的更新策略是什么?数据最多延迟多久可见?如果 Redis 不可用,服务是否降级到直查数据库?必须模拟缓存故障,确认服务仍可用(哪怕退化到 180 ms)。③ 长稳与内存——缓存的 maximumSize 是多少?跑了 30 分钟以上的浸泡测试后,内存、缓存大小、命中率是否稳定?(防止缓存无界增长导致 OOM,第 6.6 节模式八)。第四件也值得做:未命中时的延迟——因为 P99 可能落在未命中的那部分请求上(第 7.4 节的数学原理)。
  2. 更有效的方向:**问业务方「这个报表需要实时吗?」**如果业务上可以接受「每天凌晨生成、早上查看」,那么:① 用户等待时间从 30 秒降到 0(打开就是现成的);② 不需要优化计算逻辑(省下三个月的工程投入);③ 生成过程可以放在低峰期,不影响在线服务;④ 甚至可以做得更"重"(比如生成更完整的报表、更多维度),而不必担心延迟。为什么这更有效:① 它属于金字塔的 ① 级(先问要不要做)——收益最大、成本最低;② 优化计算逻辑属于 ② 级,收益是数倍但需要持续投入;③ 「不做」或「换个时间做」常常是最优解——而工程师容易忽略这个方向,因为它不是"技术工作"。注意:这个方向需要和业务方一起决定,属于产品决策而非技术决策——但它应该是性能工作的第一选项。
  3. 三个价值:① 证明你考虑过其他方案——报告里只有"我做了 X",读者无法判断 X 是不是最好的选择;有了"我否决了 Y 和 Z 及其理由",读者能看出你做过比较。② 省下未来的重复试探——同事(或半年后的你)不会再提同样的方案;这是团队层面的知识积累。③ 展示判断力——「知道什么不该做」比「知道什么能做」更难,也更有价值;它说明你不是"碰巧找到一个看起来像的原因"。④ 第四个价值(很重要):否决是有条件的——记录了「因为命中率预估只有 40% 而否决 Redis 缓存」之后,当数据分布变化(命中率可能升到 90%)时,你可以重新评估这个否决是否还成立。没有记录依据的否决,将来无法判断是否该翻案。