性能回归与门禁:一次发布后 p95 从 320 到 980

周五发布把 API p95 从 320 毫秒推到 980 毫秒,整整 64 小时后才发现。本文复算一年 26 次发布的门禁 3.0 小时、两次真回归 2 小时与一次 6 分钟回滚,并给出四项预算进 CI 的检查表与失败动作。

性能不会自己变好,但会自己变坏。

一、周五 17:00 发布,周一 09:00 的 p95 是 980

九月第二个周五 17:00,司南把「任务活动流」推上线:任务详情页底部多一块动态,显示谁在什么时候改了哪个字段。发布说明三行,测试全绿,CI 只跑构建与单元测试,四分钟就绿了。他合上电脑,周末没再看监控。

周一 09:00,面板上 API p95 从 320 ms 变成 980 ms,是原来的 3.06 倍;列表页仍是 0.35 s,首页 LCP 仍是 1.8 s,两条都没动。变慢的只有详情页这一条路径,而它恰好是五条关键路径里打开次数最多的一条。

从周五 17:00 到周一 09:00 是 64 小时(24 + 24 + 16)。这 64 小时里没有一次告警,因为当时只配了可用性告警:服务没挂,就没人喊。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

性能回归通常不是整体变慢,是一条路径上多了一次顺序扫描。 总览图上的 980 ms 是汇总后的结果,按路径拆开才看得见是哪一条在拖。

他先做的一件事不是改代码,是打开前一天的面板截图对齐:周六周日两条曲线的形状几乎一样,说明不是流量高峰造成的,而是每个请求都多付了一笔固定成本。

没有门禁的发布,等于每次都在赌。 一年 26 次发布里只要有一次带回一个慢查询,赌赢的 25 次也一起白搭。

告警要配在预算上,不是配在可用性上。 服务活着但慢了 3 倍,可用性监控一个字都不会说。

二、一小时定位:一条缺索引的查询

09:00 到 10:00,正好一小时。这一小时里没有改一行业务代码,只加了一个索引。

时间动作结果
09:00–09:15按路径拆分列表 0.35 s、首页 1.8 s 未变,只有详情页抬到 980 ms
09:15–09:35翻慢查询日志(阈值 500 ms)同一条 SQL 反复出现
09:35–09:45对这条 SQL 跑 EXPLAIN活动流表走顺序扫描,没有按项目与时间过滤
09:45–10:00加 (project_id, created_at) 复合索引预发验证后上线,曲线回到 320 ms 附近

四步加起来 15 + 20 + 10 + 15 = 60 分钟,正好是他给自己定的一小时排查时限。

慢查询日志比监控曲线更接近根因。 曲线告诉你哪条路径慢了,日志告诉你慢在哪一句;两者之间隔着一次按路径拆分,这一步不能省。

EXPLAIN 只回答一件事:走没走索引。 它不评价 SQL 写得好不好,也不告诉你该建哪个索引;先看扫描方式,再按过滤条件与排序字段决定列顺序。

修复用了十五分钟,发现用了六十四小时。 这是门禁要解决的比例问题:把发现点从「用户反馈」拉到「合并之前」。

这次回归不是新问题,是旧路径没被覆盖。 详情页一直都在,只是压测清单里只有首页与列表;缺口不在工具,在清单。

三、四项预算进门禁

四项预算的阈值在年初就定了:LCP ≤ 2.0 s、首屏传输 ≤ 200 KB、API p95 ≤ 400 ms、列表页查询 ≤ 5 次。进 CI 时,每一项都要变成一条可失败的断言。

检查工具耗时失败动作
首屏 LCP ≤ 2.0 s(五条路径 p75)无头浏览器合成测试2.5 min阻断合并
首屏传输 ≤ 200 KB构建产物按类型统计0.5 min阻断合并
API p95 ≤ 400 ms(五个端点各 100 次)压测脚本 + Resource Timing3.0 min阻断合并
列表页查询 ≤ 5 次ORM 查询计数 + EXPLAIN1.0 min阻断合并

合计 2.5 + 0.5 + 3.0 + 1.0 = 7 分钟,这是单次发布愿意付的上限;构建与单元测试另需 4 分钟,不计入这笔账。

门禁不看分数,看四个断言。 百分制会把四条线混成一个数,而四条线要分别失败:LCP 超标和查询数超标,修法完全不同,混在一起只会得到一个「再优化一下」的结论。

阈值取预算值,不取当前值。 如果拿 320 ms 当门禁,任何一次正常波动都会变红,两周后所有人学会点「忽略」。预算 400 ms 是承诺,320 ms 是现状,门禁守的是承诺。

五个端点必须含详情页。 这次漏的正是详情页;压测端点按五条关键路径列,不按接口数量列。

失败动作分三档。 超预算阻断合并;超预算但幅度在 10% 以内则记录并当天修;上线后 24 小时内触发告警则直接回滚,不在线上边跑边调。

门禁跑在预发,不跑在本机。 本机结果只用于开发时自查;同一个提交在两台机器上跑出两个数,等于没有数。

门禁结果要留档。 每次发布附一份七分钟报告,一年 26 份;它既是「这次没变慢」的证据,也是下次回归时的对照组。

四、账:5 小时换掉一次 64 小时

复算:
发布 26 次 = 48 周 ÷ 2 的双周发布 24 次 + 补丁 2 次
门禁 26 × 7 分钟 = 182 分钟 = 3.03 h ≈ 3.0 h
单次 7 分钟 = 首屏 2.5 + 体积 0.5 + 接口 3.0 + 查询 1.0
构建与单元测试 4 分钟不在这 7 分钟内,也不计入这笔账
预算告警 3 次:真回归 2 次 × 1 h = 2 h;1 次冷缓存误报重跑落在门禁内
合计 = 3.0 + 2.0 = 5.0 h
占全年 40 h:5 ÷ 40 = 12.5%
占 1,920 h:5 ÷ 1,920 ≈ 0.26%
回滚 1 次 = 部署回退 4 分钟 + 冒烟 2 分钟 = 6 分钟
本次带病窗口 = 周五 17:00 → 周一 09:00 = 24 + 24 + 16 = 64 h
修复耗时 ÷ 发现耗时 = 15 分钟 ÷ 64 h

5 小时占全年投入的 12.5%,换来的是把发现点从 64 小时后移到合并前。 这一次修复只花 15 分钟;真正贵的从来不是修,是不知道。

三次告警、两次真回归,命中率 2/3。 误报那次是在冷缓存下测首屏,重跑即恢复;误报不可怕,可怕的是误报之后把整条门禁关掉。

门禁的成本是固定的,回归的成本是浮动的。 26 次 × 7 分钟一定发生;2 次 × 1 小时是这一年运气好,下一次可能是四个小时,也可能是一次周末。

真正被省下的是注意力。 没有门禁时,每次发布后都要盯半天面板;有了门禁,这半天被压成七分钟,而且不依赖他记不记得。

回滚 6 分钟是兜底,不是主力。 这次用的是热修,加索引十五分钟解决;回滚留给「改错了但一时修不好」的情况。两种能力都要有,但别把回滚当日常。

带病 64 小时的成本没法精确算。 能算的只有两个数:5 小时的门禁,和一次 6 分钟的回滚;两个都远小于「等用户来告诉你」。

五、行动清单与复算验收

清单按「先让门禁跑起来,再让门禁值得信」的顺序排。

  • 四项预算写成 CI 里的四个断言,阈值取预算值。
  • 压测端点按五条关键路径列,详情页必须在里面。
  • 查询计数进构建,单页超过 5 次即失败。
  • 首屏合成测试覆盖五条路径的 p75,不用单次最好值。
  • 门禁报告逐次留档,26 次发布一年一叠。
  • 上线后 24 小时内预算告警自动开单,并指定回滚决策人。
  • 回滚流程每半年演练,目标 6 分钟内回到上一版本。
  • 每次真回归写一页复盘:定位时间、根因、补进哪条门禁。

验收标准:任一预算超阈值时合并被阻断;抽查最近三次发布,都能拿出七分钟内的门禁报告。

使用边界与实操验收

门禁守的是「已知指标不退化」,不负责发现新路径的缺失。

合成测试不等于 RUM。 CI 机器与网络是理想化的,它擅长抓相对变化,不预测用户的绝对体验;两者口径不同,比较只在同一口径内成立。

覆盖缺口比阈值更常见。 这次四项预算都设了,但详情页不在压测清单里,等于没设;每季度复测要专门问一句「有没有新路径没进清单」。

七分钟是可持续的上限。 单次门禁超过十五分钟,绕过的动机就会压过遵守的动机;宁可少压两个端点,也要保住这个时间。

阻断要有例外通道。 紧急修复允许跳过,但必须在 24 小时内补跑并写进发布记录;例外不记录,等于取消门禁。

门禁不替代代码评审。 它只测四个数,测不到可读性、错误处理与边界条件;预算全绿但代码烂的提交,照样应该被拒。

回归的成本会滞后出现。 一次慢查询在低流量时几乎看不出来,等到月末导出高峰才暴露;这也是复测要按季度看趋势、而不是只看单次发布的原因。

本文不覆盖压测工具选型与 CI 平台价格。 只讨论四项预算怎么变成四个断言,以及失败之后做什么。

关键参考与核验范围

以上链接于 2026-10-26 实测可公开访问;未核验版本、功能范围与政策更新。核验日期:2026-10-26。

范围上只讨论性能预算如何进门禁、失败后如何处置与这笔时间怎么算,不讨论持续压测平台、全链路追踪与容量规划。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元