文档目录

8.1 为什么需要门禁

上一节:无 | 下一节:8.2 CI 微基准门禁 配套代码:08-continuous-performance/01-why-gates


一句话结论

90% 的性能问题不是「某次大改搞坏了」,而是「几十次小改动各退化 3%,累积起来翻了一倍」。 这种退化逃过所有人工评审(因为每次都在噪声范围内),只有自动化的门禁能发现它。


一、用「慢性病」理解渐进退化

一次急性心梗,你会立刻去医院。但高血压、高血脂、血糖偏高——每年只涨一点,你毫无感觉。

十年后,它们一起导致了严重问题。而如果每年体检 + 指标超阈值就干预,就能避免。

健康管理 性能管理
急性病 → 立刻就医 线上故障 → 立刻排查(第 6 章)
年度体检 + 指标趋势 门禁 + 趋势监控(本章)
血压 > 140 就干预 微基准退化 > 15% 就失败
家庭血压计(日常监测) 生产告警(实时监测)

关键:急性病靠医生,慢性病靠机制。 性能工作的大部分价值在后者。


二、渐进退化的四种典型来源

❶ 新增功能带来的额外工作

v2.3:新增"用户画像"接口,每次请求多查一次数据库
     → 订单接口的 P99 从 95ms 涨到 101ms(+6%)
     → 在噪声范围内,code review 也看不出来

为什么难发现:加一次查询看起来"很合理",而且只涨 6%。

❷ 数据量增长

v2.3 时:订单表 100 万行,某查询走索引,8ms
v2.8 时:订单表 800 万行,同一个查询因为统计信息/选择性变化
         改走全表扫描,45ms(+460%)

为什么难发现:代码一行没改,是数据让执行计划变了。

❸ 配置漂移

某次发布:
  - 日志级别从 WARN 改成 INFO(为了排查另一个问题,忘了改回来)
  - 连接池从 20 改成 10("节省数据库连接")
  → P99 慢慢涨,但没人把这些配置改动和性能联系起来

❹ 依赖演进

- 某个下游服务上线了新版本,响应时间从 20ms 涨到 50ms
- 数据库从 SSD 换成了网络盘(成本优化)
- JDK 从 17 升到 21(大部分情况更快,但个别场景可能退化)

共同点:每一步都"看起来没问题",累积起来才成为问题。


三、为什么「人工评审」拦不住

拦不住的原因 说明
单次退化在噪声内 3%–15% 的退化,用什么方法都说不清是真的还是噪声
评审看的是代码,不是性能 “加一次查询"在代码上完全合理
跨时间的因果关系难追踪 三个月的 20 次改动,谁导致的总退化?
没人负责整体指标 每个 PR 只对自己负责,没人看"三个月趋势”
人的记忆有限 “上次 P99 是多少来着?”

所以必须自动化:

❌ 「大家注意,改代码时留意性能」
✅ 「代码提交后,CI 自动跑基准,与基线对比,退化 > 15% 就失败」

四、门禁的四种形态

形态 触发时机 检测什么 成本
CI 微基准门禁 每次提交/合并 核心算法、序列化、启动时间 低(分钟级)
CI 全链路门禁 主干合并/发布前 端到端 SLO、吞吐 高(小时级)
生产实时告警 持续 SLI 恶化、饱和度异常 中(需要监控体系)
定期回归测试 每周/每月 趋势退化、容量变化 中(专用环境)

四层配合:

① CI 微基准    → 拦住"代码级"退化(最便宜的防线)
② CI 全链路    → 拦住"集成级"退化(发布前的最后一道)
③ 生产告警     → 拦住"逃过前两道"的问题(实时兜底)
④ 定期回归     → 发现"缓慢漂移"(数据量、依赖变化)

注意顺序:从便宜的开始。不要一开始就搞全链路 CI(成本高、维护难),先把微基准门禁做起来。


五、门禁的经济性

门禁的价值 = 提前发现问题的收益 - 门禁本身的成本

收益

发现问题的时间点 修复成本
提交时(CI) 几分钟(开发者还在上下文里)
code review 时 几十分钟
发布后(测试环境) 几小时
线上(用户受影响) 几天 + 信誉损失 + 可能的赔偿

提前发现的收益是数量级的。

成本

成本项 量级
CI 运行时间 微基准几分钟;全链路几十分钟
环境成本 微基准用 CI runner;全链路需专用机器
维护成本(最大) 基线更新、阈值调整、误报处理
误报的隐性成本 一旦被忽视,门禁等于不存在

最大的风险:误报导致门禁失效

① 阈值设成噪声底线 → 每天误报 3 次
② 工程师开始认为"这个门禁不重要,重跑一下就好"
③ 三个月后出现真实退化 → 也没人看
④ 门禁实际上已经死了,但组织以为它有保护作用

这比没有门禁更糟——因为它给人虚假的安全感(第 5 章 5.7 节)。

所以核心原则:宁可漏报,不可误报。


六、一个现实的落地顺序

如果你现在什么都没有,按这个顺序建:

第 1 步:CI 微基准门禁(1 周)
   → 选 3–5 个核心算法/序列化方法
   → 测噪声底线,阈值 = 噪声 × 1.5~2
   → 先只做"报告"不做"失败"(观察两周,确认零误报)

第 2 步:生产 SLO + 饱和度告警(2 周)
   → 定义 SLO(第 1 章)
   → 加饱和度和 GC 告警(领先指标)
   → 仍然暂缓"延迟告警"(容易误报)

第 3 步:定期回归(1 个月后)
   → 每月跑一次容量曲线,记录拐点
   → 观察趋势(数据量增长的影响)

第 4 步:全链路门禁(视需要)
   → 只在有专用环境、且前 3 步稳定运行后才做

注意第 1 步的最后一句话:先只报告不失败——这是避免门禁被过早讨厌的关键。


七、本节小结

  1. 90% 的性能问题是渐进退化:每次只退化 3%–15%,累积起来翻倍。
  2. 四种来源:新增功能、数据量增长、配置漂移、依赖演进。
  3. 人工评审拦不住:单次退化在噪声内、跨时间因果难追踪、没人负责整体趋势。
  4. 门禁的四种形态:CI 微基准、CI 全链路、生产告警、定期回归——从便宜的开始。
  5. 提前发现的收益是数量级的(几分钟 vs 几天)。
  6. 最大的风险是误报:一旦被忽视,门禁等于不存在,而且给人虚假的安全感。
  7. 落地顺序:先微基准(先报告不失败)→ 生产 SLO 与饱和度告警 → 定期回归 → 全链路门禁。

八、自测

  1. 一个服务的 P99 在 12 个月里从 95 ms 涨到 400 ms,代码 review 每次都通过了。请解释这个过程,以及为什么人工评审拦不住它。
  2. 为什么「门禁误报」比「门禁漏报」更危险?请说明它对团队行为的影响。
  3. 如果你的团队现在只有「上线前手动压一次」,请给出一个三步的改进路径,并说明每一步的预期收益。
  1. 过程:12 个月里有几十次改动,每次只带来 3%–15% 的退化——例如新增功能多查一次数据库、数据量增长导致执行计划变化、日志级别被调成 INFO 忘了改回、某个下游变慢。每次退化都在噪声范围内(第 5 章 5.7 节),而且代码 diff 看不出来(“加一次查询"在代码上完全合理)。为什么人工评审拦不住:① 单次 3%–15% 的变化无法用一次实验判定真假;② 评审看的是代码正确性,不是性能;③ 跨时间的因果难以追踪——12 个月 40 次改动,谁导致的总退化?④ 没人负责"三个月趋势"这个指标,每个 PR 只对自身负责;⑤ 人的记忆有限,没人记得 12 个月前的 P99 是多少。唯一能拦住它的机制:自动化门禁(提交时对比基线)+ 趋势监控(定期回归)。
  2. 误报更危险,因为它会摧毁门禁的可信度,而可信度是门禁唯一的价值来源。具体的行为链条:① 阈值设得太紧(等于噪声底线)→ 每天误报几次;② 工程师发现"重跑一次就过了”,于是把门禁失败当作噪声忽略;③ 三个月后出现真实退化,同样被当作噪声忽略;④ 门禁实际上已经失效,但组织以为它还在保护(虚假的安全感)。漏报的代价是小得多的:漏掉的退化会被下一道防线(生产告警、定期回归)或下一次事件发现。所以核心原则是「宁可漏报,不可误报」——阈值必须高于噪声底线(通常 ×1.5–2),并且要用实测的噪声底线来验证零误报。
  3. 三步改进路径:第 1 步(1 周):建 CI 微基准门禁——选 3–5 个核心算法/序列化方法,用 JMH 或 kotlinx-benchmark 测出基线;先只报告不失败(观察两周确认零误报),再开启失败。预期收益:拦住"代码级"退化(算法退化、序列化变慢、引入装箱等),成本最低、见效最快。第 2 步(2 周):加生产 SLO + 饱和度告警——先定义 SLO(P99 + 错误率 + 负载前提),然后优先加饱和度告警(连接池 pending、队列深度、GC 停顿、容器节流),因为这些是领先指标,比延迟告警更早、更稳定。预期收益:把"用户已受影响"变成"系统在告警",提前几分钟到几小时发现。第 3 步(1 个月后):定期回归 + 容量复测——每月跑一次容量曲线,记录拐点与数据量,观察趋势。预期收益:发现"缓慢漂移"(数据量增长导致执行计划变化、依赖变慢),这是前三道防线都容易漏掉的。注意:全链路 CI 门禁应该放在最后(成本高、维护难),不要一开始就做。