P99 长尾出现时,第一刀到底该砍根因还是砍隔离?聊聊「治」和「防」的优先级
帖子正文与楼层回复。
背景
做了 15 年性能优化,我最近带团队复盘 3 起 P99 毛刺事故,发现一个反复出现的分歧:同一个现象(P99 从 80ms 抖到 800ms+),不同团队的第一反应完全不一样。
复盘 3 个真实场景:
场景 A:一个 4C8G 的 Java 服务,G1 在 4G 堆上做 mixed GC,P99 间歇性飙 3 倍。第一版方案是「加熔断 + 降级」,第二版才是 GC 调参 + 堆扩到 6G。第一版上线后 P99 反而更难看——熔断把慢请求切走了,但慢请求的根因(GC)还在,只是被「藏」进了降级分支,业务方看到的报错率上升了 40%。
场景 B:下游一个慢 SQL 把连接池打满,P99 从 120ms 抖到 2s+。团队先做了 SQL 改写 + 加索引,P99 回到 130ms;两周后慢 SQL 换个入口又复发了,这次他们补上了熔断 + 超时,P99 稳定在 150ms(熔断兜底)。
场景 C:一次 Kafka 事务生产者 initTransactions 偶发 8 秒卡顿,团队没去改 Kafka 配置,而是把事务窗口从 30s 拉到 120s,让偶发卡顿被「吞」掉。上线后 P99 达标,但事故率没降——只是从「可见的错误」变成了「不可见的延迟」。
常见分歧(我看到的 3 派)
- 根因派:长尾就是 bug,先 EXPLAIN、先 jstack、先 arthas trace,找到根因再动手。隔离是「掩耳盗铃」。
- 隔离派:根因永远治不干净(下游、内核、GC 都不是你能控的),先把熔断 / 超时 / 降级 / 隔离做扎实,根因慢慢治。
- 场景派:看长尾的形态——偶发(一天几次)治根因,结构性(持续存在)做隔离。
我的倾向(不一定对,想听大家打脸)
我倾向「场景派」,但会加一条判据:看长尾的「可归因性」。
- 如果长尾能在 30 分钟内定位到具体代码行 / SQL / 配置项,优先治根因——因为隔离方案会引入新的复杂度(降级分支、熔断状态机、重试幂等),长期维护成本比修一次根因高。
- 如果长尾 1 周内定位不到、或者根因在「不可控层」(GC、内核、第三方 SDK、云厂商),优先做隔离——这时候隔离不是「掩耳盗铃」,是「承认边界」。
- 一个反直觉的观察:隔离方案本身也会成为新的长尾来源(熔断状态切换、降级分支的额外分支、重试带来的二次压力)。所以「先隔离再治根因」的顺序,长期看未必比「先治根因再补隔离」便宜。
想听大家聊的几个具体问题
- 你们线上真的按「偶发 vs 结构性」这个判据来选吗?还是基本拍脑袋?拍的时候依据是什么?
- 有没有「先做隔离、后来发现根因根本不存在」的案例?隔离方案是不是有时候就是「正确的答案」?
- 你们团队里「根因派」和「隔离派」吵架时,最后谁赢?赢的依据是数据还是嗓门?
- 一个更具体的:如果下游是你们自己团队的另一个服务(可控),和下游是第三方 SDK(不可控),你们的第一刀会不一样吗?
我自己最纠结的是场景 C 那种「把错误藏成延迟」的做法——P99 达标了,但用户实际拿到的是「慢 120 秒的成功」还是「30 秒的错误」?从 SLO 角度哪个更该被惩罚?这个我觉得社区里还没人正面回答过。
欢迎带具体案例来拍砖,越具体越好(QPS、P99 数值、隔离手段、上线前后对比),空谈「要看场景」的我不接 😄