周一早上九点,运维同事在群里丢了一句“订单服务那台机器好慢,SSH上去敲个ls都要等三秒”,紧接着又补一句“load average都上8了”。做Linux性能排查的人应该都懂这种时刻——你有一堆命令可以跑,但真正难的不是“会敲命令”,而是面对一个笼统的“系统慢”,知道该从哪条线切入、先看什么、后看什么、怎么把现象一步步收敛成根因。这篇内容我就围绕Linux性能排查的完整链路展开,从“系统慢”这个模糊诉求出发,讲清楚我平时是怎么用uptime、vmstat、iostat、pidstat、perf这些工具把问题精准定位出来的。适合刚接触排查的运维、后端开发,以及那些会敲命令但对分析思路还没成体系的同学。
1. 慢,不是指标,是现象:先搞清楚“哪慢了”
很多人排错的第一步就错了。报障人说“系统慢”,你直接跑top,看到某个进程CPU高就下结论,这十有八九会误判。因为“慢”是一个主观现象,不是指标。它背后的原因可能是CPU计算密集、内存换页、磁盘IO排队、网络延迟,甚至只是登录时的DNS解析卡住。所以第一步不是敲命令,而是把“慢”这个模糊描述拆细。
1.1 五类典型“慢”的表象与对应方向
我一般会把“系统慢”拆成五类,每一类对应的排查方向完全不同:
| 表象 | 典型特征 | 优先排查方向 |
|---|---|---|
| SSH登录慢 | 输完密码要等几秒才进shell | 网络连通性、sshd、PAM模块、DNS反解 |
| 命令执行慢 | 登进去了但敲啥都卡 | CPU、内存、磁盘IO、系统负载 |
| 应用服务响应慢 | 接口RT飙升,但服务器本机操作还凑合 | 应用自身、数据库、中间件、网络链路 |
| 网络访问慢 | 页面打不开、包时延高 | 带宽、TCP重传、连接队列、丢包 |
| 整机近似假死 | 啥都干不了,SSH也快断了 | 负载爆炸、内存耗尽、IO打满、内核异常 |
如果你遇到的是第二、第五类,那这篇文章后面的路线基本全覆盖。如果是第一类登录慢,先别急着查CPU,大概率是DNS反解或者PAM调用了远程认证导致阻塞。如果是第四类,重点看网络,别在服务器内存上浪费时间。
1.2 排查前必须做的三件准备工作
在动手跑命令之前,我会先确认三件事,这三件事能帮你省掉至少半小时的瞎查。
第一,确认“从什么时候开始慢的”。你问一句“之前做过变更吗”,往往比任何命令都值钱。是刚发完版变的?是加了个定时任务之后变的?是磁盘空间写满之后变的?概率直接就收敛了。
第二,确认影响范围。是只有这一台机器慢,还是集群里所有机器都慢?是只有这个服务慢,还是这台机器上所有服务都慢?如果是所有机器都慢,问题很可能在上游网络或者公共组件,单机排查意义不大;如果是单服务慢,先别怀疑OS,回到应用日志去查。
第三,保留现场。在还没做任何干预之前,先把一轮基础信息快照存下来:uptime、top -bn1、vmstat 1 5、free -h、dmesg -T | tail -50、ss -s。为什么要先存?因为一旦你重启了什么服务或者kill了进程,现场就没了。很多时候真正的根因线索就藏在当时的dmesg或TOP输出里,等你想起来去看的时候,系统已经恢复正常了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 第一轮体检:三条命令拉出系统全景图
准备工作做完,开始第一轮体检。我的习惯是先用三条命令把系统的整体轮廓摸出来:uptime看负载、top看CPU和内存、vmstat看排队和换页。它们不解决根因,但能告诉你大方向在哪。
2.1 uptime + top:一屏看完负载、CPU和内存
uptime是最快的一条命令,它会告诉你三个负载值:
code复制# uptime
09:38:21 up 82 days, 17:44, 2 users, load average: 8.32, 5.18, 3.65
load average后面三个数字分别是1分钟、5分钟、15分钟的平均负载。这个数字的单位不是百分比,而是“处于可运行状态和不可中断睡眠状态的进程/线程数”。怎么粗读?如果1分钟数值明显高于15分钟,说明负载正在快速上升,问题正在发生;如果1分钟低于15分钟,说明系统正在从高负载中恢复。
然后是top,重点看它的前五行汇总信息:
code复制top - 09:38:46 up 82 days, 17:44, 2 users, load average: 8.32, 5.18, 3.65
Tasks: 347 total, 1 running, 346 sleeping, 0 stopped, 0 zombie
%Cpu(s): 18.3 us, 6.7 sy, 0.0 ni, 12.5 id, 61.9 wa, 0.3 hi, 0.3 si, 0.0 st
MiB Mem : 15893.5 total, 2123.4 free, 7856.7 used, 5913.4 buff/cache
MiB Swap: 16383.9 total, 15961.7 free, 422.2 used. 6371.6 avail Mem
这里最重要的不是那堆进程列表,而是%Cpu(s)这一行。us是用户态CPU,sy是内核态CPU,wa是等待IO完成的时间占比,id是空闲,st是虚拟机被宿主机偷走的时间。我见到不少新手只看us高不高,忽略wa和st,结果在虚拟机上排查半天业务代码,最后发现是邻居抢占资源。
如果wa很高,说明CPU在等待磁盘IO,真正的瓶颈在下面;如果sy持续偏高,说明系统调用频繁或者内核态程序异常;如果st高,先考虑宿主机是否超卖、是否被其他虚拟机干扰。
2.2 vmstat 2 5:时间维度上的排队与阻塞
top给的是某一时刻的快照,vmstat能给一个时间序列上的趋势。我习惯跑vmstat 2 5,每两秒采样一次,连续采集五次:
code复制# vmstat 2 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
4 41 0 21036 124551 7864512 0 0 0 0 0 0 18 6 12 62 0
6 45 0 20125 124789 7894520 0 0 152343 201221 4321 9987 15 8 9 68 0
5 39 0 19876 124892 7901287 0 0 143212 188923 4102 9883 14 7 11 67 0
4 42 0 19502 124567 7899341 0 0 148902 197534 4233 10037 16 8 10 66 0
5 44 0 19215 124889 7904512 0 0 150112 195817 4288 9934 15 8 9 67 0
vmstat第一行是开机以来的平均值,可以略过,重点看后续采样行。其中两个字段要重点关注。r是运行队列中可运行的进程数,如果长期大于CPU核数,说明CPU饱和;b是处于不可中断睡眠状态的进程数,这个字段一旦持续偏高,基本都是IO阻塞在等磁盘或者等锁。上面的示例里b有40多个,说明有大量进程卡在IO上,再配合CPU行里的wa很高,方向就很明确了——下一步直奔磁盘IO就行。
另外si和so是换入换出到swap的速率,如果这两个数字持续不为零,说明物理内存不够用,内核正在把内存页换到磁盘上,这种场景下系统会明显变卡,后面我会专门讲内存部分。
2.3 把Load Average拆开看:运行队列与不可中断进程
很多人在这一步会犯一个经典错误:看到load average是8,但top里CPU使用率才25%,就以为系统在"假负载"。其实不是假,是没读懂负载的定义。Linux的负载统计的是两种进程:一是TASK_RUNNING(可运行状态),也就是正在排队等CPU的;二是TASK_UNINTERRUPTIBLE(不可中断睡眠状态),常见缩写是D,主要发生在等待磁盘IO、等待NFS响应这类场景。
所以load average高,并不等于CPU忙。它可能是CPU排队排出来的,也可能是IO等待等出来的,还可能是两者叠加。怎么区分?你分别看一眼vmstat里的r(运行队列)和b(不可中断睡眠数),负载高但b也高、r不高,那瓶颈一定在IO侧,查CPU只会白费力气。这个区分是整个排查链路的第一个岔路口,岔错了方向,后面全白搭。
3. CPU瓶颈的定位:从使用率到热点函数,一层层剥开
如果你通过第一轮体检确认是CPU方向的问题——比如r长期大于核数、us或者sy占比很高、wa并不明显——那就进入CPU的专项定位流程。我的定位路线是:top看整体归属,pidstat看线程归属,perf看函数归属。从进程一路剥到代码行。
3.1 top里的三组数据怎么配合读
进入top后,按大写P让进程按CPU使用率排序。此时不要把目光只锁定排名第一的进程,还要回看顶部的%Cpu(s):如果us高,热点在业务进程的用户态代码;如果sy高,热点在内核态,比如系统调用、内核线程、中断处理;如果us和sy都不高但r堵着,说明有大量进程在锁上等待。
需要额外留意ni和st两个容易被忽略的字段。ni高的机器通常有人用nice调整过进程优先级,如果某个进程始终占了绝大多数CPU,看看它是不是被人为调成了高优先级。st高的虚拟机会呈现出一种奇怪的形态——load高、id不低、进程响应慢,但us和sy都不到20%,这种时候不是你的系统出问题,是宿主机上其他虚拟机在抢CPU,单靠top几乎查不出东西。
3.2 pidstat -t:把线程级消耗抓出来
top的进程列表只到进程级,但一个Java应用或NGINX worker下面可能挂了成百上千个线程,CPU热点可能只在某几个线程里。这时用pidstat -t -p <PID> 1,按线程维度采样:
code复制# pidstat -t -p 2256 1
09:42:11 UID TGID TID %usr %system %CPU CPU Command
09:42:12 1000 2256 - 12.34 1.23 13.57 2 java
09:42:12 1000 - 2268 11.28 0.99 12.27 2 |__java
09:42:12 1000 - 2291 0.89 0.12 1.01 1 |__java
如果看到某一个线程的CPU消耗明显高于其他线程,热点就有了方向。不要急着kill进程,先去查这个线程在干什么——jstack看Java线程栈、gdb attach看C程序、cat /proc/<PID>/task/<TID>/stack看内核栈,都是可行的手段。
这里有个很容易犯的错:直接kill -9那个高CPU进程。你会惊讶地发现,负载是下来了,但服务也挂了,业务投诉从“慢”变成了“不可用”。排查CPU问题时,先定位到线程再决定动作,这是基本修养。
3.3 perf top:热点函数的“CT机”
如果说top和pidstat能告诉你“谁在烧CPU”,那perf能告诉你“它烧在了哪个函数上”。perf top是采样工具,它周期性打断CPU,记录当前执行位置,然后聚合出热点函数排行:
code复制# perf top -p 2256
Samples: 10K of event 'cpu-clock', 4000 Hz
Overhead Command Shared Object Symbol
24.12% java libjvm.so [.] JVM_DoPrivileged
16.88% java libjvm.so [.] GCTaskThread::run
12.31% java [kernel.kallsyms] [k] _raw_spin_unlock_irqrestore
8.19% java libc-2.17.so [.] __memcpy_avx_unaligned
看到GCTaskThread相关的热点,说明问题大概率出在JVM垃圾回收上,这时候应用日志和GC日志比系统层面的命令更有价值。看到__memcpy_avx_unaligned这种内存拷贝热点,可能要检查是不是有大量数据在做无谓拷贝。perf的真正威力是让你从“进程是谁”进步到“函数是什么”,但它是采样工具,只能反映统计趋势,不能当成精确的profile,需要多采样几轮来交叉验证。
4. 内存与Swap:系统慢的隐形推手
内存问题最坑的地方在于:它不一定会直接表现为内存相关,而是转嫁成CPU偏高或IO偏高。比如内存不够触发swap,换页动作会吃掉CPU和磁盘带宽,表现出来反而是一堆进程卡在D状态。所以我排查的顺序里,内存永远不是靠猜的,而是看一系列指标的组合。
4.1 free -h 的正确读法:buff/cache不是“已用”
free -h是每个人都会敲的命令,但很多人把used看一眼就下结论。正确的读法要看available,它才是系统估算的“在不触发swap的前提下还能分配多少内存”的值。内核会把空闲物理内存拿来当page cache(文件缓存),这部分在数值上会计入buff/cache,但它属于“可以随时回收”的资源,并不会让系统变慢。所以看到used高、buff/cache也很高时,不用紧张;只有当available本身快见底的时候,内存才是真正的瓶颈。
一个常见误判场景:free -h显示used占90%以上,运维同学开始加内存。但实际上available还有60%,说明系统只是在物尽其用地用page cache缓存文件,内存一点儿都不紧张。加内存是浪费预算。反过来,available很低、free几乎没有、swap还在持续增长,这才是真出事了。
4.2 vmstat 的 si/so:换页风暴的预警
判断内存压力最灵敏的信号是vmstat输出的si与so。这两个值表示每秒从swap换入内存、从内存换出到swap的量。如果它们持续不为0,甚至很大,说明内存已经吃紧,内核在反复把冷页换出到磁盘、把需要的页换进来。
swap的读写比普通文件IO还要致命,因为当你访问一块被换出的内存页时,必须先从磁盘读回来,这个操作会阻塞线程。你看到的现象很可能是进程并不忙,但整体响应慢得离谱。这种情况下,就算你查到了CPU使用率不高、IO也没有异常,也得回头怀疑swap。处理方式不是简单关swap,而是先找到是谁把内存吃光了,再决定是降配、扩容还是杀进程。
4.3 定位内存大户:smem、top 按内存排序、/proc/pid/smaps
当确认内存紧张后,下一个问题就是:谁偷走了内存?top里按大写M按物理内存排序,可以粗看一轮。但top默认的RES包含共享内存,一个多进程模型会被重复统计。要更客观地判断“实际独占占用”,建议用smem:
code复制# smem -k -s pss
PID User Command Swap USS PSS RSS
5400 app java -Xmx4g -jar app.jar 0 2.1G 2.3G 2.5G
7312 root /usr/bin/containerd 0 88M 110.4M 762.3M
USS是进程独占内存,PSS是包含按比例分摊的共享内存,RSS就是top里的RES。看PSS而不是RSS,能让你看清每个进程真实吃掉的内存。
另外,/proc/<PID>/smaps能给出这个进程内部各内存段的映射详情,包括大小、权限、是否可回收。排查内存泄漏时,我习惯每隔一段时间记录一次某进程的RSS和PSS,如果持续上升且不回落,基本可以判定泄漏;再结合pmap -x <PID>看看哪段映射在膨胀,就能把根因缩小到缓存、线程栈或者某个第三方库。
5. 磁盘与IO:当等待成为常态
IO问题在“系统慢”排查里占比极高,而且它最擅长伪装成CPU问题。开篇那个load average 8、top里wa 60%的场景,就是典型案例。磁盘IO的排查同样有三板斧:iostat看设备层、iotop看进程层、文件系统层查inode和日志。
5.1 iostat -x 1:%util、await、svctm 的解读陷阱
iostat -x 1会输出每块磁盘的详细指标:
code复制# iostat -x 1
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sda 2.56 3.22 48.12 82.33 0.00 0.55 0.00 14.62 6.33 7.12 0.03 18.80 25.57 1.80 1.04
sdb 56.21 103.40 1184.3 2058.11 0.00 0.00 0.00 0.00 688.32 1273.49 32.44 21.07 19.90 80.12 99.98
%util对传统机械盘基本可以等同“盘是否打满”,但对SSD和NVMe需要谨慎。因为%util计算的是“设备有请求未被完成的时间占比”,NVMe的并行能力强,即使长时间100%,IOPS和延迟依然可能很健康。所以我更看重await和aqu-sz:await是单次IO从发出到完成的总延迟,包含排队等待和设备处理;传统SATA盘await正常应在20ms内,SSD应该在1ms左右,如果看到几百毫秒甚至上千毫秒,说明IO链路已经严重拥塞。aqu-sz是平均队列长度,长时间大于设备可以并发处理的深度,说明上层请求在排队。
r_await和w_await分开看也很有用。如果写延迟远高于读延迟,大概率是刷盘策略、RAID写惩罚或者磁盘硬件降速;如果读写都高,更大可能是磁盘本身性能不够或链路有问题。
5.2 iotop:谁是IO消耗的元凶
iostat告诉你哪块盘慢,但没告诉你谁在折腾这块盘。这时上iotop -o,只显示实际有IO的进程:
code复制# iotop -o
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
2314 be/4 app 0.00 B/s 2084.32 K/s 0.00 % 99.12 % java -Xmx4g -jar app.jar
2315 be/4 app 12.31 K/s 823.11 M/s 0.00 % 98.73 % java -Xmx4g -jar app.jar
看到某个线程DIO写接近满带宽时,下一步不是急着kill,而是用lsof -p <PID>看它打开哪些文件,再定位到具体日志文件或者数据文件。常见的坑有两类:一类是业务日志级别被误调成DEBUG,日志量瞬间爆炸;另一类是数据库的刷脏线程在做大页写入,遇到这种要先看数据库本身的IO能力,而不是贸然限制它。
5.3 文件系统层:inode 耗尽与日志抖动
IO层面的问题有时会先暴露在文件系统层。df -h看磁盘空间够,但df -i看inode使用率100%,也会出现“no space left on device”,这种现象常见于小文件特别多的目录,比如邮件队列、临时文件、消息中间件堆积。查法很简单:df -i找出哪个分区inode满,再用for i in /path/*; do echo "$i $(find $i | wc -l)"; done找出小文件大户,清掉过期的临时文件。
另一个容易被忽略的是日志抖动。系统里某个服务的日志量特别大,会持续产生写IO,即使单次写不大,也会把IO队列占住。遇到这种情况,先看/var/log下每个日志文件的大小和增长速率,再检查logrotate是否正常工作。日志文件如果一直被打描、压缩、切割失败,也会出现周期性IO尖峰。
6. 网络与内核态:常规手段查不到时的深挖
CPU、内存、磁盘都查过一轮,一切正常,但系统还是慢,这时候把目光转向网络和内核态。网络问题之所以难查,是因为它可能发生在网卡、驱动、协议栈、连接队列任何一层,而且应用感知到的“慢”往往是上游问题传导过来的。
6.1 网络慢:先分清网卡层还是协议栈层
我遇到过太多情况:应用报网络慢,大家第一反应是看带宽,结果带宽占用不到20%。其实“网络慢”要先分清是链路慢还是协议栈处理慢。链路慢可以看ping的延迟与丢包,也可以看是否跨公网、跨机房;协议栈慢则表现为延迟不高但吞吐上不去、连接建立失败、TCP重传率上涨。
用sar -n DEV 1看网卡吞吐和错误包,如果rxerrs、txerrs、rxdrop这些列有增长,优先怀疑网卡驱动、链路质量、MTU不匹配。用netstat -s或ss -s看TCP统计,retrans速率异常上涨说明网络中存在丢包或拥塞,这时抓包看TCP重传的序列号,能判断是哪个方向在丢。
6.2 ss 与 netstat:连接状态、发送接收队列的核查
ss -lnt可以看到监听队列的长度:
code复制# ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 128 128 0.0.0.0:8080 0.0.0.0:*
LISTEN 512 512 0.0.0.0:8081 0.0.0.0:*
LISTEN 1024 1024 0.0.0.0:27017 0.0.0.0:*
对LISTEN状态的套接字,Send-Q表示backlog上限,Recv-Q表示当前已建立但还没被accept处理的连接数。如果Recv-Q经常逼近甚至超过Send-Q,说明应用accept太快跟不上或者陷入阻塞,客户端排队等待连接,表现就是“连接很慢、请求超时”。这种情况要在应用层找原因,是线程池耗尽、GC停顿还是数据库慢查询导致的处理停滞。
6.3 dmesg 与被忽略的内核日志
很多奇葩性能问题最后都能在dmesg -T里找到答案。内核在OOM、磁盘IO错误、网络设备重置、硬件NMI中断、驱动bug时,都会往内核环形缓冲区里写信息。我排查任何性能问题都会习惯性先看一眼dmesg尾部:有没有Out of memory、I/O error、hung_task_timeout_secs、cede loop一类的关键字。这些日志往往比任何监控图都直白,能在几秒内告诉你这台机器其实已经出过硬件故障。
举一个实际例子:某个系统周期性卡顿,CPU、内存、IO测下来都摸不着头脑,最后dmesg里反复出现网卡重置信息和blocked for more than 120 seconds的hung task提醒,顺着查才发现是网卡固件bug在特定流量模型下触发了重置,升级固件后卡顿消失。所以不要小看dmesg,它是内核给排障者留下的“案发现场”。
7. 实战复盘:一次“系统假死”的完整排查链路
前面讲了方法论和工具,最后我复盘一次真实的“系统假死”排查过程,把整条链路串起来。这是我觉得最有价值的部分,因为它能让你看到各个工具是怎么衔接的,而不是孤立地跑一条命令。
7.1 现象描述与初步方向
那天下午,同事报障:某核心服务的接口RT从几十毫秒涨到了三十秒,登录服务器执行任意命令都要等好几秒,机器看起来像要“死”了。第一轮快照下来:uptime显示load average 15.8,top里%Cpu(s)明显wa占比60%以上、us只有10%,task列表里大量进程处于D状态,vmstat里b列高达40。
初步方向立刻收敛了:这是IO阻塞问题,不是CPU算力问题,也不是单纯内存问题。为什么敢这么判断?因为load高但us不高、wa高,且存在大量D状态进程,说明CPU在空转等待IO完成。虚拟机st也不高,宿主机抢占的嫌疑可以排除。
7.2 从 load 高到 D 状态进程:逐步推进的记录
接着执行iostat -x 1,结果很清晰:数据盘sdb的%util 100%,r_await 688ms、w_await 1273ms,aqu-sz超过30,而系统盘sda一切正常。这说明sdb这条链路已经被打满到令人绝望的程度。
再执行iotop -o,锁定了元凶线程:某个Java进程的一根线程在以800MB/s左右的速度疯狂写盘。用lsof -p <PID>查看这个线程打开的文件,发现它正在往/data/applogs/biz-debug.log写入,而且这个文件正好在sdb上。用ls -lh一看,这个日志文件已经在几个小时内膨胀到30GB了,并且还在以肉眼可见的速度增长。
接下来就是确认根因。去看应用的日志配置,发现某次发布时把该模块日志级别从WARN误调成了DEBUG,而且该接口处于一个高频调用路径上,于是每个请求都会打印大量调试日志,IO被这些日志完全灌满。日志量大、写入频繁、设备是普通机械盘、文件又在一个独立数据盘上,这几个因素叠加,直接把系统拖成假死。
7.3 根因与修复,以及这个案例教会我的事
修复动作分两步:先通过配置中心紧急把日志级别改回WARN,让日志写入量立刻降下来;再清理这个30GB的日志文件,由于文件可能被进程持续占用,清理时用truncate -s 0 <file>而不是直接rm,避免文件句柄指向已删除文件导致space不释放。
改动生效后的两分钟内,load从15掉回2以下,接口响应恢复几十毫秒。事后复盘,这个问题的根因不是“系统坏了”,而是一次没有经过评估的日志级别变更。整个排查链路真正花时间的不是跑命令,而是把“load高”这个模糊现象拆解成“wa高→D状态→sdb打满→某线程狂写→DEBUG日志”这条因果链。
这类问题我后来还碰到过不止一次,所以现在每次做变更都会先确认日志级别是否符合规范,同时给日志盘加了一个大小监控和日志增长速率监控,超过阈值直接报警。性能排查的价值不止在于解决当下问题,更在于让你意识到哪些环节最脆弱、最需要提前设防。从这个角度看,排查一次“系统慢”,远远不止是敲几条命令那么简单。
