1. 凌晨三点的告警声,逼出了这套排查路径
先说一个几乎所有运维都被支配过的场景:凌晨 2 点 47 分,手机在床头柜上震得跟触电一样,钉钉/企微/短信轮番轰炸——线上某台核心服务器 CPU 负载飙到 30,接口超时率开始往上爬。你揉着眼睛爬起来,打开电脑,连上跳板机,然后呢?然后对着满屏的 bash history 和 top 输出发呆,脑子里全是"该先看什么"。
这不是个例。我带过的不少新运维,甚至有些工作了两年多的朋友,遇到告警的第一反应都是:熟练地敲一个top,看到某个进程 CPU 高,就开始怀疑代码有问题,然后一封邮件甩给开发,让开发去查。但说实话,这种操作在故障处理里只能算"起手式",离真正的排查还差着十万八千里。
更麻烦的是,现在的 Linux 服务器承载的业务链路早就不是一台机器的事:前面有 Nginx、LVS、SLB,中间有 Redis、Kafka、MySQL、ES,后面还有一堆微服务 Pod。任何一个环节出问题,都可能表现为"某台机器负载高"或者"某个接口报 502"。如果你没有一套完整的排查路径,而是逮着什么看什么,大概率会陷入"修了三天三夜,最后发现是隔壁机房光模块松了"的尴尬境地。
所以我想做的事,是把这套被无数次深夜告警逼出来的排查方法论整理成一张"作战地图"。它不是那种网上随处可见的命令速查表(虽然我会把关键命令都列出来),而是一套从告警现象反推根因、按系统资源维度逐层下钻、最后止损加复盘的完整链路。你可以把它当作一个 check list 存在笔记里,下次半夜被叫醒的时候,照着走就行。
这套方法覆盖的范围我提前说清楚:主要针对 Linux 单机层面的故障定位,包括 CPU、内存、磁盘、网络四大类高频问题,附带一些应用层和虚拟化层的交叉场景。Kubernetes 集群和数据库内部的深度调优不在这篇展开,那是另外的工程。
2. 拿到告警的第一批操作:先冻结现场,再谈定位
很多人的毛病是——告警一响,手比脑子快,上去就是一顿操作:先重启服务,不行重启机器,再不行把进程 kill 掉。且慢,这个做法在 80% 的情况下是错的。你确实把故障"恢复了",但下次它还会在同一个时间点、以同一个姿势复发,因为你永远不知道它到底为什么崩。
2.1 告警分级:什么值得半夜爬起来,什么可以继续睡
看到告警先不要慌,第一步是判断这个告警的级别和真实性。我自己习惯把告警分成三档:
- P0 级(立即处理):核心业务完全不可用、大批量 5xx、数据库连接数满、磁盘已满导致服务写入失败。这种不用想,直接进入作战状态。
- P1 级(15 分钟内介入):单台机器负载异常、某个接口 P95 延迟上升、个别实例 OOM。先花两三分钟做"望闻问切",确认影响面再决定要不要叫其他人。
- P2 级(记录观察):磁盘使用率超过 80%、证书 7 天内到期、某台非核心机器 CPU 偶发飘高。这种故障大多不致命,可以白天再处理。
这个分级特别重要。我在 Zabbix 和 FlashDuty 的告警策略里都会做这样的事:P0 走电话+短信,P1 走 IM 群 @,P2 只进通知流。否则你天天半夜被磁盘 80% 的告警吵醒,真正的 P0 来了反而麻木了。
2.2 冻结现场:第一时间留住内存转储和堆栈
如果确认是 P0 或 P1,紧接着要做的不是重启,而是尽可能多地采集现场证据。说句实在话,计算机系统的故障基本上是"有因必有果",只要现场信息够全,大多数问题都能在日志和转储文件里找到答案。
需要第一时间执行的操作(按优先级排序):
# 1. 记录系统整体状态 uptime dmesg -T | tail -200 last -x | head -20 # 2. 如果是进程异常,保留进程快照和内存信息 pidstat -p <PID> 2 10 cat /proc/<PID>/status cat /proc/<PID>/maps > /tmp/maps_<PID>_$(date +%s).txt jstack <PID> > /tmp/jstack_<PID>_$(date +%s).txt 2>&1 # Java 应用 gcore <PID> # 条件允许时,内存转储 # 3. 如果是网络问题,抓包保留证据 tcpdump -i eth0 -w /tmp/capture_$(date +%s).pcap port 8080 -c 100000 &什么情况必须抓包,什么情况不需要?我的经验是:凡是涉及"连接建立失败""请求超时"的,一定要抓;凡是 CPU 打满导致的计算缓慢,抓包价值不大,重点抓进程堆栈。一次深夜排查,我因为没有提前抓包,眼睁睁看着一个诡异连接泄漏问题在重启后消失得无影无踪,第二天只能靠监控留存的十几个指标硬猜,效率低得让人抓狂。
2.3 快速建立"故障时间线"
拿到告警后,记录下这些时间点会非常有帮助:
- 告警首次触发的时间?(看监控平台)
- 服务首次报错的时间?(看日志)
- 该机器的最后一次变更时间?(看发布系统/工单记录)
- 最近一次重启时间?
我遇到过不止一次的情况是:告警发生在凌晨 1 点,但真实根因是昨天下午 4 点的一次配置变更,只是问题有延迟效应。如果不建立时间线,你会被"告警时间=故障原因时间"这个错觉带偏,白折腾几个钟头。
3. CPU 告警排查:load average 高不代表就是 CPU 不够用
CPU 是告警里最容易被误判的一类,因为它看起来"最直接"——负载高了,那就加 CPU 呗。但如果每次都这么做,你会发现自己成了十足的"资源采购员",靠扩机器掩盖问题,成本越飙越高,问题却从来没真正解决过。
3.1 拆解 load average:不可中断任务才是隐形杀手
看下面这个指标:
$ uptime 03:21:33 up 85 days, 7:12, 2 users, load average: 20.01, 19.78, 20.15机器是 8 核,load average 到了 20,第一反应是 CPU 不够。但 load average 这个指标,统计的其实是处于可运行状态和不可中断睡眠状态的平均进程数。可运行状态确实是 CPU 排队;但不可中断状态(通常是等待磁盘 IO、等待网络 IO)也会把 load 拉高。
所以正确的第一步是区分 load 的组成:
# 用 vmstat 看进程状态分布,重点关注 r 和 b 两列 vmstat 2 10 # 输出示例 # procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- # r b swpd free buff cache si so bi bo in cs us sy id wa st # 20 1 0 812344 10256 668900 0 0 5 23 2 3 65 20 3 12 0这里r表示运行队列中的进程数,b表示不可中断睡眠的进程数。如果r远大于 CPU 核数,而b很低,那确实是 CPU 瓶颈;如果b很高,说明大量进程在等 IO,这时候你加 CPU 一点用都没有。
有一次线上 MySQL 实例告警 load 打到 30(16 核),DBA 一开始疯狂调 buffer pool 参数,怎么调都没用。后来我帮他跑了一下vmstat,发现b列常年在 10 以上,wa高达 40%。再用iostat一看,两块老机械盘组成 RAID5 的 IOPS 已经见底了,正主是某个开发写的全表大查询。你瞧,如果早一点看b列,根本不至于绕这么大弯。
3.2 定位 CPU 消耗进程后,下一步是抓线程和堆栈
确认是 CPU 瓶颈后,用top找到消耗最高的进程,然后要从进程级别追到线程级别,才能知道它到底在执行什么:
# 先找到 PID top -o %CPU -b -n 1 | head -30 # 显示该进程下所有线程的 CPU 消耗 top -H -p <PID> # 如果是 Java 应用,把线程 ID 转成十六进制,去 jstack 里找对应线程栈 printf "%x\n" <TID> jstack <PID> > /tmp/jstack.txt grep -A 30 "nid=0x<TID十六进制>" /tmp/jstack.txt这个动作几乎成了我排查 Java 服务 CPU 飙高的标准姿势。举个例子:一个网关服务每次高峰期 CPU 都会突刺,top看到的是 GC 线程特别忙,但 GC 忙只是结果,不是原因。通过 jstack 对线程栈,才定位到是某个序列化框架在每次请求时都触发了一次反射调用,而反射在 JDK8 的某些版本下会大量分配短期对象,间接打爆了新生代。如果你不到线程栈这一层,只会一直在"GC 调优"这个泥潭里打转。
3.3 区分用户态、内核态与 steal 时间
关于 CPU 告警,还有三个维度一定要关注:
用户态 CPU(us)高:通常意味着业务代码/计算逻辑真的在跑,比如死循环、大量排序、序列化。这时要配合堆栈去看代码路径。
内核态 CPU(sy)高:多半和系统调用过于频繁、锁竞争(spinlock)、网络包处理相关。一个很典型的例子是某些应用大量使用 getsockopt/setsockopt,或者在高并发下频繁开关文件句柄,导致 sys 占比高得离谱。
steal 时间(st)高:这是在虚拟化环境下特有的指标,表示你的 vCPU 在等待宿主机物理 CPU 调度的时间。如果st超过 30%,那你加再多的 vCPU 都没用,要去宿主机层面找原因——通常是宿主机超卖太严重,又或者是宿主机上有"吵闹的邻居"。我们曾经就遇到过一次,某台云主机每逢整点负载就跳一下,排查了各种业务逻辑都没结果。后来云厂商给了个数据才知道,整点是宿主机上那台服务器批量跑备份任务的时间。
偷个懒,把top里那几个 CPU 相关数值做个速查对应表:
| CPU 状态 | 可能原因 | 下一步动作 |
|---|---|---|
| us 高 | 业务计算密集/死循环 | jstack/火焰图定位热点方法 |
| sy 高 | 系统调用频繁/锁竞争 | strace 追踪系统调用,perf top 看内核热点 |
| wa 高 | 磁盘 IO 瓶颈 | iostat 确认 r/s、w/s、await |
| st 高 | 虚拟化层资源争抢 | 联系云厂商/宿主机管理员 |
| id 低且 r 高 | 纯 CPU 排队 | 扩容/优化代码路径 |
3.4 小技巧:用火焰图把 CPU 热点一网打尽
如果你手上的服务是 Java、Go 或者 Python 的,建议把火焰图这个工具体系吃透。我常用的组合是perf record + FlameGraph:
# 以 99Hz 采样 60 秒(低频率减少对业务的影响) perf record -F 99 -p <PID> -g -- sleep 60 perf script > out.perf # 然后用 FlameGraph 的脚本生成火焰图 stackcollapse-perf.pl out.perf > out.folded flamegraph.pl out.folded > cpu_heatmap.svg生成的火焰图直观到什么程度?有一次我排查一个推送服务的 CPU 高企问题,火焰图一出来,明明白白看到将近 60% 的 CPU 消耗在一个正则解析库里,而那个库是多年前引入的"顺手工具",业务上早就停止使用了。删除那部分逻辑后,服务 CPU 下降了一半多。这类问题如果用top一层层找,可能得花一两天,火焰图半小时给你答案。
4. 内存排查:free 命令只是开始,OOM 才是终点
内存告警往往以两种面貌出现:一是 free 命令显示可用内存逐步见底,二是某个进程被 OOM Killer 干掉导致服务直接挂掉。两者处理思路完全不同。
4.1 看懂内存的"假性不足":free 命令的输出没那么简单
在没搞懂 Linux 内存管理前,很多人看到下面这个输出会吓一跳:
$ free -h total used free shared buff/cache available Mem: 15G 9.1G 1.2G 120M 5.2G 5.1Gused 9.1G,free 只剩 1.2G,好像内存已经很紧张了。实际上,那 5.2G 的buff/cache是 Linux 拿来当文件缓存用的,"回收就能用"。真正要看的指标是available,它才是"在不触发 swapping 的情况下,还能分配给新进程的内存总量"。只要 available 没掉到危险线以下,就不用太担心。
正确排查路径是:
# 1. 看内存整体水位和交换情况 free -h swapon -s # 2. 看每个进程的内存占用 top -o %MEM -b -n 1 | head -30 ps aux --sort=-%mem | head -20 # 3. 看内存压力相关指标 cat /proc/pressure/memory4.2 OOM 场景的关键证据链:别让系统白白背锅
如果系统确实发生了 OOM,dmesg里通常会留下完整的"案发记录":
dmesg -T | grep -i -E "out of memory|oom|killed process"典型的输出长这样:
[Thu Apr 11 02:31:47 2025] Out of memory: Killed process 12345 (java) total-vm:12345678kB, anon-rss:3456789kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:45678kB oom_score_adj:0这里有几个信息点要抓:
- 被杀的进程 PID 和名字:确认是不是你的核心服务;
- anon-rss:占用的匿名内存量,这是评估它是否"吃太多"的关键;
- 触发 OOM 时的系统内存水位:可以从同段 dmesg 的前面几行拿到。
我踩过一个经典的坑:某台机器的 MySQL 总是半夜 OOM,DBA 说"MySQL 配置没问题,是机器内存太小"。但我把 dmesg 完整日志捞出来一看,发现 OOM 触发前一个小时内,page cache被一个定时任务疯狂填充,而这个定时任务是一次不完整的备份脚本留下的孤儿进程。正常情况下 MySQL 内存配置没有问题,但孤儿任务把可用内存全部吃光了,最后内核把"最大的进程"挑出来杀——很不幸,最大的进程就是 MySQL。这个问题的解法不是给 MySQL 调参,而是修掉那个定时任务。
4.3 找出真正的内存"黑洞":从 /proc 到 smem
当你需要精确评估哪个进程在真正消耗物理内存时,top的 RES 列会欺骗你——因为它不区分共享内存的重复计数。更靠谱的是:
# smem 按 USS/PSS 排序,能剔除共享内存的重复计算 smem -tk -s rss | head -20 # 或者手工看指定进程的详细内存 cat /proc/<PID>/status | grep -E "VmRSS|VmSize|VmSwap"另外,针对 Java 应用一定要留意堆外内存的问题。JVM 的堆内内存可以通过-Xmx控制,但堆外(元空间、线程栈、DirectByteBuffer、JNI 引用)经常超预期。一个真实案例:某服务堆设置 2G,但top显示 RES 已经到了 4.5G。用smem查完才发现,真正吃内存的元凶是 RDMA 相关的 DirectBuffer 没有释放。这类问题靠free和top很难看见,必须深入到进程的内存映射表里。
4.4 Swap 已用光不等于世界末日,但页面换入换出是危险的信号
很多人以为 swap 用了 90% 就很危险。其实对于现代 Linux 系统,只要没有持续的si/so(swap 换入换出),swap 占用高更多是"历史遗留",不代表当前压力。真正需要警惕的是:
vmstat 2 5 # 如果 si 和 so 持续不为 0,说明内存是真的紧张,系统在拼命换页,业务延迟会明显恶化遇到 swap 持续抖动的情况,如果业务允许,可以直接调低vm.swappiness:
# 临时调整 sysctl vm.swappiness=10 # 永久生效 echo "vm.swappiness=10" >> /etc/sysctl.conf但说实话,这是"缓解"而不是"根治"。swap 持续发生就意味着物理内存确实不够用了,终究得回到"加内存"或者"优化内存消耗"这两个方向上去。
5. 磁盘告警排查:从空间不足到 IO 延迟的完整套路
磁盘类是白天也能遇到的高频告警,而且很多问题是从"空间满"开始的。
5.1 磁盘空间快满的第一波操作:定位大文件与回收空间
最基础但也最容易出错的环节,是有人会直接rm -rf掉某个大文件,然而df一看,空间还是没释放。原因很简单:有进程还持有这个文件的文件句柄,文件虽然从目录里删了,但空间要等进程关闭句柄或退出才能真正释放。
正确做法是:
# 1. 查看哪个挂载点满了 df -h # 2. 找到该挂载点下最大的目录/文件 du -xh --max-depth=1 /data | sort -rh | head -20 # 3. 如果删除了大文件但空间没释放,用 lsof 找还在占用该文件的进程 lsof +L1 # 列出所有被删除但仍被进程占用的文件第 3 步是我的保留技能。有一次 Nginx 日志盘满了,操作同学把 access.log 删了,过了两个小时又满。后来用lsof +L1一看,原来是 Nginx 的日志 fd 一直没重新 open——虽然文件已被删除,但 Nginx worker 依然向旧 inode 写入。标准的修复不是删文件,而是kill -USR1 <nginx-master-pid>让它重开日志句柄。
对于大日志、大文件,可以提前准备一套"轮转+压缩+清理"策略。这里给个 logrotate 的示例配置:
cat /etc/logrotate.d/myapp /data/logs/myapp/*.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate postrotate # 触发应用重新打开日志文件的命令 /bin/kill -USR1 $(cat /var/run/myapp.pid) 2>/dev/null || true endscript }这个配置里copytruncate很关键——它先复制文件再清空原文件,应用无需重启日志描述符就能继续写,适合那些不支持 reopen 的程序。
5.2 inode 耗尽:df 显示有空间,但什么都写不进去
另一个"鬼故事"型告警是:df -h明明还有 20G,但应用报"磁盘空间不足"(No space left on device)。常见原因之一是inode 耗尽。inode 是文件系统的元数据索引,如果小文件特别多,inode 会先于数据块耗光。
df -i # 如果 IUse% 接近 100%,基本就是 inode 耗尽遇到这种情况,得找小文件大户:
# 统计每个目录下的文件数 for dir in /data/*/; do echo "$(find $dir -type f | wc -l) $dir"; done | sort -rn | head -10制造业场景里,我见过一个典型的 inode 爆满案例:某个程序在失败重试时,每次都会往 /tmp 下写一个带时间戳的错误文件,但忘记清理。文件只有几百字节,几个月积攒下来几百万个,直接把 ext4 的 inode 用光了。修起来不难:写个脚本把超过 30 天的 /tmp 文件批量删除。但要想根治,还是得改程序逻辑,错误输出要落盘但不应该无限堆积。
5.3 IO 延迟高:iostat 里的四个关键数字
如果告警是"磁盘性能急剧下降""MySQL 慢查询激增",那就需要看 IO 方面的证据。
iostat -x 2 10输出比较长,我只着重看四列:
- %util:磁盘忙绿程度,但注意不是"用了多少",而是"采样周期内设备处于忙绿状态的时间占比"。接近 100%,说明设备一直在干活。
- await:平均每次 IO 请求的处理时间(排队+处理)。SSD 一般个位数毫秒,传统机械盘 10~20ms 算正常,超过 50ms 就要警惕。
- r_await/w_await:读写各自的等待时间,可以区分是读还是写导致的高延迟。
- svctm:真正的纯处理时间(不含排队),现代指标里看
await - svctm就能估算排队有多严重,如果排队时间占比很高,说明请求量已经超过设备能力。
我常用这一招定位 MySQL 所在的云盘是否出现性能瓶颈:iostat -x 1,盯着await和%util。如果%util接近 100%、await高达 30ms,再对比同一台机器上的其他挂载点,如果只有这个数据盘有问题,基本可以判定是云盘级别的问题,这时候该提工单联系云厂商了。如果只能怪自己的业务——比如全表扫描之类的,那就去调 SQL 和索引。
5.4 提前预案:磁盘告警的自动化处理思路
与其等深夜被磁盘告警吵醒,不如提前做一个轻量级的"自愈+通知"脚本。简单思路:
- 每隔 10 分钟检查各挂载点使用率;
- 超过 85% 时自动跑
du找大目录并清理 tmp 类文件; - 超过 90% 才真正告警到人,并且把"大文件 Top 10"一并带出来。
这样一来,P2 级的磁盘告警会在半睡半醒的层面被自动消化掉,真正到人手里的,大概率已经是需要人肉介入的 P1 了。这也是近几年 flashduty 这类平台想做而没能完全自动化解决的事——告警分级容易,告警自愈难,但至少我们可以先做一个脚本层面的半自动闭环。
6. 网络告警排查:连通性、丢包、重传与 TCP 队列
网络问题在我的深夜告警清单里占到的比例逐年上升。微服务和容器化之后,排查链路的复杂度高了不少。这里讲几个最常用的下钻路径。
6.1 第一层:连通性与路由
无论什么网络告警,先做三件小事,确认网络链路是否还通:
# 1. 基础连通性 ping -c 5 <目标IP> # 2. 路由走向(确认没有走到错误的网关/过短的路径) traceroute -n -T -p 443 <目标IP> # 3. 端口连通性 nc -vz -w 3 <目标IP> 8080我建议不要用普通的 ICMP traceroute,因为现在不少网络节点会丢弃 ICMP,导致 traceroute 显示不完整。用 TCP 模式的 traceroute(-T)更接近真实业务链路的走向。
6.2 第二层:网卡层面的丢包与错误
如果 ping 通,但业务仍然有超时,下一个可能的方向是网卡层丢包。
ethtool -S eth0 | grep -E "rx_|tx_" | grep -v ": 0"重点关注几个计数器:
rx_dropped:网卡收了包,但因为 ring buffer 满、内存不足等原因丢弃;rx_missed:硬件 FIFO 溢出导致的丢包,说明网卡中断处理不过来;tx_dropped/tx_errors:发送方向异常。
遇到网卡丢包,可以把队列数调大,或者把多队列 RSS 打开,让中断分散到多个 CPU 核上:
# 查看当前队列数 ethtool -l eth0 # 修改队列数(需要厂商驱动支持) ethtool -L eth0 combined 16另外别忘了看一眼软中断(softirq)的 CPU 使用。如果所有软中断都压在一个核上,建议开启 RPS(Receive Packet Steering):
echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus在拆分布式存储集群的时候,我遇到过一次诡异现象:同机房内两个节点之间互传文件,带宽利用率始终上不去,稳定在 300Mbps(万兆网卡)。iperf3测试结果也差不多。最后ethtool -S一看,rx_missed疯狂增长。把队列数从 8 抬到 16、并打开 RPS 之后,带宽直接冲到 4Gbps。这种问题不打开网卡统计,靠肉眼和top是永远找不出来的。
6.3 第三层:TCP 层状态与重传
如果端口和网卡都没问题,就要下沉到 TCP 层:
# 1. 当前连接状态统计 ss -s # 理想情况下,TIME_WAIT 和 ESTABLISHED 各司其职,不会堆积到诡异程度 # 2. 重传情况 netstat -s | grep -E "retransmit|retransmitted" # 如果重传率显著上升,多半是链路质量/对端处理能力问题 # 3. 查看 TCP 队列溢出 ss -lnt # Recv-Q/Send-Q 如果持续非 0,说明接收/发送队列满了TCP 队列满了是特别容易被忽视的告警点。所谓"半连接队列"(syn queue)满,会表现为 SYN 被丢弃,客户端发起的 TCP 连接一直建立不起来。所谓"全连接队列"(accept queue)满,则表现为客户端能连上,但服务端 accept 不出来,请求堆积。
排查命令:
# 看全连接队列是否溢出(listen 状态且 Send-Q 达到上限) ss -lnt | grep LISTEN # 看当前是否有 SYN 半连接溢出 netstat -s | grep "SYNs to LISTEN"有一次排查一个"Nginx 偶尔出现 502"的问题,抓包发现客户端三次握手已经完成了,但 Nginx 就是没有返回数据。最后ss -lnt一看,Nginx 的 listen 队列吧满了——因为后端有大量请求阻塞在 worker 的线程池里,worker 来不及 accept。问题根子在后端应用处理不过来,但表现为网络层排队。如果不是看到队列满这个信号,我们不知道还要在后端日志里找多久。
6.4 第四层:抓包定位具体交互
常规排查解决不了的时候,才轮到 tcpdump。但记住,不要在收到告警后才第一次学怎么用 tcpdump,平时就要熟悉。
# 抓特定端口的数据包并存文件,后面用 Wireshark 分析 tcpdump -i eth0 -nn -s 0 -w /tmp/problem_$(date +%s).pcap host 10.0.0.5 and port 8080抓包之后重点看三个现象:
SYN重传次数异常 → 网络链路或对端队列问题;RST频繁被发出 → 大概率是负载均衡器/防火墙策略或连接池超时设置;- 窗口大小不断缩到 0 → 接收端处理不过来,接收缓冲区满了。
如果在虚拟化/云环境里,还有一个值得注意的坑:某些安全组/防火墙会静默丢弃 RST 或部分分段包,导致客户端一直等待超时,而不是快速失败。这种"假死"网络问题,日志上看起来像是"远程服务器在慢慢吞吞吐数据",实际上在抓包里就是一片空白。
7. 从告警到根因的完整链路:复现一次真正的故障作战
光讲原理和命令,还是差点意思。这里补一个我独立处理过的完整案例,你可以看到上面几张地图是怎么组合起来用的。
7.1 背景与告警表现
某天晚上 22 点左右,我负责的一个在线交易服务的消息消费者集群开始告警:某台 8C16G 结点 CPU 使用率持续 95% 以上,队列消费延迟从 30 秒逐步爬升到 15 分钟。同时,业务方反馈对账任务开始出现超时失败。
按照前面说的流程,我第一时间做了三件事:
- 快速分级:核心链路延迟增长 + CPU 高企,属于 P1,先不上线完整作战状态,但必须持续盯;
- 冻结现场:记录
uptime、vmstat、top快照,并jstack保留线程现场; - 排查时间线:查看当天是否有发布记录——发现下午 17:30 有一次小版本发布,正好与告警时间窗口吻合。
7.2 排查链路:从 load 到线程栈到 GC 日志
top显示,CPU 消耗大户是这个消费进程,us占 70%,sy占 15%。vmstat的r列稳定在 8 以上,b接近 0,确认这是纯 CPU 热点问题。
接着top -H -p <PID>,发现有两个 GC 线程的 CPU 占用极高。jstat -gcutil <PID> 1000 10一看,Full GC 频率从平时的每 10 分钟一次降为每 10 秒一次,老年代增速异常。
然后按jstack去匹配 GC 线程的栈顶,发现触发 GC 的原因是分配速率太快——大量对象在堆里被创建,堆很快被打满。再用jmap -histo:live <PID>看对象分布,排在前面的类里出现了一个业务对象,名为OrderSnapshot。
到这里,问题已经缩窄到"某个方法在高频创建 OrderSnapshot 对象"。代码 review 后发现:新发布版本里有一个"记录订单快照用于运营分析"的逻辑,但它没有做批量聚合,而是在每一次状态机转换时都生成一个包含全量字段的快照对象。消息量一大,这个对象创建直接导致 GC 风暴。
7.3 止损方案与后续优化
止损措施很简单:临时把该功能的采样率从 100% 降到 10%,让 GC 压力降下来,队列恢复消费。第二天代码改成批量写入并使用固定对象池复用。
这个案例里最有价值的不是最后的定位,而是中间那条"load 高 → 区分是 CPU 还是 IO → 看线程 → 看堆 → 看对象 → 看代码"的下钻路径。每一步都在缩小范围,每一步都有证据支撑。被动挨打式的排查和这种带套路的进攻式排查,效率差距可以到 10 倍以上。
8. 故障复盘与长期沉淀:一个人的作战地图如何变成团队的
处理完一次故障,真正的工程价值在你写复盘报告的那一刻才开始。我发现很多团队的复盘都停留在"时间线+责任分配"层面,缺了最重要的两个环节:盲区记录和预案演练。
8.1 复盘阶段的固定问题清单
每次做完故障处理,我至少会问自己和团队几个问题:
- 这次故障暴露了哪块监控盲区?比如:网卡丢包、TCP 队列溢出、inode 耗尽,这些指标是否在监控平台里都覆盖了?
- 我们现有的告警级别设置合理吗?有没有把 P0 和 P2 混在一起?
- 处理过程中,哪个环节最慢?(通常是日志搜集和信息同步)
- 有没有可能在告警触发前就自动发现并自愈?
这套问题看起来很简单,但坚持做下来,你会发现自己团队的 MTTR(平均修复时间)明显下降。最直观的作用是:下次同样的问题再次出现时,你不需要从头开始摸索,直接按复盘文档里的步骤走。
8.2 整理成一份"可检索的作战手册"
我自己的习惯是,每处理完一例夜间告警,就会把"告警现象 + 排查命令 + 最终根因 + 修复动作"四元组编成一条记录,放进团队知识库。几个月下来,这本"作战手册"会变得非常有价值,因为里面全是真实命中过的场景。
比如这样一条记录:
问题现象:zabbix 上报 vsphere 证书状态告警,某虚拟机的 vmtools 失联排查命令:esxcli storage core device list / vim-cmd vmsvc/getallvms / ssh 到 ESXi 用 hostd 日志根本原因:vCenter 证书即将过期,导致 vSphere 管理通道中断修复动作:更新 vCenter 根证书并重启 management agents下次注意:提前 30 天检查证书有效期,并在监控平台设置证书到期预警
这类文档远比从网上复制粘贴的"Linux 命令大全"更贴合你的真实环境。毕竟,搜索引擎能告诉你disk full怎么查,但它并不知道你公司那台跑着老版本内核的机器有哪些独特的坑。
8.3 在夜深人静时,给自己留一条"低焦虑"的退路
最后说一点可能不太"技术",但非常重要的个人经验:不要把深夜告警当成敌人,要把它当成系统在这个时间点给你的提示信号。
我见过太多同事,一听到告警就精神紧绷、操作变形,明明是一个简单的磁盘空间问题,因为慌张,在排查过程中误删了数据目录。现在我的习惯是:半夜接到 P1 告警,先花一分钟喝口水、打开落地灯、把手机放远一点,然后才打开电脑。看似耽误了一分钟,实际上能避免很多因慌乱导致的二次事故。
如果你能把这套作战地图变成肌肉记忆——拿到告警先分级、再冻结现场、按 CPU/内存/磁盘/网络四个维度逐层下钻——你大概率能在多数情况下做到"睡得比系统早":即便被叫醒,也能在 20 分钟内给出结论,几分钟止损,然后在第二天白天从容地写复盘文档。
这套方法不复杂,难的是在每一次告警里坚持按流程走,而不是绕回"凭感觉猜、重启解决问题"的老路。坚持两个月,你也会形成自己的排查直觉。