2024 年我们一台应用服务器出过一次很典型的"变慢"故障。运营反馈说系统卡,接口忽快忽慢,但又没到完全不可用的程度。我登上去想看看怎么回事,却一下子有点发懵——服务器变慢的原因太多了:CPU 可能被烧满,内存可能不够,磁盘 IO 可能堵了,网络可能出问题。到底是哪一个?当时我对着终端,东敲一个命令、西敲一个命令,看了半天也没理出头绪。事后我痛定思痛,把 Linux 服务器性能排查这件事认认真真梳理成了一套有顺序、有章法的方法论。本文复盘这次实战,把这套"四板斧"讲清楚。
问题背景
环境:CentOS 7,Java 应用服务器
事故现象:
- 系统卡顿,接口响应忽快忽慢
- 没有完全宕机,但体验明显劣化
- 持续了一两个小时,时好时坏
现场排查(一开始的混乱):
# 我当时是这样东一榔头西一棒子的:
$ top # 看了一眼,CPU 好像有点高?
$ free -h # 内存好像也不太够?
$ ps aux # 进程一大堆,看不出名堂
# —— 没有顺序,没有判断标准,看了等于没看
复盘后定下的排查顺序:
1. top / uptime —— 先看 load 和 CPU 总体,定方向
2. CPU 细分 —— us / sy / wa,区分三种"忙"
3. 内存 —— free,但别被 buff/cache 骗了
4. 磁盘 IO —— iostat,看是不是 IO 在拖
5. 锁定进程后 —— 再往进程内部钻
6. sar —— 看历史数据,事后也能复盘
根因(后来定位到的):
这次是一个定时任务在跑大批量数据导出,
既吃 CPU 又打满磁盘 IO,和在线请求抢资源。
修复 1:top 先看清——load 和 CPU 总体
# === 排查永远从 top 开始,它是性能的"总览仪表盘" ===
$ top
top - 15:42:01 up 30 days, load average: 18.7, 16.2, 9.4
Tasks: 210 total, 3 running, 207 sleeping
%Cpu(s): 35.2 us, 12.1 sy, 0.0 ni, 8.3 id, 44.1 wa, ...
MiB Mem : 16000 total, 800 free, 9000 used, 6200 buff/cache
# === 先看 load average:三个数,过去 1/5/15 分钟 ===
# load 大致表示"正在运行 + 正在等待资源的任务数"。
# 怎么判断它高不高?要除以 CPU 核数:
$ nproc
8
# load 18.7 / 8 核 ≈ 2.3 —— 平均每个核背着 2.3 个任务,
# 明显过载了。
# 经验:load / 核数 ≈ 1 算满,持续 > 1 就是过载。
# === 看三个数的趋势 ===
# load average: 18.7, 16.2, 9.4
# 1 分钟(18.7) > 15 分钟(9.4) -> 负载正在【上升】
# 反过来,如果 1 分钟的数最小,说明高峰已经过去。
# === 再看 %Cpu(s) 那一行,它告诉你 CPU 在忙什么 ===
# 这一行是后面所有判断的分水岭,下一节专门讲。
# === top 里几个实用交互键 ===
# P 按 CPU 使用率排序(默认)
# M 按内存使用率排序
# 1 展开显示每个 CPU 核心,看负载是否均衡
# 数字键能看出:是所有核都忙,还是只有一两个核被打满
# (单核打满往往是单线程的程序)
修复 2:CPU 高,要区分三种完全不同的"忙"
=== %Cpu(s) 那一行,每个字段的含义 ===
us (user) : 用户态 CPU —— 跑你的应用代码、业务逻辑
sy (system) : 内核态 CPU —— 系统调用、内核在干活
ni (nice) : 跑被调过优先级的进程
id (idle) : 空闲
wa (iowait) : CPU 空着,在【等磁盘/网络 IO】完成
hi/si : 处理硬中断 / 软中断
st (steal) : 被同物理机上的其它虚拟机抢走的时间
=== 关键:us 高、sy 高、wa 高,是三个不同的病 ===
【us 高】—— 应用自己在烧 CPU
含义:你的程序在大量做计算。
可能:死循环、复杂运算、频繁 GC、正则回溯、
序列化/反序列化太重。
方向:往【应用进程内部】查,看是哪个线程在烧。
【sy 高】—— 内核在烧 CPU
含义:系统调用太频繁。
可能:频繁创建销毁进程/线程、海量小 IO 系统调用、
频繁的上下文切换、网络包处理过多。
方向:看上下文切换(vmstat 的 cs)、系统调用。
【wa 高】—— CPU 在干等 IO
含义:CPU 本身不忙,它在等磁盘/网络把数据给它。
★ 我们这次的现场:wa 高达 44%
可能:磁盘 IO 是瓶颈,大量读写把磁盘堵住了。
方向:不要再盯 CPU 了,转去查【磁盘 IO】。
=== 一个最常见的误判 ===
看到 top 里 CPU 占用高,就以为是"CPU 不够、要加核"。
但如果高的是 wa,加 CPU 核心一点用都没有 ——
病根在磁盘,CPU 只是陪着干等。
先分清 us / sy / wa,再决定往哪个方向查,
这是整个性能排查里最重要的一次分流。
# === vmstat:比 top 更适合看 CPU/上下文切换的动态 ===
$ vmstat 2 5 # 每 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
5 3 0 820000 10000 6200000 0 0 8200 6400 5200 9800 35 12 9 44 0
# r:正在运行/等 CPU 的进程数。r 持续 > 核数 = CPU 不够
# b:处于不可中断睡眠(通常在等 IO)的进程数。b 高 = IO 堵
# cs:每秒上下文切换次数。cs 异常高 = 线程太多/竞争激烈
# wa:和 top 一致,这里 44 —— IO 等待严重
# si/so:swap 换入换出。非 0 说明内存不够、在用交换分区
修复 3:内存——别被 free 的 buff/cache 骗了
# === 看内存用 free ===
$ free -h
total used free shared buff/cache available
Mem: 15Gi 8.8Gi 0.8Gi 0.2Gi 5.9Gi 5.5Gi
Swap: 2.0Gi 0.3Gi 1.7Gi
# === 新手最大的误区:看到 free 只剩 0.8Gi 就慌 ===
# "内存只剩 800M 了,要爆了!" —— 这是错的。
# === buff/cache 是什么 ===
# Linux 有个理念:空闲的内存就是浪费的内存。
# 所以它会主动拿暂时没用的内存去缓存磁盘文件
# (buff/cache),让下次读文件更快。
# 这部分内存【随时可以还回来】—— 一旦程序要用,
# 系统立刻把 cache 释放掉给程序。
# === 真正该看的是 available,不是 free ===
# available:估算"现在还能给新程序用多少内存",
# 它已经把"可回收的 cache"算进去了。
# 上面 available 是 5.5Gi —— 内存其实还宽裕,
# 别被 free 的 0.8Gi 吓到。
# 口诀:free 看 available,不要看 free 那一列。
# === 什么时候内存才是真的紧张 ===
# 1. available 很低(真的没多少可用了)
# 2. swap 的 used 在持续增长 —— 内存不够,
# 系统被迫把内存数据往磁盘的交换分区挪
# 3. vmstat 里 si/so 持续非 0 —— 正在频繁换页,
# 这个非常伤性能,磁盘速度比内存慢几个数量级
# === 谁吃了内存:按内存排序看进程 ===
$ ps aux --sort=-%mem | head -6
# 或在 top 里按 M 键
# === 看某个进程内存的细节 ===
$ cat /proc//status | grep -E 'VmRSS|VmSwap'
# VmRSS:进程实际占用的物理内存(常驻内存)
修复 4:磁盘 IO——iostat 揪出真凶
# === 上一步 wa 高,现在转战磁盘 IO ===
# iostat 来自 sysstat 包(yum install sysstat)
$ iostat -x 2 3 # -x 显示扩展信息,每 2 秒一次
Device r/s w/s rkB/s wkB/s await r_await w_await %util
vda 120 480 8400 61000 85.3 12.1 103.5 99.2
# === 怎么读这几个关键列 ===
# r/s, w/s : 每秒读 / 写次数(IOPS)
# rkB/s,wkB/s : 每秒读 / 写的数据量(吞吐)
# await : 平均每个 IO 请求的耗时(毫秒)——
# 这个数高,说明磁盘响应慢、在排队
# %util : 磁盘繁忙度。★ 最直观的一个指标
# 接近 100% = 磁盘几乎一刻不停在干活,
# 已经是瓶颈了
# === 我们这次的现场 ===
# %util 99.2% —— 磁盘被打满了
# w/s 480、wkB/s 61MB/s —— 大量写入
# await 85ms —— IO 请求平均要等 85 毫秒,非常慢
# 结论:磁盘 IO 就是这次卡顿的瓶颈。
# === 下一个问题:是哪个进程在疯狂读写磁盘 ===
$ iotop -o
# -o 只显示真正在产生 IO 的进程
PID USER DISK READ DISK WRITE COMMAND
23891 app 0.00 B/s 58.20 M/s java -jar export-job.jar
# 真凶现身:一个数据导出任务,每秒往磁盘写 58M
# === 也可以看单个进程累计的 IO ===
$ cat /proc/23891/io
read_bytes: 120000000
write_bytes: 9800000000
# 这个进程已经写了 9.8G
修复 5:锁定进程后,再往进程内部钻
# === 前四步把范围收敛到了"某个进程",现在往里钻 ===
# 我们这次是个 Java 进程,以 Java 为例。
# === 第一步:找出进程内"最烧 CPU 的线程" ===
$ top -Hp 23891 # -H 显示线程,-p 指定进程
# 找到 %CPU 最高的那个线程的 PID,假设是 24015
# === 第二步:把线程 PID 转成 16 进制 ===
$ printf '%x\n' 24015
5dcf
# === 第三步:打印 Java 线程栈,定位到具体代码 ===
$ jstack 23891 | grep -A 30 '0x5dcf'
"export-worker-3" #45 prio=5 ... nid=0x5dcf runnable
java.lang.Thread.State: RUNNABLE
at com.app.export.ExportJob.writeRows(ExportJob.java:128)
...
# 一路追到 ExportJob.java:128 —— 就是它在猛写
# === 通用手段(不限于 Java)===
# 看进程在做什么系统调用:
$ strace -p 23891 -c -f # -c 统计,-f 跟子线程
# 它会统计这个进程在调哪些系统调用、各花多少时间。
# 比如发现大量 write 调用,印证它在疯狂写磁盘。
# 看进程打开了哪些文件:
$ ls -l /proc/23891/fd
# 能看到它正在写哪个文件
# === 这次的处理 ===
# 确认是凌晨数据导出任务,被误配成了白天高峰跑。
# 处理:① 立刻把这个任务降优先级,别和在线抢
$ ionice -c 3 -p 23891 # 把它的 IO 优先级设为 idle
$ renice +19 -p 23891 # 把它的 CPU 优先级调到最低
# ② 改调度,挪到凌晨低峰执行
# ③ 长远:导出任务和在线业务做资源隔离
修复 6:sar——没在现场,也能复盘历史
# === 一个尴尬:故障往往是"过去时" ===
# 运营说"刚才卡了一下",等你登上去,top 一切正常。
# 现场没了,top/iostat 这些"实时"工具就没用了。
# 这时需要能看【历史】的工具 —— sar。
# === sar 会自动、定期采集系统指标并存档 ===
# 它同样来自 sysstat 包,装好后默认每 10 分钟
# 采一次,数据存在 /var/log/sa/ 下。
# === 看某天的历史 CPU 使用情况 ===
$ sar -u -f /var/log/sa/sa15 # sa15 = 当月 15 号
12:00:01 CPU %user %system %iowait %idle
14:30:01 all 35.10 12.04 44.21 8.65
14:40:01 all 38.22 11.80 45.10 4.88
# 一眼看到 14:30 那会儿 iowait 飙到了 44% ——
# 哪怕事故已经过去,也能精确还原当时的状况
# === sar 能看的历史指标很全 ===
$ sar -u # CPU(今天的)
$ sar -r # 内存
$ sar -b # 磁盘 IO 总览
$ sar -d # 每块磁盘的 IO
$ sar -n DEV # 网络流量
$ sar -q # load average 和运行队列
$ sar -W # swap 换页
# 加 -f /var/log/sa/saXX 就是看历史某天
# === 经验 ===
# 实时排查靠 top/vmstat/iostat,
# 事后复盘靠 sar。
# 一台生产服务器,sysstat(提供 sar)应该是标配,
# 装好、确认它的定时采集在正常跑。
# 否则故障一过,你就永远失去了现场。
命令速查
排查阶段 命令 看什么
=============================================================
总览 top / uptime load average、%Cpu 那一行
CPU 动态 vmstat 2 r、b、cs、us/sy/wa
区分忙的类型 top 的 %Cpu(s) us 高/sy 高/wa 高,三个方向
内存 free -h 看 available,不是 free
内存换页 vmstat 的 si/so 非 0 = 内存不够在用 swap
磁盘 IO iostat -x 2 %util、await
哪个进程在 IO iotop -o DISK READ/WRITE
进程内热点线程 top -Hp 找最烧 CPU 的线程
Java 线程栈 jstack 定位到代码行
系统调用 strace -p -c 进程在调什么系统调用
历史复盘 sar -u/-r/-d -f saXX 事后还原当时状况
口诀:top 定方向 -> us/sy/wa 分流 -> 锁进程 -> 钻内部 -> sar 复盘
避坑清单
- 性能排查要有固定顺序:top 定方向,再按 CPU/内存/IO 分头深入,别东一榔头西一棒子
- load average 要除以 CPU 核数来判断,看三个数的趋势能知道负载在升还是在降
- CPU 高必须先分清 us/sy/wa,这三个是完全不同的病,决定了往哪个方向查
- wa 高是 CPU 在等 IO,此时加 CPU 核心毫无用处,病根在磁盘,要转查 IO
- free 看内存别看 free 那一列,buff/cache 可随时回收,真正该看的是 available
- swap 的 used 持续增长、vmstat 的 si/so 非 0,才是内存真的紧张的信号
- iostat 的 %util 接近 100% 说明磁盘被打满,await 高说明 IO 请求在排队
- 用 iotop -o 找出真正在产生磁盘 IO 的进程,别只凭猜测
- 锁定进程后用 top -Hp 找热点线程,Java 配合 jstack,通用场景用 strace
- 生产服务器必装 sysstat,sar 能看历史数据,故障过去了也能复盘当时的现场
总结
这次服务器变慢的排查,真正给我上了一课的,不是某一个具体的命令,而是"排查要有章法"这件事本身。我清楚地记得故障刚发生时我的样子:登上服务器,先敲个 top 瞄一眼,觉得 CPU 好像有点高;又敲个 free,觉得内存好像也有点紧;再敲个 ps,面对一屏幕的进程不知道该看哪个。我看了一大堆命令的输出,却始终没能形成任何一个判断,因为我心里根本没有一套顺序,也没有一套标准——我只是在漫无目的地"看",而不是在"排查"。事后我把这件事彻底想清楚了:服务器性能排查,本质上是一个不断收敛范围的过程,你必须先有一个总览,从总览里分流出方向,顺着方向锁定到具体的进程,最后再钻进进程内部找到那一行代码。这个过程必须是有顺序的,跳过任何一步,或者顺序乱了,你都会迷失在海量的指标里。这套方法论的第一步,永远是 top,它是整台服务器的总览仪表盘。在 top 里,我最先要看的是 load average 那三个数字,但看它有个前提——必须拿它去除以 CPU 的核数,一个 load 18 在八核机器上是严重过载,在三十二核机器上却很轻松;同时这三个数字分别代表过去一分钟、五分钟、十五分钟,把它们放在一起看趋势,就能知道负载到底是正在往上冲,还是高峰已经过去。但 top 里真正决定整个排查走向的,是 %Cpu 那一行,它告诉你 CPU 究竟在忙些什么——而这正是我过去最大的盲区。我以前看到 CPU 占用高,脑子里唯一的反应就是"CPU 不够用了,得加核",可这次我才真正理解,CPU 的"忙",其实是三种完全不同的病:us 高,是你自己的应用代码在疯狂做计算;sy 高,是内核在忙,通常意味着系统调用太频繁;而 wa 高,意思是 CPU 其实一点都不忙,它只是空空地坐在那里,等着磁盘或网络把数据喂给它。这三种病的查法南辕北辙,而我们这次的现场,wa 高达百分之四十四——如果我当时顺着"CPU 高就加核"的直觉去扩容,加再多的 CPU 核心也是徒劳,因为那些核心只会跟着一起干等,病根压根不在 CPU,而在磁盘。所以 us、sy、wa 的这一次分流,是整个排查里最关键、也最省功夫的一个动作:它一步就把你从"CPU 方向"切换到了正确的"磁盘 IO 方向"。顺着 wa 高这条线索转去查磁盘,iostat 里有一个指标特别直观,就是 %util,它代表磁盘的繁忙度,当它贴近百分之百,就说明这块磁盘已经一刻不停在连轴转了,它就是瓶颈;再用 iotop 一看,真凶立刻现形——是一个数据导出任务,每秒往磁盘灌进去几十兆。范围收敛到这个进程之后,再往里钻就水到渠成了,对 Java 进程可以用 top -Hp 找出最烧的线程、再用 jstack 把它精确定位到某一行代码,对任何进程都可以用 strace 去看它到底在反复调用哪些系统调用。最后我还想特别说一下 sar,因为它解决的是性能排查里一个特别让人无奈的尴尬:故障常常是"过去时"。运营跑来说"刚才系统卡了一下",可等你手忙脚乱登上服务器,top 显示一切风平浪静,现场已经没了——top、iostat、vmstat 这些工具全都只能看"此刻",现场一旦消失,它们就全部失效。而 sar 是那个能让你回到过去的工具,只要服务器装了 sysstat,它就会默默地、每隔几分钟采集一次系统的各项指标并存档,哪怕事故已经过去几个小时,你依然能把当时那一刻的 CPU、内存、IO 状况精确地调出来还原。所以我现在把 sysstat 当成每一台生产服务器的标配,装好它、并且确认它的定时采集确实在跑,因为它保存的,是你未来某次深夜排障时唯一能抓住的那个"现场"。这次排查从最初的手忙脚乱,到最后整理出 top 定方向、us/sy/wa 分流、iostat 锁瓶颈、jstack 钻代码、sar 留复盘这样一条清晰的链路,我最大的收获就是明白了:面对一台变慢的服务器,真正能救你的,从来不是你记住了多少个命令,而是你心里有没有那张"先看什么、再看什么、看到这个结果该往哪走"的地图。命令谁都能查到,而那张地图,得靠一次次这样的复盘,一笔一笔自己画出来。
—— 别看了 · 2026