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