凌晨两点,钉钉群里一条“磁盘告警”直接把睡意干没了。点开一看,某台业务机器 / 使用率 96%,再翻监控,已经持续写了半小时日志。那一刻脑子里的第一反应不是“为什么”,而是“先杀进程还是先腾空间”。这几乎是每个 Linux 运维都经历过的深夜场景:告警不会挑工作时间来,故障也不会按你的技能树出题。
这份《Linux 故障排查实战手册》不是教科书式的命令罗列,而是把我这些年处理线上故障的套路、踩过的坑、以及“到了现场先干什么后干什么”的节奏,整理成一张能照着走的作战地图。目标是让新同学遇到告警不至于抓瞎,让有经验的兄弟也能在复盘时对照查漏。无论你用的是 CentOS、Ubuntu 还是国产化系统,这套方法论基本通用,核心就是把“慌乱”变成“流程”。
1. 故障排查的整体逻辑:先定级,再分层,别上来就敲命令
很多人一收到告警就冲上机器,top敲完没看明白又去df -h,紧接着tail -f刷日志,半小时过去,问题没定位,现场反而被自己冲乱了。排查故障和急诊分诊是一个道理:先稳住局面,再按优先级处理,最后才谈得上根治。
1.1 第一步:判断故障等级,决定你的响应姿态
先花十秒钟搞清楚三件事:
- 影响面多大?是单机问题,还是集群/多节点同时告警。同时告警优先查公共依赖(数据库、网关、存储、网络出口)。
- 业务是否已受损?用户能感知(超时、报错、卡顿)和暂时无感,响应级别完全不同。
- 是否在持续恶化?比如磁盘以每分钟 2% 的速度在涨,这比一个已经稳定在 95% 的磁盘要紧急得多,因为留给你的窗口期可能只剩半小时。
我自己的习惯是把告警分为 P1/P2/P3 三档。P1 是业务挂了或即将挂,需要立即介入甚至回滚;P2 是资源濒临临界值、性能明显劣化,可以花 5-10 分钟盘一下再动手;P3 是可疑苗头,比如某进程内存缓慢上涨,能等到白天再慢慢查。定级的作用是约束你的行为,避免在 P1 场景下还去慢慢研究根因,先止损再说。
1.2 第二步:按“硬件-系统-应用”三层快速切分
定完级,接下来就是分诊。我一直沿用“白盒三层法”:
- 硬件层:CPU 是否飙满、内存是否耗尽、磁盘是否写满、网卡是否有错包。这些相对直观,命令也就
top/free/df/ip -s link那几条。 - 系统层:进程是否异常、文件句柄是否泄漏、系统负载为何升高、内核日志有没有报错。要看
ps、/proc、dmesg,以及/var/log/messages这类系统日志。 - 应用层:业务日志、慢查询、接口调用链、GC 情况。应用相关的排查往往最耗时,因为它需要结合业务逻辑,光看系统指标不够。
三层不是孤立存在的。比如 CPU 飙高,可能是应用死循环,也可能是内核态频繁切换,还可能是被邻居机器吵到了(超线程/云上宿主干扰)。我的建议是快速把三层的证据各收集一轮,再判断重点深挖哪一层。切忌拿着锤子看什么都是钉子,只会top的人容易把所有问题都归成“Java 线程问题”。
1.3 第三步:留好现场,再动手
这条是我踩坑换来的血泪教训。早些年处理一次 CPU 飙高,我上去就把可疑进程 kill 了,结果进程死了,现场也没了,连是什么业务逻辑导致的都没办法复盘。现在我的规矩是:任何操作之前的 30 秒,先把现场“快照”留全。
# 保留故障时的关键信息,防止因为误操作丢失现场 { echo "===== time ====="; date echo "===== load/top ====="; top -bn1 | head -30 echo "===== mem ====="; free -h echo "===== disk ====="; df -hT echo "===== io ====="; iostat -x 1 3 echo "===== net ====="; ss -antp | head -100 echo "===== process ====="; ps -eo pid,ppid,%cpu,%mem,rss,cmd --sort=-%cpu | head -40 echo "===== dmesg ====="; dmesg -T | tail -100 } > /tmp/troubleshoot_$(date +%Y%m%d_%H%M%S).log 2>&1这段脚本我建议直接存到服务器上,随便找个目录放着,比如/opt/scripts/snapshot.sh,出现告警先跑一遍,再开始排查。文件不大,但能让你后续复盘时知道“当时到底发生了什么”,而不是靠记忆脑补。
2. 五类高频告警的快速定位路径
说句实在话,运维 80% 的告警都逃不出这五类:CPU 飙高、内存不足、磁盘写满、网络异常、进程丢失。下面把每一类的快速定位路径和我的判断经验写清楚。
2.1 CPU 飙高:先分清是用户态、内核态还是上下文切换
top显示 CPU 100% 时,先按键盘数字1看每个核的情况,再按P按 CPU 排序。但如果只看这一屏,你很容易误判,因为 CPU 高背后至少有四种完全不同的原因:
- 用户态高(%us):应用或脚本在拼命计算,最常见是死循环、大循环、频繁 GC。
- 内核态高(%sy):系统调用过于频繁,常见于大量小文件读写、频繁创建和销毁线程、iptables 规则过多、网络软中断密集。
- 软中断高(%si):网卡多队列不均、单队列网卡流量被打满。
- 等待 IO 高(%wa):CPU 在等磁盘,表面看 CPU 高,根子在磁盘慢。
# 查看 CPU 核心数、负载和 top 进程 lscpu | grep -E '^CPU\(s\)|^Model name|^Socket|^Core|^Thread' uptime # 按 CPU 排序看进程 top -bn1 | sort -k9 -rn | head -20 # 确认上下文切换和运行队列 vmstat 1 5如果vmstat里cs(context switch)达到几十万甚至上百万,说明系统在疯狂切换线程,这时候与其盯top,不如去数线程数。Java 应用线程数飙到几千,光切换就能把 CPU 吃满。我处理过最典型的一个案子就是线程池没配拒绝策略,请求积压后疯狂 new 线程,最终 CPU 100%。
2.2 内存不足:OOM 只是结果,内存去哪了才是问题
看到free -h显示 available 很小,别急着加内存。Linux 的 free 命令里 buff/cache 是会被回收的,真正要警惕的是可用内存(available)持续走低。更直接的信号是dmesg里出现Out of memory: Killed process。
内存排查路径我一般这样走:
# 看整体内存分布 free -h # 按内存占用排序进程(第二行按物理内存排序) ps -eo pid,ppid,rss,vsz,cmd --sort=-rss | head -20 # 观察 OOM 是否在内核日志里留下了痕迹 dmesg -T | grep -i -E 'out of memory|killed process' # 统计各进程的页表、堆、栈等细分内存(需要 root) cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapTotal|SwapFree'一个常被忽略的点是:top里的 RES 并不等于进程真正占用的物理内存,多个进程共享的共享库和共享内存会被重复计算。判断内存泄漏比较可靠的手段是持续观察几天,看 RSS 是否符合业务周期。如果业务没增长、请求量没变,RSS 却一直阶梯式上涨,那基本就是泄漏。用jstat看 Java 堆、用pmap看进程地址空间,也能进一步缩小范围。
我踩过的一个典型坑:某系统内存告警,free显示 cached 占了 10G,一堆同事说内存泄漏要重启。其实那只是内核用它来做 page cache,业务高峰过后不一定需要释放,真正到内存紧张时,内核会优先回收缓存。别动不动就重启,先把数据看明白。
2.3 磁盘写满:从“谁占的空间”和“谁在写”两头查
磁盘告警往往是最让人头大的,因为空间是慢慢没的,而且“谁占空间”和“谁在写文件”经常不是同一个答案。比如日志文件虽然被删了,但进程还持有句柄,空间一直不释放——这是经典中的经典。
# 查看磁盘和 inode 占用 df -hT df -iT # 找出超过 1G 的大目录(以 / 为例,按需修改路径) du -hx --max-depth=2 / 2>/dev/null | sort -rh | head -30 # 找出被删除但依旧被进程占用的文件(空间不释放的元凶) lsof +L1lsof +L1这条命令值得单独说。正常情况下进程打开的文件会有链接计数,删除后计数变 0。当你把日志文件rm了,但没重启应用,应用依旧往那个 fd 里写,lsof +L1就能列出那些“link count = 1”的文件。解决办法要么 kill 进程,要么> /proc/<pid>/fd/<n>清空那个句柄,后者可以不用重启服务就释放空间,是线上保命技能。
2.4 网络异常:连接数、丢包、端口,一个都不能漏
网络类告警通常表现为:连接超时、连接数过多、丢包率上升、网卡软中断打满。我喜欢按“四查法”来走:
# 查看监听端口和连接状态统计 ss -antp | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn # 查看网卡收发包、错误、丢弃统计 ip -s link show eth0 # 查看 socket 队列是否堆积(Recv-Q/Send-Q) ss -lnt连接数异常高且大量集中在TIME_WAIT,先看系统参数net.ipv4.ip_local_port_range和net.ipv4.tcp_tw_reuse的配置;如果大量连接是SYN_RECV,那要考虑是不是被扫了端口或回包有问题;如果Recv-Q长期不降,通常是应用读得慢,不是网络问题。
有一个经验值得记牢:尽量不要在排查阶段把时间耗在ping上。ping通只代表 ICMP 通,不代表 TCP 通了业务就通。线上很多故障是“ping 全通,业务全挂”。要用telnet/curl测端口,用ss看队列,用tcpdump抓包,才能把问题拉到正确的层面。
2.5 进程异常:白了、僵了、丢了,三种情况三种打法
进程常见异常有三位:S 状态的 D(不可中断睡眠)、Z(僵尸)、以及进程一会儿有一会儿没有。D 状态基本是进程在做 IO,卡在磁盘或 NFS 上,ps -o stat里看到 D 就去查存储;Z 状态说明子进程死了但父进程没调用 wait,批量出现时盯父进程,多半是父进程逻辑有 bug;进程一会儿有一会儿没有,优先看 supervisor/systemd 重启策略和业务崩溃日志。
# 找出 D 状态和 Z 状态进程 ps -eo pid,ppid,stat,cmd | grep -E ' [DZ]' # 查看 systemd 服务的重启次数和最近日志 systemctl status <service-name> systemctl show <service-name> | grep -E 'NRestarts|ExecMain'在这里插一句:遇到进程反复重启,不要只盯着业务日志,先看systemctl status开头的几行,那里有最后一次退出的时间、退出码,甚至main process exited, code=killed, status=9这种关键信息,能帮你快速判断是被 OOM kill 了,还是被 systemd 超时杀掉了。
3. 让排查事半功倍:一套顺手的高频命令组合
排查就是打仗,兵器得趁手。下面这套组合拳我用了很多年,覆盖面足够应对 90% 的 Linux 故障场景。每个命令都会给一句“什么场景下使用”的说明,方便你按需取用。
3.1 性能看板四件套:top、vmstat、iostat、sar
top -bn1:非交互模式取一次快照,适合写脚本,也可以把结果落到日志里。vmstat 1 5:观察运行队列、CPU 空闲、IO 等待、上下文切换,5 秒采样 5 次。iostat -x 1 3:看每块盘的%util、await、svctm。%util接近 100% 只说明盘忙,不一定是容量问题,可能是 IO 模式太差。sar -q -f /var/log/sa/sa$(date -d yesterday +%d):回溯历史负载,判断是偶发还是持续。很多问题你接手时已经过去了,sar是帮你坐时间机器的工具。
提示:
sar需要 sysstat 包支持,部分精简系统可能没装,提前确认好。如果没有历史数据,也别慌,/var/log/messages、应用日志里的时间戳也能帮你重建现场。
3.2 日志检索三板斧:grep、tail、journalctl
日志是排查的“监控探头”,不会用日志等于盲人摸象。
# 看某个服务的实时日志 journalctl -u <service-name> -f journalctl -u <service-name> --since "10 minutes ago" # 传统日志目录按关键字捞 grep -E 'ERROR|Exception|Timeout' /var/log/app/xxx.log | tail -200 # 统计错误出现频次,比看原始日志更能判断趋势 grep -c 'OutOfMemoryError' /var/log/app/xxx.log grep -oE 'ERROR [^ ]+' /var/log/app/xxx.log | sort | uniq -c | sort -rn | head -20看日志的核心技巧不是“看全文”,而是“先看类型和频次”。拿grep -oE提取异常的摘要信息,再用uniq -c排序,往往一眼就能看出是哪类错误最多。不要一上来就tail -f盯屏,那是工作效率黑洞。
3.3 现场快照脚本:把上面整套组合成一条命令
把常用命令合成一个脚本,放到每台服务器的/opt/scripts/,遇到告警直接跑,输出文件再慢慢看。脚本不必复杂,关键是每条命令都要能独立执行,一个挂了不影响其他部分。之前给的snapshot.sh就是一个好模板,使用前根据自己的环境删减命令即可。
另一个实用技巧:把snapshot.sh做成定时任务,每小时跑一次,保留最近 7 天。这样遇到故障时,即使你没来得及采集现场,因为定时快照已经帮你把“谱”记下来了。代价极小,收益极大,我强烈推荐。
4. 三个真实案例复盘:从告警到根因的完整闭环
光说理论不够,我把三个处理过的典型故障复盘出来,每个案例都按“告警现象、排查过程、根因定位、解决措施”的顺序写,展示实战中这套地图是怎么用的。
4.1 CPU 飙到 200% 的“罪魁祸首”居然是日志框架
现象:某业务机 CPU 告警,top看到 java 进程占了 200%(双核),但业务并发并不高。
排查过程:先用top -H -p <pid>看到两个线程 CPU 高,拿到线程号转成十六进制,jstack后找到了日志输出相关的类。此时我还没下结论,又去看了磁盘 IO,iostat显示那块盘的%util不高。最后翻应用配置,发现日志级别是 DEBUG,而且输出到一个终端文件,每条日志都会执行同步 flush。
根因:日志框架在 DEBUG 级别下每条日志都会做一次序列化+IO 操作,CPU 消耗被日志写放大。不是业务逻辑问题,是“打日志”本身把 CPU 吃掉了。
解决:日志级别调整为 INFO,关闭控制台输出,核心接口降级为异步日志。CPU 直接落回 5%。
这个案例想说明什么?CPU 高不代表应用在做业务计算,很有可能是“辅助动作”在空转。排查时千万别只看业务代码,日志框架、监控 agent、备份脚本统统可能是元凶。
4.2 磁盘 100% 又释放不了的“灵异事件”
现象:监控告警/使用率 100%,但du -sh /*一层层看下去,加起来远远小于df显示的总占用。
排查过程:我先跑了df -hT看到/dev/mapper/root满了,然后lsof +L1,果然列出好几个被删除但仍被进程持有的文件,全部是应用日志。原来应用日志通过 logrotate 按天切割,但应用进程没收到信号重新打开文件,老文件被删除后,句柄一直指向那个 inode,空间就没真正释放。
根因:不是“日志文件太多”,而是“删除了文件但进程没释放 fd”。所以如果你只盯着目录看,永远找不到是谁占了空间,因为文件已经不在目录里了。
解决:lsof +L1列出 pid 和 fd 号之后,用ls -l /proc/<pid>/fd/确认,然后用> /proc/<pid>/fd/<fd号>把那个句柄清空。这样不用重启应用,磁盘空间立即释放。然后再回头修 logrotate 的配置,加copytruncate或告诉应用重新打开日志。
4.3 内存频繁 OOM,最终揪出了隐藏的“缓存雪崩”
现象:某缓存服务内存告警,频繁 OOM,触发进程被内核杀掉,重启后过几小时又 OOM。
排查过程:dmesg里能看到完整的 killed process 记录,包括当时进程的 RSS 和 swap 使用。接着看free -h,swap 使用了大量空间,但我注意到一个反常的事:缓存服务配置的 maxmemory 明明远低于物理内存。翻应用监控发现,是在某个固定时间点后内存开始线性上涨,而这个时间点正是某个“批量预热缓存”的任务执行时间。
根因:预热任务一次性往缓存写入千万级 key,导致服务内部数据结构膨胀和频繁 rehash,直接打爆了 maxmemory 限制。OOM 是结果,罪魁是任务设计不合理,没有做分批限速。
解决:把预热任务改成分批写入,加限速,同时监控缓存 key 数量变化曲线。此后内存曲线变得平稳。
从这个案例能学到一个通用原则:OOM 不可怕,可怕的是 OOM 之前你不看历史曲线。任何内存问题都要回答“从什么时候开始涨的”这个问题,而这个答案,监控图比命令更能告诉你。
5. 排查完之后的收尾:别让告警第二天再来敲门
故障恢复不是终点,你要做的是让同类问题下次别再半夜把你叫醒。收尾阶段我固定做三件事:写小结、补监控、改代码或配置。每一步都不复杂,但漏掉一步,下个夜班就在前面等你。
5.1 补好监控和告警阈值
很多故障并不是“突然发生”的,只是你缺了发现它的眼睛。比如磁盘空间,如果你只在 90% 才告警,那留给你的处理窗口自然很窄;如果你从 60%、70%、80% 分级告警,就能提前发现“每周稳定增长 5%”的慢性问题。
监控建议不用很贵很复杂,最简单的方案是 crontab + shell 脚本 + 企业微信机器人/钉钉机器人。脚本检查指标,超过阈值调 webhook 发消息。效果立竿见影,而且不依赖特定监控平台。关键是把阈值定得不扰民但留足窗口,比如磁盘 80% 告警、90% 严重告警,CPU 持续 10 分钟 90% 才告警,避免瞬时尖峰误报。
5.2 写好一份“20 分钟内能看懂的复盘记录”
复盘记录的核心不是记录操作步骤,而是记录“决策链路”:你当时看到了什么证据、为什么判断是某一层的问题、最后怎么验证的。这比洋洋洒洒写几千字操作流水更有价值。
我一般用四段式:
- 现象:监控图/告警消息截图
- 影响:哪些机器、哪些业务、持续了多久
- 过程:时间线 + 证据(命令输出、日志)
- 改进:监控补点、代码修复、预案更新
复盘的粒度不必精雕细琢,但一定要当场写完。拖到第二天,细节就模糊了,再想写就得靠脑补,那还不如不写。
5.3 沉淀一套应急预案,把经验变成团队能力
最后一点是我觉得最值得投入的事:把高频故障的处理过程固化成应急预案。比如“磁盘告警应急预案”“CPU 飙高应急预案”,每个预案里写清楚三块:第一,第一响应人 5 分钟内该跑哪些命令;第二,哪些操作禁止做(比如 OOM 时不要盲目重启);第三,什么时候需要升级到 P1。
预案的价值在于,它能让你在凌晨三点半脑子不清醒的时候,依然按照合理的顺序操作。个人经验再多也会被情绪影响,但一条写好的流程不会。把这份文档维护在团队文档中心,每次故障后更新一次,半年之后,你会发现大部分问题都有章可循。
写到这里,最后还是分享一个我自己的习惯:每次处理完故障,我都会把snapshot.sh生成的快照文件整理进故障归档目录,文件名带上日期和故障编号。这活儿看起来不起眼,但真有同事在三个月后回头查“上次磁盘满的时候那台机器的负载曲线到底长什么样”,这套归档就成了珍贵的佐证材料。故障排查是一门经验学科,经验从哪来?就是从一次次快照、日志、复盘里攒出来的。你把这个闭环跑顺了,告警再来时,就真没啥可怕的了。