我刚接手一套跑了六七年的业务系统时,经常干的一件事就是:半夜被人从被窝里叫起来,说线上服务挂了。一开始我也慌,登录服务器东看一眼西看一眼,CPU高就杀进程,内存不够就重启应用,日志报错就上网搜。折腾到天亮,问题可能暂时没了,但我并不知道它为什么发生,也不知道它会不会再来。后来踩的坑多了,才慢慢摸清楚一套相对固定的故障排查思路。这篇就聊聊我在Linux系统排查这件事上积累下来的经验,思路为主,命令为辅,希望能给你一条能直接上手的路径。
1. 排查故障前的两个核心认知:分层思维与先看现象再看原因
1.1 故障为什么难排查:因为你面对的永远是症状,不是原因
很多新手甚至一些干了两三年的运维,排查故障时最大的问题是"头痛医头"。CPU跑满了就查CPU,内存不够就清缓存,磁盘满了就删日志。这些操作不是不对,而是顺序错了,方向也偏了。
我自己的体会是:任何故障现象都只是系统某个环节异常的外在表现。比如用户反馈"网站打不开",这背后可能是DNS解析问题、Nginx进程挂了、后端Tomcat线程耗尽、数据库连接池被占满、服务器负载过高导致响应超时,甚至只是某个机房交换机抖动。如果你直接从"网站打不开"去修网站,等于闭着眼睛修车——你连发动机在哪都不知道。
所以我给自己定了一条原则:先不急着动手修,先搞清楚"坏在哪一层"。
Linux系统的运行从外到内大致可以分为四层:硬件层(CPU、内存、磁盘、网卡)、内核层(进程调度、内存管理、文件系统、网络协议栈)、应用层(数据库、Web服务、中间件、业务进程)、业务层(用户请求、业务流程)。故障排查的过程,就是先定位问题出在哪一层,再逐层深入。这个思路听起来像废话,但真正实施起来,能帮你省掉大量瞎折腾的时间。
1.2 故障排查的分层模型:从硬件到内核到应用的逐层剥离
我习惯把排查过程比作剥洋葱。最外层是用户可感知的异常,往里是业务进程的异常,再往里是系统资源的异常,最内核可能是硬件或者内核参数问题。
具体拆开看:
- 第一层:用户感知层。用户说慢、打不开、报错。这一层只负责提供方向,比如"响应慢"大概率是资源瓶颈,"连接拒绝"大概率是进程挂了或者端口监听丢了,"报502/504"大概率是网关后面的服务出了问题。
- 第二层:业务进程层。去看业务的日志、进程状态、端口监听情况。这一步能确定是应用自己挂了,还是应用活着但处理不了请求。
- 第三层:系统资源层。用top、free、df、iostat这类命令看CPU、内存、磁盘、IO的情况。这一层能定位是不是资源耗尽导致应用无法正常工作。
- 第四层:内核与硬件层。看系统日志、内核日志、硬件报错信息。很多诡异的问题最后都藏在这一层,比如磁盘坏道导致的IO卡顿、内存ECC纠错风暴、网卡丢包。
实际排查时,我一般从上往下走,但也不是机械地一层层看,而是先用几个关键命令快速扫描,凭经验缩小范围,再针对性地深入。有了这个分层模型,后面的排查思路就有了骨架。
2. 第一反应该做什么:从登录到现场信息采集的规范动作
2.1 登录后十分钟内的黄金排查窗口期
故障发生后的前十分钟非常关键。为什么?因为很多故障的现场证据是"易逝"的——进程还在但已经异常了、负载还在但很快会恢复、日志还在滚动但马上会被刷掉。如果你一上来就重启服务或者重启机器,等于把案发现场给破坏了。
我要求的规范动作是这样的:登录服务器后,先不要碰任何服务,按照下面顺序采集信息。
第一步,看当前时间、系统运行时长和负载情况:
date uptimeuptime会告诉你系统运行了多久、当前负载是多少。load average有三个值,分别是1分钟、5分钟、15分钟的平均负载。如果1分钟远大于15分钟,说明负载是刚刚突然涨上来的,属于急性故障;如果三个值都很高且接近,说明系统已经持续高负载很久了,属于慢性问题。
第二步,看进程概览,找到最耗资源的进程:
top -c按大写P按CPU排序,按大写M按内存排序,第一时间看清楚到底是哪个进程在吃资源。加-c参数是为了显示完整的命令行,很多进程只看进程名认不出来,看到完整命令才能知道它是干什么的。
第三步,看内存和Swap的使用情况:
free -h重点不是看内存用了多少,而是看Swap用了多少、cache/buffer有多少、available还有多少。很多系统内存显示用了90%以上,但available还很充足,这种情况多半是文件缓存占着内存,并没有真正的内存压力。
2.2 需要第一时间保存下来的现场信息清单
除了上面几个实时命令,有几类信息是需要在动手修复前存下来的,免得后面复盘时"死无对证"。
我会把这些信息重定向到文件里:
# 进程快照 ps -eo pid,ppid,user,%cpu,%mem,rss,vsz,stat,etime,cmd > /tmp/ps_snapshot.txt # 网络连接状态 ss -anpt > /tmp/ss_snapshot.txt # 系统日志尾部 journalctl -xe --no-pager -n 200 > /tmp/journal_tail.txt # 文件系统使用情况 df -hT > /tmp/df_snapshot.txt # 内核日志 dmesg -T > /tmp/dmesg_snapshot.txt为什么要存这些?因为很多故障在恢复之后,你再想去查当时的状态就查不到了。比如内存突然飙升导致OOM,等你看的时候进程可能已经没了;网络连接数爆发,等你看的时候连接可能已经断完了。先把现场拍照保存,后面不管是自己复盘还是写故障报告,都有依据。
我踩过的坑是:故障时没保存现场,等系统恢复了想查问题,结果日志被滚动覆盖,进程信息也查不到,只能凭记忆写复盘,写出来自己都不信。所以现在只要是处理线上故障,第一步永远是保存现场,这个习惯救过我很多次。
3. CPU类故障排查:从load average到上下文切换的完整链路
3.1 load average高不等于CPU利用率高:先分清三种情况
很多人看到load average高就判定CPU有问题,这是个常见的误解。load average指的是处于可运行状态和不可中断睡眠状态的进程平均数量,它不仅仅反映CPU忙不忙,还包含等待IO的进程数。
我遇到过三种典型情况:
- load高但CPU利用率也高:说明CPU真的在满负荷运转。这时候要看是用户态(us)高还是内核态(sy)高,判断是业务代码在计算还是系统调用太频繁。
- load高但CPU利用率低:这是最迷惑人的情况。CPU几乎没干活,但load很高,说明大量进程阻塞在IO上(磁盘IO或者网络IO),进程在等数据,虽然没占用CPU,但被算进了load里。
- load高且CPU的wa(IO等待)很高:基本可以断定是磁盘IO瓶颈。进程都在等磁盘读写完成,这时候加CPU也没用,瓶颈在磁盘。
区分这三类情况是排查的第一步。用top看CPU那一行的us、sy、wa、hi、si、st这几个值,基本就能判断方向。如果us高,问题在应用层;如果sy高,问题可能在系统调用频繁、锁竞争、内核参数不合理;如果wa高,问题在存储层。
3.2 CPU使用率高的定位手段:找到真正的"元凶"线程
确认是CPU问题后,接下来要回答一个问题:是哪个进程的哪段逻辑在消耗CPU?
常规操作是top按CPU排序,找到PID,但有时候你看到的进程名并不能直接告诉你它内部在干嘛。比如Java进程,进程名叫java,但真正消耗CPU的是某个线程里执行的某段代码。
这时候需要用线程级定位。以Java应用为例:
# 找到CPU占用最高的Java进程PID top -c # 然后在该进程内找到CPU占用最高的线程TID top -H -p PID # 把线程TID转成十六进制 printf "%x\n" TID # dump出Java线程栈,搜索对应nid jstack PID > /tmp/jstack.txt在jstack输出里搜索转换成十六进制后的线程ID,就能看到这个线程正在执行的代码位置,是GC线程、业务线程还是锁等待线程一目了然。这个方法我用于定位过好几次线上CPU飙满的问题,最后都证实是应用代码里某段循环逻辑出了问题,而不是系统层面有问题。
C/C++程序的思路类似,用perf top看热点函数,或者用gdb attach到线程上采集调用栈。总之,CPU排查不能停在"哪个进程",必须往下追到"哪段代码"才算完。
3.3 上下文切换过高:被忽视的CPU性能杀手
有时候CPU利用率不算高,但系统响应就是慢。这时候需要关注上下文切换(context switch)的指标。
用vmstat可以快速看到系统整体的上下文切换次数:
vmstat 1 5看cs这一列,单位是千次/秒。如果上下文切换高到几万甚至几十万,CPU时间大量浪费在线程切换上,真正干活的时间就变少了。
上下文切换高的常见原因有两类:一类是线程数过多,大量线程在竞争CPU时间片;另一类是锁竞争激烈,线程频繁阻塞和唤醒。前者可以从进程的线程数看出来(ps -Lp PID或者top -H看线程数),后者通常要从代码层面排查锁粒度是否过大,或是否过度使用了线程池。
还有个容易被忽略的点:如果上下文切换高且系统软中断(si)占用高,很可能是网络包量太大导致的收包中断频繁。这时候要看网卡的中断处理方式和RPS/RSS配置是否合理。
4. 内存与磁盘IO故障排查:free、iostat与inode问题的实战解读
4.1 内存排查的核心不是"用得多不多",而是"够不够用"
内存问题的排查,我的判断标准很简单:available(可用内存)还有多少,而不是used(已用内存)有多少。
Linux内存管理有个特点:它会尽量把空闲内存用来做文件缓存(cache),提升IO性能。所以你在free里看到的内存使用率达到90%以上,其实很正常,只要available没被耗尽,就不算内存不足。
需要警惕的是两种情况。一种是available持续走低,甚至降到几十MB甚至0,这时候系统开始频繁使用Swap,性能会急剧下降。另一种是Swap占用持续增长,说明物理内存已经不够了,内存页在内存和磁盘之间来回倒腾。
排查内存问题,我的动作顺序是:
# 看整体内存瓶颈 free -h # 看进程级别内存消耗排行 top -o %MEM # 看每个进程的详细内存映射 ps -eo pid,user,rss,vsz,cmd --sort=-rss | head # 看Swap使用情况,判断是否发生了内存交换 cat /proc/meminfo | grep -E "SwapTotal|SwapFree|Dirty|Writeback"这里特别提醒一下:top命令里的RES(常驻内存)是进程实际占用的物理内存,VIRT(虚拟内存)包含了映射的共享库和未实际分配的内存,两者差距可能非常大。看进程内存占用,以RES为准。
4.2 磁盘IO瓶颈的确认方法与性能基准判断
磁盘IO问题最典型的特征是系统卡顿、load高、CPU的wa高。你可以用iostat确认:
iostat -x 1 5重点看这几个参数:
- %util:设备的繁忙度,接近100%说明设备已经饱和。
- await:IO请求的平均等待时间,包括排队时间和处理时间。机械盘正常情况下个位数或十几毫秒,如果几十毫秒甚至上百毫秒,说明IO路径上有问题。
- svctm或r_await/w_await:读写请求的实际服务时间。如果r_await和w_await都高,且svctm不高,说明是排队时间过长,设备或队列已饱和;如果svctm本身就很高,说明设备自身处理变慢,可能存在硬件故障。
另外要说一下r/s和w/s(每秒读写次数)与rkB/s、wkB/s(每秒读写的数据量)。这两个指标配合能判断IO模式:如果r/s很高但rkB/s不高,说明是大量小块随机读,典型的数据库场景;如果rkB/s很高,说明是顺序大块读。随机小IO和顺序大IO对存储系统的压力完全不同,遇到问题时要根据业务类型去判断是否合理。
4.3 文件系统层面的隐藏坑:inode耗尽与文件句柄泄漏
磁盘空间没满,但系统就是报"no space left on device",这种问题十个人里有八个会先怀疑磁盘满了,结果df一看还有几个G。我第一次遇到时也卡了很久,最后才想到查inode。
# 检查inode使用率 df -iinode是文件系统里管理文件元数据的结构,每个文件或目录都要占用一个inode。如果小文件数量爆炸(比如某个服务的日志文件没有切割,而是不断创建带时间戳的小文件;或者临时文件目录堆积了大量小文件),inode会先于磁盘空间耗尽。
另一个容易踩的坑是文件句柄耗尽。文件句柄是内核用来管理打开文件的引用计数,每个进程能打开的文件数量有限制,系统全局也有限制。
# 查看系统级限制 cat /proc/sys/fs/file-nr # 查看某个进程打开的文件数 ls /proc/PID/fd | wc -l # 查看进程的文件句柄限制 cat /proc/PID/limits | grep "open files"服务运行几天后响应变慢甚至拒绝连接,查了大半天也没发现CPU和内存有问题,最后发现是某个进程的文件句柄数达到了上限,连接数一多就直接拒绝接受新请求了。这类问题在默认ulimit配置偏小的环境里特别容易触发。
5. 网络层故障排查:从端口监听到连接队列溢出的定位套路
5.1 先从本机确认:进程活着不等于端口在监听
用户反馈"服务不可用",你登录服务器一看,进程还活着,很多人就懵了。但实际上,进程活着和端口正常提供服务是两回事。
排查顺序:
# 确认进程存在 ps -ef | grep 服务名 # 确认端口监听状态 ss -lntup | grep 服务端口 # 确认端口上的连接状态 ss -ant | grep 服务端口监听状态正常后,再看连接状态。如果是Web服务,正常的连接状态大部分应该是ESTABLISHED或TIME_WAIT。如果SYN_RECV大量堆积,说明连接请求进来了但三次握手没完成,可能是半连接队列满了。如果大量连接处于CLOSE_WAIT,说明对端关闭了连接,但本端应用没有调用close关闭socket——这通常是应用代码里的连接泄漏。
刚才提到的这几类情况,单靠重启服务往往能暂时缓解,但根因不解决,过几天还会再犯。连接泄漏的问题要回代码里找资源释放逻辑,半连接队列满要看Nginx的backlog配置和内核的somaxconn参数。
5.2 网卡层面排查:丢包、错误计数与软中断分布
如果端口和连接都正常,但服务就是响应慢,就要往更底层看——网卡是否在丢包。
# 查看网卡统计信息 ethtool -S eth0 | grep -E "err|drop|discard" # 或者看/proc cat /proc/net/dev重点看rx_dropped、rx_errors、tx_errors这些计数。如果rx_dropped持续增长,说明网卡接收到了数据包但因为队列满等原因被丢弃了,这会导致TCP层出现重传,表现为响应变慢、连接不稳定。
另外还要关注软中断分布:cat /proc/softirqs,看NET_RX的计数是不是集中在某一个CPU上。如果所有收包中断都挤在一个CPU核心上,那个核心就会满载,而其他核心闲着,这就是RSS(Receive Side Scaling)没配置好的典型症状。一般可以通过设置网卡多队列或者启用RPS(Receive Packet Steering)来把软中断分散到多个CPU上。
5.3 连接数异常:Time_wait堆积与并发连接过高的处理思路
高并发场景下,短连接多、Time_wait多是非常常见的情况。一堆人在网上说要改内核参数调小Time_wait回收时间,但我在实际生产里发现,很多情况下直接改内核参数并不是最优解。
举个例子。对于一个内部API服务,客户端频繁建立短连接,服务端的Time_wait连接堆积是正常现象。Time_wait本身是TCP协议为了保证最后一个ACK能被对方收到而设计的,适当保留是有意义的。如果强行开启tcp_tw_reuse和tcp_tw_recycle,虽然能加速回收,但tcp_tw_recycle在某些NAT环境下会导致客户端连接异常,我见过不止一次因为开启这个参数导致线上用户上网异常的事故。
我的建议是:
- 先搞清楚连到这个端口的是长连接还是短连接,客户端是否支持连接复用。
- 如果是短连接场景,优先改客户端使用连接池,从源头减少新建连接的数量。
- 确认无必要后,再考虑调整服务端的内核参数,并且要在测试环境充分验证。
6. 日志分析与排错利器:从systemd journal到内核日志的线索挖掘
6.1 日志排查的优先级:先看内核日志和系统日志,再追应用日志
很多人排错第一步就去翻应用的日志文件,但我的习惯刚好相反:先看内核日志和系统日志,再看应用日志。
原因很简单:很多底层问题(磁盘故障、内存错误、文件系统异常、NIC故障)都会先在内核日志里留下痕迹,这些问题是应用日志里看不到的。而且,如果系统层面已经出了问题,应用日志里的报错可能只是表象,比如磁盘只读导致应用无法写日志、内存分配失败导致线程崩溃。
内核日志查看方式:
# 查看内核日志(包含硬件错误、OOM、文件系统报错等) dmesg -T # 查看系统日志(systemd管理的服务状态和日志) journalctl -xedmesg输出里我重点找几个关键词:Out of memory,看是否发生过OOM;I/O error、blk_update_request这类,看是否有磁盘错误;soft lockup、hard lockup,看是否有内核线程卡死;segfault,看是否有进程发生过段错误崩溃。
6.2 系统日志的关键关键词:OOM、I/O error、segfault、timed out的判读
OOM这个可以单独讲一下。当系统内存不够时,内核会启动OOM Killer,选择进程杀掉来释放内存。杀谁由/proc/PID/oom_score决定,分数越高越容易被杀。有时候OOM Killer会杀掉数据库、杀掉关键业务进程,对线上影响非常大。
OOM日志长这样:
Out of memory: Killed process 12345 (java) total-vm:1048576kB, anon-rss:102400kB, file-rss:0kB看到这个,你需要判断的是:为什么内存会不够?是业务流量增长导致内存需求上涨,还是应用代码存在内存泄漏,又或是JVM堆配置不合理?
另一个常见的是blocked for more than 120 seconds,这通常出现在磁盘IO卡死或者内核死锁时。看到这类日志,基本可以判断存储子系统出了问题,排查方向直接转向磁盘状态和存储网络。
6.3 高效日志检索的实操建议:journalctl与grep的组合技巧
线上日志量非常大,不讲究方法的话,光是翻日志就能翻到怀疑人生。我常用的几个技巧:
# 按时间范围过滤 journalctl --since "2024-12-01 10:00:00" --until "2024-12-01 11:00:00" # 按服务名过滤 journalctl -u nginx.service --since "10 minutes ago" # 只看某个PID相关的日志 journalctl _PID=12345 # 大量应用日志检索,用grep配合上下文 grep -n "ERROR" application.log | tail -100 # 查某个关键字前后各5行的内容 grep -n -B 5 -A 5 "OutOfMemoryError" application.log还有一个用得很多的组合:先看错误日志的时间点,再去对照同一时间点的系统资源监控数据,往往能快速定位问题。比如日志里大量报数据库连接超时,再看同一时间段内存和磁盘IO的监控曲线,发现磁盘IO跑满了,那问题就清楚了——数据库连不上的根因是磁盘拖慢了数据库的响应。
7. 综合实战:一次完整的故障定位复盘(虚拟案例)
7.1 故障现象与初步排查:从用户报障到发现load持续走高
这套方法不是我纸上谈兵,我根据这些思路整理了一个虚拟案例的完整复盘,你能看到整个推理链条是怎么串起来的。
假设场景:某业务系统突然被用户投诉"页面打开很慢",运维收到告警:负载持续走高。管理员登录服务器后按步骤操作。
uptime看到load average分别是18、15、10。1分钟和15分钟都在高位,说明不是瞬时抖动,而是持续恶化。top看到wa那一列达到45%,us只有15%——这就说明CPU不忙,大量进程在等IO。综合分析判断:瓶颈在磁盘IO,而不是CPU。
7.2 逐层深入验证:依赖iostat和dmesg逐步锁定问题根因
定位到磁盘IO后,用iostat -x 1观察具体设备。输出显示某个数据盘的%util稳定在98%,await高达80ms,正常应该在5-10ms。基本确认是这块磁盘的IO已经饱和或异常变慢。
dmesg -T | tail -50里果然发现频繁出现:
blk_update_request: I/O error, dev sdb, sector 12345678 op 0x0:(READ) err = -5err=-5对应的是I/O错误,这基本就实锤了:磁盘报告了IO错误。这时候判断不是性能瓶颈导致的磁盘慢,而是磁盘硬件开始出现物理故障,系统在持续读取坏扇区导致整个磁盘IO被拖慢。
进一步确认可以用smartctl查看磁盘的SMART信息,看Reallocated_Sector_Ct、Current_Pending_Sector这些计数是否异常增长。按照流程提交磁盘更换。
7.3 复盘:如果跳过某些步骤会走到哪些歧路
这个案例里,最典型的弯路是:如果第一个反应是按"CPU高"去处理,把CPU密集型的服务做限流或者重启,不仅不会有任何效果,反而会误判方向,浪费宝贵的故障处理时间。
如果只看到了load高和wa高,但没有看dmesg里的I/O error,可能会去做磁盘容量规划、调整IO调度算法,甚至加大缓存去扛IO压力,但磁盘的物理坏道摆在那里,无论软件怎么调,读写那些坏扇区时的错误行为还是一样会拖垮IO。硬件问题的最终解法只能是换硬件,软件层面的调优只能暂时缓解症状。
8. 排障工具箱与经验总结:快速诊断命令组合和日常准备
8.1 我常用的诊断命令组合(按场景速查)
按照故障类型整理了一份我常用的命令组合,遇到问题可以直接对号入座:
| 场景 | 命令组合 | 关键关注点 |
|---|---|---|
| 系统整体状态 | uptime、top、free -h、vmstat 1 5 | load趋势、CPU的us/sy/wa、available内存、cs上下文切换 |
| CPU异常 | top -H、perf top、jstack | 具体线程ID、热点函数、代码调用栈 |
| 内存不足 | free -h、ps -eo rss排序、dmesg | grep -i oom | 进程RSS排行、OOM记录、Swap增长 |
| 磁盘IO | iostat -x 1、dmesg -T、smartctl -a | %util、await、I/O error、SMART计数 |
| 文件系统 | df -hT、df -i、lsof +L1 | inode使用率、只读挂载、已删除但未释放的文件 |
| 网络连接 | ss -ant、ss -lntup、ethtool -S | 连接状态分布、端口监听、丢包计数 |
| 日志分析 | dmesg -T、journalctl -xe、grep分析应用日志 | 内核报错关键词、错误时间点、上下文关联 |
这套组合不是死的,实际用起来可以根据场景增减。核心逻辑是:任何问题,都要有一个从现象到系统到应用到硬件的最短路径。
8.2 日常监控体系的建设:防患于未然的几个关键指标
排查再熟练,也不如提前发现问题。日常监控我建议至少覆盖以下指标,配合告警阈值:
- CPU:使用率、load、软中断分布、上下文切换。
- 内存:available、Swap使用量、OOM事件。
- 磁盘:空间使用率、inode使用率、IO的%util和await、SMART异常。
- 网络:端口连通性、连接数、丢包计数、TCP重传率。
- 应用:进程存活、接口响应时间、错误日志关键字。
告警阈值不要一味求低,阈值设太低会导致告警轰炸,时间长了大家反而不重视。我的做法是分三级:Warning(开始异常)、Critical(需要处理)、Emergency(立即行动),每级对应不同的处理时效。
8.3 几条保命经验:故障现场保留、变更记录的重要性
最后分享几条经验,都是我拿实际教训换来的:
第一,绝对不要在故障发生时直接重启服务,除非你已经保存了足够的现场信息,或者你已经明确判断重启不会丢失关键证据。重启是最后的兜底手段,不是第一排查手段。
第二,每次变更操作前记录变更内容。很多故障其实不是突然发生的,而是某次变更引发的连锁反应。如果连"昨天晚上改了什么配置"都说不清,排查会困难很多。养成操作前先确认、操作后记录的习惯,能让故障定位工作量减少一半。
第三,建立自己的排障文档。每处理完一次故障,把现象、定位过程、根因、修复方案、复盘结论整理成文档。下次再遇到相似问题时,直接翻自己的笔记,比上网搜索高效得多。我自己积累的排障笔记,很多问题的定位时间被压缩到了十分钟之内。
故障排查这件事,说到底拼的是两样:一是思路清不清楚,二是经验够不够多。思路可以按这篇的方法去建立,经验只能靠一次次实战积累。先把这个排查框架装进脑子里,下次再遇到系统异常时,你就不会慌了。