接到“系统慢”的工单,是每个做Linux运维和服务端开发的人都躲不开的场景。用户不会给你更多线索,顶多补充一句“网站转圈”“接口超时”或者“数据库卡住了”。新手最本能的反应,是冲到服务器上先敲一下top,看到某个进程CPU占用很高,就下结论说“就是它的问题”。但真实世界的性能故障,往往不是一颗CPU尖刺那么简单——磁盘I/O排队、内存换页、锁竞争、句柄耗尽、网络重传,全都能够以“慢”这个字作为表象。我早期踩过最大的坑,就是在“慢”面前太早进入了“优化模式”,而没有先做“侦察模式”。
这篇文章想分享的,是我做Linux性能排查时沉淀下来的一套实战打法:把一个模糊的“慢”翻译成可以量化的指标,从机器级指标追到进程级证据,最后落到根因修复和复盘。没有太花哨的命令,基本都是在线上环境验证过的思路和工具,适合Linux运维、服务端开发,以及所有被“系统慢”三个字折磨过的人。
1. 先把“系统慢”翻译成具体的技术指标
1.1 用户的“慢”常见对应哪几类瓶颈
不管用户的描述是“页面转圈”“接口超时”还是“报表出不来”,从操作系统的角度看,“慢”终究会落到几类核心资源上:
- CPU:计算量过大、线程频繁切换、锁竞争导致活锁;
- 内存:物理内存不足触发swap,进程被换入换出,延迟飙升;
- 磁盘I/O:设备队列排队、吞吐量饱和,应用卡在读写等待上;
- 网络:带宽占满、连接堆积、丢包重传,整个请求路径被拖长。
这几类还会交叉传染。比如CPU被打满后,网络应用无法及时应答,用户感知到的是“超时”;swap剧烈时,磁盘I/O同步变高;磁盘故障导致文件同步卡顿,上层依赖文件的服务也跟着慢。所以第一步从来不是猜,而是先跑一组命令,把可能的方向排除到只剩一两个。
1.2 没有基线判断,最容易误判
每台服务器都有自己的“正常形态”。有的机器常年负载在5以上,因为它本身就是高并发接入层;有的机器常年负载不到0.5,突然跳到3就已经很反常。如果不了解机器的历史表现,单看绝对数字很容易被误导。
我自己的习惯是,日常就把同型号机器的关键指标存一份基线:CPU核数、内存总量、平均负载、iowait占比、磁盘util。这不是什么高深监控,哪怕每天手工记录一次都行。知道这台机器“正常的样子”是什么,遇到异常时才能判断异常到底跳了多高、持续了多久。
1.3 把命令按“侦察顺序”排好,别东一下西一下
很多新手的排查过程是随机敲命令,top看两眼、free看两眼、再去看一眼日志,最后什么结论也没有。我的建议是按下面的顺序走,每条命令都只有一个目的,看完再决定下一步:
- 先
uptime,看load average趋势,判断问题是否正在发生; - 再
vmstat 1 5,看CPU、内存、磁盘链路的整体状态; - 若CPU有问题,用
top和pidstat定位进程; - 若内存异常,用
free、smem、pidstat -r继续; - 若I/O有瓶颈,用
iostat -x和pidstat -d找进程; - 若怀疑网络,用
ss和sar -n检查连接与重传。
这条链路的核心思路是:先看机器,再看进程,最后看代码或配置。顺序错乱,效率会低很多。
2. 第一轮侦察:uptime与load average到底在告诉你什么
2.1 load average不是CPU使用率
$ uptime 14:22:01 up 12 days, 6:04, 2 users, load average: 6.34, 5.12, 4.80很多人在这一步就把load average当成CPU使用率。其实它表示的是“运行队列长度”,也就是过去一段时间内,处于可运行状态和不可中断睡眠状态的平均进程数。注意,不可中断睡眠状态通常是进程在等待磁盘I/O或其他资源,所以load高不等于CPU忙,也可能是大量进程卡在I/O上。
比如一台8核机器,load是8.0,说明平均每个核有一个任务在排队,已经处于饱和边缘;换成16核机器,load同样是8.0,其实还很宽裕。所以在读load之前,先确认核数,用nproc一下就知道。
2.2 1分钟、5分钟、15分钟怎么组合判断
uptime给出的三个数,分别是最近1分钟、5分钟、15分钟的平均负载。这三个数之间的大小关系比绝对值更重要:
- 1分钟明显大于5分钟和15分钟,说明负载正在快速升高,事情正在发生;
- 三个数都在高位并且基本持平,说明这个状态已经持续了一段时间,不是突发;
- 1分钟已经回落,15分钟仍然很高,说明刚过去一个长耗时高峰,或者有一个长时间任务在收尾。
这一眼就能判断问题是“正在发生”还是“已经过去”。如果已经过去,就不必急着抓现场,可以先看日志和监控曲线;如果正在发生,下一步立刻上vmstat。
2.3 uptime能打住的情况和必须继续的情况
有一种情况,load很低,但用户还是说慢。这说明瓶颈不在CPU运行队列,而可能在内存交换、网络、应用锁或者远端依赖上。此时继续纠结负载没有意义,应该转向free、ss、应用日志。
另有一种情况,load从0.5突然飙到10,并且1分钟线还在往上走,那基本可以确定系统某个资源出问题了。这时候不要再盯着load,直接进入系统级扫描。
3. 系统级扫描:vmstat的一分钟,把机器状态摊在桌上
3.1 vmstat输出的关键列怎么读
$ vmstat 1 5 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 1845600 12304 5671230 0 0 12 34 102 45 8 2 88 2 0 3 1 0 1832340 12304 5671340 0 0 567 10230 1350 678 12 6 61 21 0vmstat后面带两个参数表示每隔1秒输出一次,连续输出5次。第一次输出的往往是开机以来的平均值,参考意义不大,关键是看后面的采样值。核心列如下:
| 区块 | 列名 | 含义 |
|---|---|---|
| procs | r | 可运行状态进程数,等CPU的队列长度 |
| procs | b | 不可中断睡眠进程数,通常代表等I/O阻塞 |
| memory | swpd | 已使用的swap空间,单位KB |
| memory | free | 空闲物理内存 |
| memory | buff/cache | 缓冲区与页缓存占用 |
| swap | si/so | 每秒换入/换出内存的大小 |
| io | bi/bo | 每秒块设备读/写的大小 |
| system | in/cs | 中断次数、上下文切换次数 |
| cpu | us/sy/id/wa/st | 用户态、系统态、空闲、I/O等待、被偷取时间占比 |
3.2 r列和b列:CPU排队还是I/O阻塞
r列高而b列很低,说明CPU已经成了瓶颈,大量任务在排队等处理器;b列长期不为0,同时wa也很高,说明进程正在等待I/O,磁盘基本脱不了干系。
比较有意思的是r和b同时高的场景:CPU在忙于处理I/O路径上的中断和内核线程,而进程又阻塞在设备等待上,这时候看起来CPU也很忙,磁盘也很忙,像是“两头堵”。遇到这种情况,不要急着优化业务代码,先把I/O链路的真实压力降下来,CPU占用往往也会跟着回落。
3.3 si/so和wa/st是两组容易被忽略的报警信号
si和so持续非零,说明物理内存已经不够用,系统在反复换页。这个状态比free数值低要严重得多,因为换页是进程级别的“慢性死亡”,响应延迟会变得非常不稳定。
wa高说明I/O在拖CPU的后腿;st高则是虚拟化环境特有的问题,表示宿主机在抢占虚拟机的CPU时间片,st超过20%时,应用卡顿很可能不在自己机器内部,而要去和虚拟机平台方确认资源争抢。
4. 进程级定位:从top到pidstat再到线程级
4.1 用top之前,先把筛选逻辑想清楚
很多人top进去只看默认的进程列表,然后挑一个CPU最高的进程开始分析。正确的做法是先想清楚当前需要验证什么假设。
如果你怀疑CPU瓶颈,在top界面按一下数字1,看每个CPU核是否都打满;按大写P按CPU排序,按大写M按内存排序。如果怀疑线程层面的问题,按H切换线程视图,再按P排序,会发现某个进程内部的单个线程CPU占用异常高。
batch模式也很有用,适合把历史快照留下来:
top -b -n 1 -o %CPU | head -30这条命令一次输出当前CPU最高的前30个进程,适合写到日志文件里做事后对比。
4.2 pidstat的三类报表:CPU、内存、磁盘
pidstat比top更适合做持续采样,尤其是在问题一闪而过的场景下:
pidstat 1 5 pidstat -r 1 5 pidstat -d 1 5第一条看每个进程的CPU使用率,-r看内存占用,-d看进程的磁盘读写。实用的一点是,pidstat会输出%wait列,表示进程等待CPU的时间占比。如果%wait很高,说明进程不是没活干,而是抢不到CPU;如果I/O那个维度的读写很高,进程身份立刻就明确了。
我线上排障时经常用pidstat -d找出“看起来CPU不高但系统很慢”的元凶,因为很多进程的CPU占用不高,却在疯狂写磁盘,最终把整个设备的I/O队列拖垮。
4.3 线程级排查怎么往下钻
拿到进程PID之后,如果还想再往下钻,看线程级的资源消耗:
pidstat -t -p PID 1 5如果发现某个线程的CPU几乎打满,下一步可以把它和代码关联起来。Java服务里可以用jstack导出线程栈,找那个线程的tid;C++服务可以用perf top或gdb去看热点。工具细节跟语言相关,但思路一致:先定位线程号,再对应到代码栈,避免在进程级别反复猜测。
5. 内存专项:free命令的两大常见误区和真实泄漏定位
5.1 free -h的每个数字代表什么
$ free -h total used free shared buff/cache available Mem: 15Gi 4.1Gi 1.2Gi 300Mi 9.8Gi 10Gi Swap: 2.0Gi 0B 2.0Gi这里最容易翻车的点是:used不等于真实独占用,free低也不代表内存不够。Linux会把空闲物理内存尽量用作页缓存和文件缓存,所以free那一行很低很正常,真正需要关注的是available这一列。available的意思是“在不触发交换的前提下,还能分配给新进程的内存大小”,它考虑了可回收缓存。上面这个例子中free只有1.2Gi,但available有10Gi,内存其实是健康的。
真正的危险信号不是free低,而是swap持续增长、si/so非零,以及available持续逼近0。
5.2 从/proc/meminfo补足free看不到的细节
free是一个汇总视图,排查内存问题时我会再确认几个字段:
grep -E 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SReclaimable|Shmem' /proc/meminfo重点看Cached和SReclaimable。Cached里既包含磁盘文件缓存,也可能包含tmpfs/shmem占用,后者无法回收;SReclaimable是内核可回收的slab内存。如果Cached巨大又长期不降,可能是文件缓存正常,也可能是某个路径在反复读写同一批文件;如果SReclaimable异常高,通常和内核对象泄漏有关,比如dentry/inode缓存失控。
5.3 一个不写代码也能操作的内存泄漏定位流程
内存泄漏不一定要看堆栈。最朴素的判断方式是连续采样RSS:
pidstat -r 1 60 > mem_sample.log跑一段时间后观察某个进程的RSS是否单调上涨、且系统available持续下降。正常缓存会随着回收波动,泄漏则是只涨不降。进一步确认可以用:
cat /proc/PID/status | grep -E 'VmRSS|VmSize|VmSwap'VmRSS是物理常驻内存,VmSwap是被换出的部分。如果应用容器或服务本身不缓存数据,RSS还一直涨,那基本就是泄漏,接下来再交给开发去查具体对象引用。这个流程最大的好处是:不需要改代码、不引入额外工具,只要把采样间隔拉长,规律自然会显现。
6. 磁盘I/O专项:iostat与pidstat -d找到真正吃磁盘的进程
6.1 iostat -x的关键指标哪些可信哪些要谨慎
$ iostat -x 1 3 Device r/s w/s rkB/s wkB/s avgqu-sz await r_await w_await %util vda 0.00 890.00 0.00 3560.00 12.47 14.00 0.00 14.00 98.20重点关注几项:%util表示设备带宽利用率,avgqu-sz是设备请求队列长度,await是I/O请求从进入到完成的总等待时间,w_await是写请求平均等待。
但有个认知要纠正:%util接近100%只能说明设备“忙”,不代表一定“慢”。如果设备本身是NVMe固态盘,%util高但await只有1ms不到,那业务基本无感;如果机械盘或网络存储%util只有60%,await却超过50ms,那才是真正的慢设备。所以**%util和await需要组合起来看**。
6.2 区分“磁盘忙”和“磁盘慢”
简单给个判断经验:
- %util很高、await也很高、avgqu-sz很大:设备过载或者掉速,排队严重;
- %util很高、await很低:设备在满负荷工作但响应快,可能是持续高吞吐业务;
- %util不高、await却很高:设备本身响应慢,或者走了网络存储、被其他租户影响;
- avgqu-sz持续大于设备队列深度:请求堆积,需要扩容或优化读写模式。
很多数据库场景的“慢查询”并不是SQL有问题,而是磁盘await高导致每次刷脏页都卡顿。此时光看数据库慢日志没用,要把iostat和数据库刷盘线程的等待时间放在同一时间轴对比。
6.3 从await高到定位进程的完整链条
如果iostat确认磁盘有问题,下一步不要直接看业务代码,先找进程:
pidstat -d 1 5看到某个进程的w/s和wkB/s非常高,再结合文件路径确认它在写什么。比如一个日志采集进程在高峰期把日志级别从WARN调成了INFO,写入量可能瞬间翻几十倍;一个备份脚本在业务高峰时段启动,也可能直接把磁盘打满。找到进程、看到文件路径之后,修复手段往往简单得让人意外:调整日志级别、避开高峰时段、限制并发数,或者把大文件放到更快的盘上。
7. 网络维度:ss一眼识别连接堆积,而不是所有慢锅都甩给服务器
7.1 ss -s与几个关键TCP状态
系统资源都正常,但服务还是慢,就要看网络连接层。
$ ss -s Total: 285 (kernel 0) TCP: 51 (estab 16, closed 20, orphaned 3, synrecv 0, timewait 2)ss -s给出当前连接状态的总览。常见几个状态需要区分:
- ESTABLISHED:正常建立的连接;
- SYN_RECV:半连接堆积,可能有人发起大量握手;
- TIME_WAIT:主动断开方等待关闭的连接,过多会占表项;
- CLOSE_WAIT:被动断开方没有关闭socket的连接,应用层不处理时越堆越多。
很多文章把TIME_WAIT说得非常危险,但实际上TIME_WAIT在大部分场景下是正常状态,只要数量不冲破端口范围,影响有限。真正要警惕的是CLOSE_WAIT只增不减,这通常意味着应用代码忘了关闭连接。
7.2 CLOSE_WAIT与Recv-Q堆积的真实含义
如果确认CLOSE_WAIT数量很大,可以进一步:
ss -tanp | grep CLOSE_WAIT关键在于看关联的进程。CLOSE_WAIT堆积不是内核问题,而是应用层没能正确关闭socket。常见原因包括:读超时没设置、线程阻塞、异常路径没有释放连接。修好代码之后,CLOSE_WAIT数量才会回落。
还有一个常见的网络层陷阱值得单独说:Recv-Q堆积。执行ss -lnt,如果某个监听端口的Recv-Q长期大于backlog,说明应用accept的速度跟不上连接到达速度,也就是系统层没问题、应用处理不过来,而不是网络问题。
7.3 判断该不该继续在OS层排查
有一种“慢”,不是服务器性能造成的,而是外网链路、DNS解析、客户端网络、甚至用户所在地网络的问题。系统资源正常、连接状态正常、丢包率也不高,但用户依然反馈慢,这时候就要把排查边界划清,把问题推给网络链路方或者做更细粒度的请求追踪,而不是继续在服务器上盲敲命令。
如果在服务器上看网络质量,可以用sar -n DEV,ETCP 1 5观察重传率,以及ss -ti看TCP重传计数。确有重传再深入,没有的话果断换方向。
8. 复盘:一个“系统慢”工单从接收命令到根因落地的全过程
8.1 工单背景与第一手指标快照
某内部业务系统在晚高峰收到用户反馈,报表页面加载极慢,白屏时间接近15秒。服务器的配置是12核、32G内存,虚拟化环境。我登录后的排查记录如下:
先看uptime:
$ uptime 20:31:01 up 30 days, 2:10, 3 users, load average: 18.90, 16.52, 12.311分钟负载18.9,5分钟16.5,15分钟12.3,明显在持续升高。但这还不能判断是CPU还是磁盘,继续vmstat:
$ vmstat 1 5 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 7 0 121100 28000 882100 0 0 1200 45000 3000 1800 20 12 10 58 0r只有2,b有7,wa高达58%,si/so都是0。到这里基本可以判定:不是CPU不够,也不是内存压力,而是磁盘I/O在拖后腿。
8.2 把根因从磁盘队列追到日志配置
继续用iostat定位设备:
$ iostat -x 1 3 Device r/s w/s rkB/s wkB/s avgqu-sz await w_await %util vda 2.00 32.00 16.00 128.00 0.10 2.00 2.00 3.00 vdb 0.00 3420.00 0.00 80120.00 28.60 34.00 34.00 99.90vdb的写请求每秒3420次,吞吐量约80MB/s,avgqu-sz 28.6,await 34ms,%util 99.9。很明显,问题全在这个数据盘上。
再使用pidstat -d找到写盘进程:
$ pidstat -d 1 5 PID command w/s wkB/s 2714 log_agent 3180.00 76000.00日志采集进程占据了绝对大头。接着查看该进程的日志路径和配置,发现业务方把某个系统日志级别从WARN调成了INFO,高并发写日志导致的每分钟日志文件量是原来的几十倍,日志Agent又按固定间隔批量刷盘,最终把磁盘队列打穿。
8.3 修复动作与后续监控
处理分三步走:
- 临时把日志级别调回WARN,页面响应立刻回到2秒以内;
- 调整日志Agent的批量写盘策略,加大批次间隔,降低写盘频率;
- 将日志数据目录从繁忙的数据盘vdb迁到另一块吞吐更高的设备,并在这台机器上配置磁盘util超过85%持续5分钟即告警的规则。
后续观察一周,负载回落到3左右,vmstat的wa降到5%以下,用户反馈恢复正常。这次复盘给我最深的体会是:表面上看着像应用慢、像数据库慢的问题,只要一步步从系统指标往下追,大都能在一个小时内定位到具体进程和配置项。其实排查慢,多数时候不是命令不够,而是跳级太早、假设太多。
如果非要说一个最值得养成的习惯,我推荐:每一次线上排障,都随手把命令输出和判断结论记下来。哪怕只是几行文本,下次碰到类似症状,翻出来对照,能省下大半天时间。