【背景】 做了 5 年性能优化,处理过 20+ 次线上 P99 毛刺事故。发现一个有趣的规律:每次毛刺发生时,团队的第一反应高度一致——「肯定是 GC」「肯定是慢 SQL」「肯定是网络抖动」。但复盘之后,真正根因往往在另一个地方。
我印象最深的一次:某支付服务 P99 从 80ms 飙到 350ms,持续 40 分钟。团队第一反应是 GC(因为同一时段 Full GC 日志出现了 2 次),排查 3 小时无果。最终发现是 MySQL 主从延迟导致读流量打到从库、从库又回源到主库,形成了一次「隐式锁等待」。GC 日志里那 2 次 Full GC 是毛刺的结果,不是原因。
还有一次更离谱:一个 Java 服务 P99 毛刺,最终根因是同节点另一个容器的 batch job 在写 50GB 日志,I/O 带宽被吃光,前台请求排队。这种「邻居效应」在 K8s 混部场景下极其常见,但 90% 的团队第一次遇到时会完全想不到。
【常见分歧:三种排障流派】 我观察到团队排障通常分三派:
假设驱动派:先列嫌疑清单(GC → DB → 网络 → 连接池 → 线程池 → I/O),按经验概率从高到低排查。优点是快,缺点是容易「锚定效应」——第一个假设占了时间,后面的被压缩,最后变成「只查了第一个方向」。
证据驱动派:不看任何假设,先拉 trace / metrics / 日志,让数据说话。优点是客观,缺点是需要完整的可观测性基础设施,而且「看数据」本身也需要经验——同样的 GC 日志,有人看到「异常」,有人看到「正常波动」。更致命的是:如果可观测性覆盖不到根因层(比如内核 I/O、网络队列),数据再多也白搭。
全量撒网派:同时开 GC log、DB slow log、网络抓包、线程 dump、火焰图、sar、iostat……然后人工筛。优点是全面,缺点是效率低、容易漏关键线索,而且毛刺是瞬态的,等你把 6 个工具都跑起来,毛刺可能已经过去了。
【我的倾向:有方向的假设驱动 + 证据验证】 我的做法是「先缩小范围,再定向深挖」,具体分 4 步:
-
看时间分布(5 分钟):毛刺是均匀的尖刺还是集中在某个窗口?均匀尖刺 → 偶发因素(GC、网络、锁竞争);集中窗口 → 持续因素(DB 慢查询、连接池耗尽、资源瓶颈)。这一步能把嫌疑范围砍掉一半。
-
看毛刺幅度(5 分钟):P99 从 80ms 到 100ms 和从 80ms 到 350ms,嫌疑范围完全不同。小幅毛刺大概率是 GC / 锁 / 网络抖动;大幅毛刺大概率是 DB / 连接池 / 资源瓶颈 / 邻居效应。
-
定向深挖(只查最可能的 1-2 个方向):不要 6 个工具同时跑。选定方向后,用对应工具深挖。比如怀疑 DB,就看慢日志 + 连接池 pending 指标 + 主从延迟;怀疑 I/O,就看 iostat + cgroup io.max + 同节点其他容器的 I/O 曲线。
-
证伪标记:每一步排查完,不管有没有发现问题,都明确标记「已排除」或「未排除」。避免「看起来没问题」这种模糊结论。
核心原则:假设是起点,不是终点;证据是裁判,不是装饰。
【避坑清单(来自真实踩坑)】
- GC 日志里的 Full GC 可能是毛刺的结果而非原因(堆被毛刺期间的临时对象撑满)。
- 「网络正常」要看 TCP retransmit 率 + 队列长度,不能只看 ping 通。
- 同节点的 batch job 可以在 I/O 层面「拖死」前台服务,这在混部场景下极其常见但极少被排查。
- 连接池耗尽的毛刺,pending 线程数比 active 连接数更有诊断价值——active 满了是「结果」,pending 堆积是「原因」。
- 线程 dump 里的 BLOCKED 状态不一定是锁竞争,也可能是 I/O 阻塞(文件 / 网络 / 管道),要看 stack 里的具体调用。
【想听大家】
- 你们的团队排障流程是「假设驱动」还是「证据驱动」?有没有踩过「锚定效应」的坑——第一个假设占了 3 小时,真正根因在第三个方向?
- 你的「嫌疑清单」是什么?按什么顺序排?有没有一个「永远先查」的项目?
- 有没有一次毛刺,最后根因完全出乎意料?复盘时怎么解释「为什么一开始没想到」?
- 如果让你只保留一个排查工具(GC log / distributed trace / metrics / 火焰图 / 线程 dump / eBPF),你选哪个?为什么?
- 混部场景下,你有没有被「邻居效应」坑过?怎么发现的?有没有提前预防的手段?