排错套路
排错套路
# 写在前面:先谈心法,再谈套路
工作这些年,我踩过不少生产事故,也旁观过很多同事排错。慢慢总结出一个感受:排错水平的差距,一开始并不是工具用得熟不熟,而是"心法"对不对。
新手上来第一反应是登服务器、看代码、猜原因;老兵上来先问三件事——影响面多大?能不能回滚?现场留了没有?
后来我把它总结成自己排错的三条铁律:
- 止血优先:生产事故第一要务是恢复业务,不是搞清楚"为什么"。能回滚就回滚,能切流量就切流量,能扩容就扩容——根因分析是复盘阶段的事。
- 留证据:止血之前,务必留一份现场——堆快照、线程栈、日志切片、监控截图。哪怕留一个未重启的节点当"标本",也胜过事后无米之炊。
- 变更可回滚:自己写的代码、改的配置、执行的 SQL,默认要能回滚。不能回滚的变更,本身就是故障放大器。
心法定了,下面的所有"套路"才有真正的用武之地。
# 在不同环境排查问题,有不同的打法
要说排查问题的思路,我们首先得明白是在什么环境排错。不同环境的约束不同,打法也不同。
| 环境 | 特点 | 排查手段 | 关键约束 |
|---|---|---|---|
| 开发环境 | 权限最全、可重启、可调试 | 单步调试、任意工具 | 能否稳定重现 |
| 测试环境 | 有远程访问权限、可造数据 | jvisualvm、Arthas 远程附加;压测造场景 | 数据与生产的差异 |
| 生产环境 | 权限严、流量真、恢复优先 | 主要靠监控、日志、快照 | 能多快恢复 |
生产环境有三个绕不开的约束:
- 权限管控严格——一般不允许调试工具从远程附加进程。
- 恢复优先于分析——难以留出充足的时间慢慢排查。
- 环境复杂、流量真实——最容易出问题,也是出问题最多的环境。
所以生产环境的排错重心不是"临场发挥",而是事前把该埋的桩埋好——监控、日志、快照、链路是否到位,直接决定你能不能在几分钟内做出正确判断。
# 生产问题排查的四大支柱
其实,排查问题就像在破案。生产出问题时,因为要尽快恢复应用,不可能保留完整现场用于排查和测试。因此,是否有充足的信息可以了解过去、还原现场,就成了破案的关键。
现在我做需求的时候,会默认把这四样东西一并铺好:日志、指标、快照、链路。
# 一、日志(Logs)
日志就不用多说了,写代码时主要注意两点:
- 确保错误、异常信息可以被完整地记录到文件日志中;
- 用合理的日志优先级,生产日志级别开在
INFO以上:DEBUG用于开发调试;INFO用于重要流程信息;WARN用于需要关注的问题;ERROR用于阻断流程的错误。
一个小建议:日志一开始就用结构化格式(JSON)写,并且把 TraceID 打进去。等出了事再想统一日志格式,改造成本比一开始就做要大得多。
# 二、指标(Metrics)
在生产环境排查问题时,我希望我的应用有多个层次的监控:
- 主机层面:CPU、内存、磁盘、网络。部署在 K8s 上还要看 Pod 层。有一层 OS 就要做一层监控。
- 网络层面:专线带宽、交换机基本情况、网络延迟。
- 中间件和存储:不仅监控进程的 CPU、内存、IO 等基础指标,更要监控组件内部的关键指标(MySQL 慢查询、Redis 连接数、Kafka Lag)。Prometheus 的
exporter生态覆盖得挺全。 - 应用层面:JVM 的类加载、内存、GC、线程等(我用 Micrometer 做应用指标)。
- 业务层面——我踩过的坑:技术指标全绿、下单量却跌了一半。加几个业务指标(下单量、支付成功率、登录量),比 CPU 曲线值钱得多。
# 三、快照(Snapshots)
这里的"快照"指应用进程在某一时刻的状态。我给所有 Java 服务默认加这两个 JVM 参数,OOM 时自动 dump:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=...
2
之后用 MAT 分析堆快照就行。同类的工具还有:jstack 抓线程栈、async-profiler 采 CPU 热点、jcmd 抓一切。
踩过的坑:
HeapDumpPath一定要指向一个磁盘容量足够、不会被容器销毁的路径。K8s Pod 崩了之后本地文件系统跟着没了,dump 也就没了——记得挂 PVC 或者主机路径。
# 四、链路(Traces)
微服务和分布式系统时代,一次请求跨十几个服务是常事。没有链路追踪,你连"慢在哪一跳"都定位不了——只能靠猜。
- 常见方案:SkyWalking、Jaeger、Zipkin、OpenTelemetry。
- 关键要求:TraceID 全链路透传——包括异步线程、消息队列、定时任务。TraceID 断在哪,你的排错就断在哪。
日志、指标、快照、链路都到位后,再来看定位问题的套路。
# 分析定位问题的套路
定位问题,首先要定位问题出在哪个层次上——是自己应用的问题,还是外部因素?我一般先看程序有没有异常,异常信息通常比较具体,可以马上定位到大概方向;如果是资源消耗型的问题可能没异常,就靠指标监控配合显性问题点来定位。
一般情况下,程序的问题来自以下三个方面。
# 第一,程序发布后的 Bug
回滚后可以立即解决。这类问题占生产故障的很大比例——所以我在心法里强调"可回滚"是第一原则。先回滚止血,具体的版本差异分析放到复盘再做。
自己开发时也养成一个习惯:任何生产变更,脑子里过一遍"可监控、可灰度、可回滚"——三个都能答上来再上线。
# 第二,外部因素
比如主机、中间件或数据库的问题。这类问题的排查方式,按照主机层面的问题、中间件或存储(统称组件)的问题分为两类。
主机层面的问题,可以使用工具排查:
| 问题类型 | 常用排查工具 |
|---|---|
| CPU 相关问题 | top、vmstat、pidstat、ps |
| 内存相关问题 | free、top、ps、vmstat、cachestat、sar |
| IO 相关问题 | lsof、iostat、pidstat、sar、iotop、df、du |
| 网络相关问题 | ifconfig、ip、nslookup、dig、ping、tcpdump、iptables |
组件的问题,可以从以下几个方面排查:
- 排查组件所在主机是否有问题;
- 排查组件进程基本情况,观察各种监控指标;
- 查看组件的日志输出,特别是错误日志;
- 进入组件控制台,使用一些命令查看其运作情况。
# 第三,系统资源不够造成系统假死
通常需要先通过重启和扩容解决问题,之后再进行分析——但记住心法:至少留一个节点作为现场。系统资源不够,一般体现在 CPU 使用高、内存泄漏或 OOM、IO 问题、网络相关问题这四个方面。
# CPU 使用高
如果现场还在,具体的分析流程是:
- 首先,在 Linux 服务器上运行
top -Hp pid命令,来查看进程中哪个线程 CPU 使用高; - 然后,输入大写的
P将线程按照 CPU 使用率排序,并把明显占用 CPU 的线程 ID 转换为 16 进制; - 最后,在
jstack命令输出的线程栈中搜索这个线程 ID,定位出问题的线程当时的调用栈。
如果没有条件直接在服务器上运行 top 命令,可以用采样法:间隔固定秒数(比如 10 秒)运行一次 jstack 命令,采样几次后,对比得出哪些线程始终处于运行状态。
如果现场没有了,我们可以通过排除法来分析。CPU 使用高,一般由下面的因素引起:
- 突发压力:通过负载均衡的流量或日志量来确认。Nginx 等反向代理都会记录 URL,可以依靠 Access Log 进行细化定位,也可以通过监控观察 JVM 线程数的情况。压力问题导致 CPU 使用高的情况下,如果程序的各资源使用没有明显不正常,之后可以通过压测 + Profiler(
jvisualvm、async-profiler)进一步定位热点方法;如果资源使用不正常(比如产生了几千个线程),就需要考虑调参。 - GC:通过 JVM 监控 GC 相关指标、GC Log 进行确认。如果确认是 GC 的压力,那么内存使用也很可能会不正常,需要按照内存问题分析流程做进一步分析。
- 程序中死循环或不正常的处理流程:结合应用日志分析。一般情况下,应用执行过程中都会产生一些日志,可以重点关注日志量异常部分。
# 内存泄露或 OOM
最简单的分析方式,就是堆转储后使用 MAT 分析。堆转储包含了堆现场全貌和线程栈信息,一般观察支配树图、直方图就可以马上看到占用大量内存的对象。
需要注意的是,Java 进程对内存的使用不仅仅是堆区,还包括:
- 线程使用的内存(线程个数 × 每一个线程的线程栈)
- 元数据区(Metaspace)
- 堆外内存(Direct Memory / Native Memory)——最容易被忽略,Netty、gRPC 用得多
每一个内存区都可能产生 OOM,可以结合监控观察线程数、已加载类数量、堆外内存等指标分析。另外,务必检查 JVM 参数设置是否合理。
# IO 相关问题
除非是代码问题引起的资源不释放等问题,否则通常都不是由 Java 进程内部因素引发的。
# 网络相关问题
一般也是由外部因素引起的。
- 对于连通性问题,结合异常信息通常比较容易定位;
- 对于性能或瞬断问题,可以先尝试使用
ping等工具简单判断,如果不行再使用tcpdump或Wireshark来分析。
# 分析和定位问题的十二条心得
有些时候,我们分析和定位问题时,会陷入误区或是找不到方向。遇到这种情况,可以借鉴下我踩坑总结出来的十二条心得。前九条偏技术方法,后三条偏排查习惯——在真实事故中,后者往往决定成败。
# 一、考虑"鸡"和"蛋"的问题
发现业务逻辑执行很慢且线程数增多的情况时,我们需要考虑两种可能性:
- 程序逻辑有问题或外部依赖慢,使得业务逻辑执行慢,在访问量不变的情况下需要更多的线程数来应对。比如,10 TPS 的并发原先一次请求 1s 可以执行完成,10 个线程可以支撑;现在执行完成需要 10s,那就需要 100 个线程。
- 有可能是请求量增大了,使得线程数增多,应用本身的 CPU 资源不足,再加上上下文切换问题导致处理变慢了。
出现问题的时候,我们需要结合内部表现和入口流量一起看,确认这里的"慢"到底是根因还是结果。
# 二、通过分类寻找规律
在定位问题没有头绪的时候,可以尝试总结规律:
- 我们有 10 台应用服务器做负载均衡,出问题时可以通过日志分析是否是均匀分布的,还是问题都出现在 1 台机器;
- 应用日志一般会记录线程名称,出问题时我们可以分析日志是否集中在某一类线程上;
- 如果发现应用开启了大量 TCP 连接,通过
netstat我们可以分析出主要集中连接到哪个服务。
如果能总结出规律,很可能就找到了突破点。
# 三、根据调用拓扑分析,不能想当然
比如,我们看到 Nginx 返回 502 错误,一般会认为是下游服务的问题。但下游不一定就是你的 Java 程序,链路可能是 Nginx -> Traefik -> 应用,一味排查 Java 程序始终找不到根因。
又比如 Spring Cloud Feign 出现连接超时,也不一定是服务端的问题——如果客户端连的是 Nginx 代理而不是走 Eureka 服务发现,那超时其实是 Nginx 宕机所致。
我的做法:自己维护一张服务依赖图放在飞书文档里,每次架构变动就顺手更新。事故时对着图排查,比脑子里回忆快 10 倍。
# 四、考虑资源限制类问题
观察各种曲线指标,如果发现曲线慢慢上升然后稳定在一个水平线上,那么一般就是资源达到了限制或瓶颈:
- 网络带宽曲线上升到 120MB 左右不动了 → 可能打满了 1GB 网卡;
- 数据库活跃连接数上升到 10 个就不动了 → 可能是连接池打满。
平顶曲线 = 触碰瓶颈,这是一条非常灵的经验,屡试不爽。
# 五、考虑资源相互影响
CPU、内存、IO 和网络这四类资源相辅相成,一个资源的瓶颈很可能引起其他资源的连锁反应:
- 内存泄露 → Full GC → CPU 飙升;
- 网络/磁盘慢 → 内存队列堆积 → 内存暴涨。
因此不能只看单一指标下结论,要看全景。
# 六、排查网络问题要考虑三方面
到底是客户端问题、服务端问题,还是传输问题。
以数据库访问慢为例:
- 客户端:连接池不够、GC 停顿、CPU 占满;
- 传输:光纤、防火墙、路由表;
- 服务端:慢 SQL、锁等待、磁盘 IO。
服务端慢一般看 MySQL 慢日志,传输慢用 ping,都排除了且仅部分客户端慢,就是客户端本身的问题。别一上来就假设"肯定是服务端的锅"。
# 七、快照类与趋势类工具结合使用
| 工具类型 | 代表工具 | 用途 |
|---|---|---|
| 趋势类 | jstat、top、监控曲线 | 观察指标变化,定位大概问题点 |
| 快照类 | jstack、MAT、Heap Dump | 详细分析某一时刻的细节 |
先用趋势类总结规律,再用快照类深入分析。反过来容易误判——单个快照只是一个瞬间,至少要多个快照对比才能得出结论。
# 八、不要轻易怀疑监控
我曾看过一个空难事故的分析:飞行员发现仪表显示所有油箱都缺油,第一反应是"油表坏了",结果没多久引擎就断油熄火了。
同样地,应用出问题时,我们宁愿相信自己的经验,也不相信监控图表——这可能让我们完全朝错误方向排查。
如果真的怀疑监控有问题,可以看这套监控对不出问题的应用显示是否正常,如果正常那就应该相信监控而不是自己的经验。
# 九、无法定位到根因时的补救措施
如果因监控缺失等原因无法定位根因,相同问题就有再出现的风险,需要做好三项工作:
- 做好日志、监控和快照的补漏工作,下次遇到问题时可以定位根因;
- 针对问题的症状做好实时报警,确保出现问题后可以第一时间发现;
- 考虑做一套热备的方案,出现问题后可以第一时间切换到热备系统快速解决问题,同时保留老系统的现场。
# 十、先假设,再验证;不做"只验证"的排错
我自己以前排错常见的失败模式:不断"我看看"、"我试一下"——没有假设,纯粹碰运气,然后一个下午过去,还是那句"再看看"。
后来强迫自己用假设驱动的方式:
- 根据现象提出一个可证伪的假设("我怀疑是 X 引起的,因为 Y");
- 设计一个能证实或证伪的验证动作;
- 执行、观察、更新假设。
每一步都要能回答"我在验证什么"。答不上来,就是在瞎忙——这时候该停下来喝口水,重新捋一遍现象。
# 十一、别一个人硬扛
新人时期我总觉得"我自己能搞定",半小时没进展也不吭声,怕别人觉得我菜。后来才意识到:在生产事故里死磕是团队的浪费。
我现在给自己定的规矩:
- 10 分钟原则:10 分钟内没有明确方向、没能止血,就主动求助;
- 信息透明:在群里同步排查进展和当前假设,让别人有机会补位;
- 别怕暴露不懂:DBA 一眼看出来的问题,我盯着可能盯半小时。
# 十二、区分"根因"和"诱因"
一次事故往往是多个因素叠加的结果:
- 诱因:让问题显现的那一下——比如流量突增、一次发布、一条运营 SQL;
- 根因:埋在系统里的那个雷——连接池配小、代码逻辑缺陷、缺少限流、慢 SQL 没索引。
只处理诱因("限流一下就好了"、"重启一下就好了")而不追根因,同样的雷下次还会踩。每次事故要问自己一句:如果诱因换个样子,这个雷还会不会炸? 会的话,说明根因没除。
# 顺手做几件事,让下一次排错更轻松
排错这件事很有意思:你现在排错顺不顺,取决于半年前的自己有没有埋好桩。所以每次做完一个需求,我会顺手做几件事:
- 补监控:这个功能挂了会怎么被发现?告警配了没?业务指标缺不缺?
- 补日志:关键路径的入参、异常分支的上下文有没有打?TraceID 透传了没?
- 想回滚:这次改动能不能一键回滚?开关有没有留?灰度怎么灰?
- 想降级:依赖挂了怎么办?兜底数据有没有?熔断阈值合不合理?
这些事情做的时候都是"多花 10 分钟",但真出事的时候,救命的就是它们。
# 复盘:把每一次故障变成自己的经验资产
定位到问题原因后,一定要做好记录和复盘。每一次故障都是宝贵的资源——但要真正把它变成"资源",需要做扎实四件事。
# 1. 完整的时间线
从告警触发到恢复的每个动作、每个决策,精确到分钟。别只写"20:00 出问题,20:30 恢复"——中间那 30 分钟每一步做了什么、判断依据是什么,才是复盘价值最高的部分。
# 2. 根本原因分析
用 5 Whys 追到底——不是"因为 CPU 高了"就完事,而是继续问:
- 为什么 CPU 会高?→ 因为有个死循环。
- 为什么死循环没被发现?→ 因为单测没覆盖这个分支。
- 为什么单测没覆盖?→ 因为这段代码是配置分支,很少走到。
- 为什么这段代码这么脆弱?→ 因为它假设配置一定合法。
- 为什么假设配置一定合法?→ 因为没做参数校验……
追到底才知道该改在哪里。停在第一层,只能得出"下次注意"这种没用的结论。
# 3. 短、中、长期改进方案
改进方案分层写:
- 短期(当天到一周):修补当前问题、临时监控、代码回滚;
- 中期(一到两个月):加单测、补告警、增加降级预案;
- 长期(季度级):架构上的调整,比如拆服务、换存储、加隔离层。
每条必须有 Owner、有 Deadline、可跟踪。写完不落地的复盘等于没做。
# 4. 定期回顾
我会把自己参与过的故障归档,季度性回看一次:
- 有没有反复踩同一个坑?(说明改进没落地)
- 哪一类问题占比最大?(说明该重点补哪块能力)
- 有没有共性——比如都是变更引起的?还是都是外部依赖挂了?
反复踩同一个坑,是最大的浪费。
# 重点回顾
写到最后,我把自己的排错观整理成三条:
- 有依据,不猜:分析问题一定要有依据,靠猜是猜不出来的——所以监控、日志、快照、链路要提前铺好。定位时从大到小思考:先定层次(内 vs 外)、再定类别(CPU vs 内存)、再定细节。遇到瓶颈时退回来看全局,比一头扎进细节更快。
- 止血优先,别恋战:生产事故的第一目标是恢复业务,不是"我一定要现场找出根因"。留好现场,先回滚、扩容、切流量把业务救回来,根因慢慢查。
- 每次故障都要有沉淀:修完就散不叫复盘。把时间线、根因、Action Item 沉淀下来,下次再遇到同类问题,你和团队都能少踩一个坑。
排错这件事,最终拼的不是谁工具用得溜,而是谁能在混乱中保持冷静、在证据不足时不瞎猜、在解决问题后愿意回头补桩。