Linux CPU 性能分析:从平均负载、上下文切换到 iowait 与僵尸进程
Linux CPU 性能分析:从平均负载、上下文切换到 iowait 与僵尸进程
Linux 性能问题真正难的地方,通常不是缺少命令,而是不知道应该观察什么、指标之间是什么关系、下一步为什么要用这个工具。
top、uptime、vmstat、pidstat、iostat、perf、strace 都很常见,但如果只会机械地执行命令,很容易陷入一种尴尬状态:看到 CPU 高就重启进程,看到 Load 高就扩容,看到 iowait 高就怀疑磁盘,结果问题偶尔消失,却没有真正定位瓶颈。
性能分析更像一条证据链:
先从宏观指标确认系统发生了什么,再逐层缩小范围,最后落到具体进程、线程、系统调用或函数。
这篇文章围绕 Linux CPU 性能分析建立一套完整的知识框架,重点讨论:
- 平均负载到底表示什么,为什么它不等于 CPU 使用率;
- CPU 使用率各个字段应该如何解释;
- CPU 上下文切换为什么会消耗性能;
- 如何区分 CPU 密集、I/O 等待和调度竞争;
- 为什么系统 CPU 很高,却找不到高 CPU 进程;
- 如何处理短时进程、不可中断进程和僵尸进程;
- 如何把
uptime、mpstat、pidstat、vmstat、perf、strace、pstree等工具串成一套排查流程。
1. 性能优化的核心不是工具,而是指标、原理和证据链
从应用视角看,性能最常见的两个核心指标是:
- 吞吐量(Throughput):单位时间内系统能够完成多少工作,例如 QPS、TPS、PPS;
- 延迟(Latency):一次请求从开始到完成需要多久。
从系统资源视角看,还要观察:
- CPU 使用率;
- 平均负载;
- 内存使用情况;
- I/O 使用率、吞吐量和延迟;
- 网络吞吐和丢包;
- 上下文切换;
- 中断;
- 资源饱和度。
性能问题的本质通常可以描述为:
某类资源接近瓶颈,导致请求处理速度无法继续随着负载增长而增长,最终表现为吞吐下降、延迟升高甚至请求失败。
一个完整的性能优化闭环可以抽象为:
flowchart LR
A[选择性能指标] --> B[设定性能目标]
B --> C[建立性能基线]
C --> D[定位瓶颈]
D --> E[实施优化]
E --> F[重新验证]
F --> G[监控与告警]
G --> D
这里最容易被忽略的是基线。
例如看到每秒上下文切换 8000 次,单独看这个数字没有太大意义;如果这台机器平时稳定在 7000~9000 次,那么它可能完全正常。但如果系统过去长期只有 500 次,突然上升到 8 万次,就应该重点调查。
因此,性能分析不应该是“看到某个数字大就判定异常”,而应该同时考虑:
- 指标的物理含义;
- 系统的资源规模;
- 历史基线;
- 指标之间是否相互印证;
- 异常发生时业务负载是否变化。
2. 建立 CPU 性能分析的整体视图
一次请求从应用进入操作系统,大致会经过:
1 | |
性能分析可以从两个方向观察:
- 应用负载视角:吞吐、延迟、请求数、并发量;
- 系统资源视角:CPU、内存、磁盘、网络、中断、调度。
这两个方向最终需要在某个位置汇合。
例如:
1 | |
这就是一条完整的性能证据链。
相比之下,下面这种排查方式是不完整的:
1 | |
重启可能让症状消失,却没有解释“为什么”。
3. 平均负载 Load Average 到底是什么
执行:
1 | |
可能看到:
1 | |
最后三个数字分别表示过去:
- 1 分钟;
- 5 分钟;
- 15 分钟;
的平均负载。
3.1 Load Average 不是 CPU 使用率
这是最常见的误区。
平均负载不是“CPU 使用率的百分比”,它更接近:
单位时间内处于可运行状态和不可中断状态的平均任务数。
也就是:
- 正在 CPU 上运行的任务;
- 等待 CPU 的任务;
- 处于不可中断睡眠状态的任务。
在进程状态中主要对应:
R:Running / Runnable;D:Uninterruptible Sleep。
所以 Load 高,可能是:
- CPU 真忙;
- 大量任务排队等 CPU;
- 大量任务在等待 I/O 或其他不可中断资源。
这也是为什么:
Load Average 高,不代表 CPU 使用率一定高。
3.2 R 状态和 D 状态
R:运行或可运行
R 状态包含:
- 正在 CPU 上运行;
- 已经具备运行条件,但正在 Run Queue 中等待 CPU。
当 Runnable 任务远多于 CPU 的逻辑处理器数量时,意味着 CPU 竞争严重。
D:不可中断睡眠
D 状态通常意味着任务正在等待内核中的某个关键操作完成,常见场景是硬件 I/O。
一个容易产生误解的地方是“不可中断”。它不是说:
这个进程霸占着 CPU,连硬件中断都不能发生。
更准确地理解是:
任务进入了不可中断睡眠,普通信号不能把它提前唤醒;它仍然已经让出了 CPU。
另外,D 状态并不等价于磁盘 I/O。磁盘 I/O 是常见原因,但不是唯一原因。因此看到 D 进程只能说明“它值得重点调查”,不能直接宣布磁盘故障。
3.3 平均负载应该和多少 CPU 比
最理想的情况,是每个逻辑 CPU 上都有一个可运行任务。
查看逻辑 CPU 数量可以使用:
1 | |
也可以用:
1 | |
假设机器有 2 个逻辑 CPU:
- Load ≈ 2:基本被充分利用;
- Load < 2:仍有调度余量;
- Load > 2:开始出现排队或不可中断任务。
比较平均负载时,一般应与操作系统可见的逻辑 CPU 数进行比较。
3.4 1、5、15 分钟负载怎么看趋势
三个值不是“三选一”,而是共同描述趋势。
情况一:三个值接近
1 | |
说明负载比较平稳。
情况二:1 分钟远小于 15 分钟
1 | |
说明系统此前压力很大,但正在恢复。
情况三:1 分钟远大于 15 分钟
1 | |
说明系统最近出现新的压力,需要继续观察是否会持续。
3.5 不要迷信“70% 阈值”
课程中给出了“Load 超过 CPU 数量的约 70% 就应该关注”的经验值。这个数字只能作为启发式参考,不能当成所有系统的统一告警线。
真正可靠的做法是:
- 持续采集平均负载;
- 记录业务高峰和低峰;
- 建立机器自己的历史基线;
- 关注趋势变化和数量级变化。
4. 平均负载高,怎么判断到底是谁导致的
Load 高后不能停在 uptime,而要进一步回答:
活跃任务到底在干什么?
最常见的三类场景是:
| 场景 | Load | CPU 使用率 | 典型特征 |
|---|---|---|---|
| CPU 密集 | 高 | 高 | %usr 或 %sys 高 |
| I/O 等待 | 高 | 不一定高 | %iowait 高、D 进程增加 |
| CPU 竞争 | 高 | 高 | Run Queue 很长、%wait 高、上下文切换多 |
4.1 CPU 密集场景
模拟一个 CPU 忙任务:
1 | |
观察平均负载:
1 | |
观察每个 CPU:
1 | |
定位进程:
1 | |
如果看到:
- 某个 CPU 接近 100%;
iowait很低;- 某个进程
%CPU接近 100%;
则 Load 上升主要由 CPU 密集任务导致。
4.2 I/O 等待场景
早期案例使用:
1 | |
但这一写法在不同系统上的效果并不稳定,因为它主要通过 sync() 制造压力;如果缓存较少,可能只看到系统 CPU 升高,未必看到明显 iowait。
更可靠的实验方式可以使用支持更多 I/O 场景的 stress-ng,但具体参数应根据当前发行版手册确认。
判断 I/O 问题时,不要只看 Load,而应结合:
1 | |
4.3 大量 Runnable 任务竞争 CPU
例如在 2 CPU 机器上创建 8 个 CPU 任务:
1 | |
此时通常会看到:
- Load 接近任务数;
- CPU 接近饱和;
- 每个任务
%wait很高; - 非自愿上下文切换增加。
这类问题的关键不是“某个进程单独跑得特别慢”,而是:
系统创建了超过 CPU 调度能力的活跃任务。
5. CPU 使用率到底是怎么算出来的
CPU 使用率比 Load 更直观,但要正确解释它,必须理解它的来源。
Linux 会把不同 CPU 状态累计在:
1 | |
示例:
1 | |
这些值是从系统启动以来累计的 CPU 时间。
系统性能工具通常不会直接用开机以来的累计值,而是间隔一段时间取两次采样做差。
可以近似写成:
1 | |
某一种 CPU 状态的比例则是:
1 | |
因此,不同工具如果采样窗口不同,结果可能不一致。
例如:
top是短时间滚动采样;ps常反映进程生命周期内的平均情况。
不能拿两个不同时间窗口的数字直接硬比。
5.1 CPU 各字段的含义
常见字段如下:
| 字段 | 含义 | 常见排查方向 |
|---|---|---|
us / usr |
用户态 CPU | 应用计算热点、业务代码 |
ni |
nice 调整后的低优先级用户态 CPU | 低优先级任务 |
sy / sys |
内核态 CPU | 系统调用、上下文切换、内核线程 |
id |
CPU 空闲 | 没有 CPU 工作 |
wa / iowait |
CPU 空闲期间存在 I/O 等待任务 | 存储、块设备、I/O 模式 |
hi / irq |
硬中断 | 设备中断 |
si / softirq |
软中断 | 网络、软中断处理 |
st / steal |
虚拟机被宿主机抢走的 CPU 时间 | 虚拟化资源竞争 |
guest |
运行虚拟机的 CPU 时间 | 虚拟化工作负载 |
5.2 iowait 最容易被误解
很多人会把:
1 | |
直接解释为:
1 | |
这并不成立。
从 CPU 记账角度看,iowait 更接近:
CPU 本来处于 idle,但此时系统存在等待 I/O 完成的任务。
所以 iowait 高并不能单独证明磁盘已经达到设备极限。
判断 I/O 是否真的成为瓶颈,还应该继续看:
- 磁盘吞吐;
- IOPS;
- I/O 延迟;
- 队列长度;
- 具体进程的 I/O;
- 设备是否达到饱和。
6. CPU 上下文是什么,为什么切换会变慢
Linux 可以同时运行远多于 CPU 数量的任务,但单个 CPU 在某个瞬间仍然只能执行一个执行流。
CPU 从任务 A 切换到任务 B 前,必须先保存 A 的运行状态,再恢复 B 的运行状态。
最基本的 CPU 上下文包括:
- CPU 寄存器;
- Program Counter(程序计数器);
- 栈指针等执行状态。
上下文切换可以抽象为:
sequenceDiagram
participant A as Task A
participant K as Kernel Scheduler
participant B as Task B
A->>K: 触发调度
K->>K: 保存 A 的 CPU 上下文
K->>K: 选择新的可运行任务
K->>K: 恢复 B 的上下文
K->>B: 从 B 上次的位置继续运行
问题在于:
保存和恢复上下文不是免费的。
它要消耗 CPU,并且可能影响:
- CPU Cache;
- TLB;
- 内核栈;
- 地址空间相关状态。
当切换特别频繁时,CPU 花在“切任务”上的时间越来越多,真正执行应用代码的时间反而越来越少。
7. 三种上下文切换:进程、线程和中断
7.1 进程上下文切换
Linux 中进程既可能运行在用户态,也可能通过系统调用进入内核态。
传统 x86 权限模型中通常可以理解为:
- Ring 3:用户态;
- Ring 0:内核态。
进程上下文不仅包含寄存器等 CPU 状态,还关联:
- 虚拟地址空间;
- 用户栈;
- 内核栈;
- 进程级资源状态。
不同进程之间切换时,代价通常比同进程线程之间切换更高。
7.2 系统调用不是“切换到另一个进程”
例如:
1 | |
系统调用会经历:
1 | |
但它仍然是同一个进程。
因此需要区分:
- 特权级切换 / mode switch:同一进程在用户态与内核态之间切换;
- 进程上下文切换:从进程 A 切到进程 B。
系统调用同样有 CPU 上下文保存和恢复成本,但不需要完成完整的进程地址空间切换。
7.3 线程上下文切换
线程是内核调度的基本执行单位,进程更像资源容器。
同一进程内的线程共享:
- 虚拟内存;
- 全局变量;
- 打开的资源等进程级对象。
线程自己仍拥有:
- 栈;
- 寄存器;
- 调度状态。
所以:
- 不同进程的线程之间切换,成本接近进程切换;
- 同一进程内的线程切换,因为地址空间等资源可以继续复用,通常更轻量。
7.4 中断上下文切换
硬件事件发生时,CPU 会暂停当前执行流,转而执行中断处理程序。
中断上下文通常只需要保存内核执行必要的状态,不需要像进程切换那样完整切换用户地址空间。
但中断过多同样会让 CPU 大量时间消耗在中断处理上。
因此如果:
1 | |
就应该调查中断来源,而不是只盯着用户进程。
8. 什么情况会触发调度和上下文切换
常见原因包括:
- 当前任务结束;
- 当前任务主动睡眠;
- 当前任务等待 I/O、锁、内存等资源;
- 调度器决定让另一个任务运行;
- 更高优先级任务变为可运行;
- 硬件中断;
- 多核系统中的重新调度。
需要注意一个历史化的表述:用“固定时间片轮流运行”来解释调度非常直观,但现代 Linux 常用的 CFS 调度器不能简单理解成传统固定时间片轮转。CFS 的核心是通过 vruntime 等机制维护公平性,任务实际可运行时长会动态变化。
因此在性能分析里,应该关注的是:
任务为什么频繁变成可运行、阻塞、唤醒,以及调度器为什么频繁切换执行对象。
而不是死记“一个时间片是多少毫秒”。
9. 如何查看上下文切换
9.1 vmstat:看系统整体
1 | |
重点关注:
1 | |
关键字段:
r:正在运行或等待 CPU 的任务数;b:不可中断睡眠任务数;in:每秒中断次数;cs:每秒上下文切换次数;us:用户 CPU;sy:系统 CPU;wa:iowait。
如果同时看到:
1 | |
往往意味着系统在做大量调度工作。
9.2 pidstat:看进程和线程
进程上下文切换:
1 | |
重点字段:
cswch/s:Voluntary Context Switch,自愿上下文切换;nvcswch/s:Non-voluntary Context Switch,非自愿上下文切换。
自愿上下文切换高
通常说明任务主动进入等待,例如:
- I/O;
- 锁;
- 内存或其他资源;
- sleep / condition wait。
非自愿上下文切换高
通常说明:
- CPU 竞争激烈;
- 调度器频繁强制切换任务。
如果进程级数据解释不了系统级 cs,要想到:
Linux 调度的是线程。
查看线程级上下文切换:
1 | |
这一步很关键。多线程程序主进程指标不高,不代表子线程没有产生海量调度。
10. 一个典型的上下文切换故障:线程远多于 CPU
使用 sysbench 可以模拟大量线程调度。
不同版本的 sysbench 参数存在差异,旧资料中的示例类似:
1 | |
先观察:
1 | |
如果出现:
1 | |
而机器只有 2 个 CPU,那么证据已经非常明显:
- Run Queue 远大于 CPU 数;
- 上下文切换出现数量级增长;
- 系统 CPU 极高;
- CPU 大量时间花在内核调度上。
再使用:
1 | |
可以进一步看到具体线程的切换频率。
10.1 中断也要一起检查
多核调度还可能伴随 Rescheduling Interrupt(重新调度中断)。
可以观察:
1 | |
如果 RES 等重新调度相关中断快速增长,就能和线程调度问题相互印证。
这体现了性能排查的一个重要原则:
不要只相信单个工具,用不同数据源交叉验证。
11. 每秒多少次上下文切换算异常
没有一个适用于所有机器的绝对阈值。
课程中的经验是:
- 从几百到一万以内,在不少系统上可能属于正常范围;
- 超过一万或者出现数量级增长时,应提高警惕。
但更重要的是:
1 | |
例如:
- 平时 6000/s,今天 9000/s:未必异常;
- 平时 500/s,突然 80000/s:很可疑。
12. CPU 使用率 100% 时,排查思路应该怎么走
看到 CPU 100%,第一步不是马上看源码,而是先回答:
到底是哪一种 CPU 高?
12.1 用户 CPU 高
如果:
1 | |
优先怀疑:
- 算法复杂度;
- 死循环;
- 大量计算;
- JSON / 压缩 / 加解密;
- JVM / 语言运行时热点;
- 业务函数热点。
12.2 系统 CPU 高
如果:
1 | |
优先调查:
- 系统调用频繁;
- 上下文切换;
- 锁竞争;
- 内核线程;
- 网络协议栈;
- 调度行为。
12.3 iowait 高
继续向 I/O 路径排查,不要直接宣布“磁盘坏了”。
12.4 hi / si 高
调查硬中断和软中断,例如:
1 | |
网络类问题还可能需要继续检查软中断和网卡队列。
13. 从进程进一步定位到函数:perf
当 top、pidstat 已经定位到高 CPU 进程后,下一步需要回答:
这个进程内部到底是哪个函数在消耗 CPU?
这时 perf 是非常重要的工具。
13.1 perf top
1 | |
指定进程并显示调用关系:
1 | |
重点看:
Overhead;- Shared Object;
- 用户态
[.]/ 内核态[k]; - Symbol;
- 调用链。
采样数量太少时,不要对百分比过度解读。
13.2 perf record + perf report
需要留存数据或离线分析时:
1 | |
结束采样后:
1 | |
系统级采样也可以使用:
1 | |
13.3 为什么不建议一开始就 GDB
GDB 很强,但它会主动控制甚至暂停程序。
在线上性能排查初期,更适合优先使用采样式、低侵入的工具缩小范围。
等已经定位到可疑函数,再在线下或可控环境中用调试器深入分析。
14. 高 CPU 案例:从 11 QPS 到 2200+ QPS
一个 Nginx + PHP 的案例中,压测结果只有约:
1 | |
使用 top 可以观察到:
- 两个 CPU 的用户态 CPU 都接近 100%;
- 多个
php-fpm进程持续占用 CPU。
进一步对 php-fpm 使用:
1 | |
调用链最终指向 sqrt() 等热点。
查看业务代码后发现类似:
1 | |
这段代码属于无意义的测试计算,却被带到了请求路径。
删除后再次压测,吞吐从十几 QPS 上升到两千以上。
这个案例最重要的不是“删掉 for 循环”,而是完整的定位过程:
flowchart TD
A[接口性能很差] --> B[top 看系统 CPU]
B --> C[确认 usr 接近饱和]
C --> D[定位 php-fpm]
D --> E[perf 分析热点函数]
E --> F[定位 sqrt 调用链]
F --> G[回到源码确认]
G --> H[删除无意义计算]
H --> I[重新压测验证]
这是一个非常典型的“系统指标 → 进程 → 函数 → 源码 → 验证”过程。
15. 为什么系统 CPU 很高,却找不到高 CPU 进程
这是比“某个进程直接 100%”更有迷惑性的场景。
可能看到:
1 | |
但进程列表中:
1 | |
所有进程加起来都解释不了系统的 80% 用户 CPU。
这时要警惕:
短时进程。
15.1 top 是快照工具,有盲区
如果一个进程:
1 | |
而 top 3 秒刷新一次,那么这个进程很可能在两次采样之间已经完成完整生命周期。
这时你看到的是:
1 | |
15.2 短时进程常见来源
主要有两种:
- 应用通过
exec()、Shell 等方式不断调用外部二进制; - 应用不断崩溃,并被 supervisor、systemd、容器平台等机制快速重启。
15.3 从 PID 不断变化发现线索
如果 top 偶尔看到:
1 | |
下一次却变成:
1 | |
这说明它不是一个稳定长期存在的进程。
此时可以进一步:
1 | |
找到父进程关系。
16. 专门抓短时进程:execsnoop
execsnoop 的价值在于它不是定时轮询进程表,而是监控 exec() 事件。
典型输出会包含:
1 | |
这样可以直接看到:
- 谁启动了短时进程;
- 父 PID;
- 命令行参数;
- 执行是否成功。
这类工具体现了一个重要区别:
top/ps:快照型观测;perf/execsnoop:事件或采样型观测。
当问题对象生命周期比工具刷新周期还短时,必须换成事件记录思路。
17. 短时进程高 CPU 案例:真正的问题不是“业务代码在算”
一个 PHP 应用在每次请求中执行:
1 | |
原本意图是模拟 I/O 压力,但 stress 因权限问题无法创建临时文件,快速失败退出。
结果变成:
1 | |
大量短时进程持续创建和销毁,系统 CPU 被吃掉,但普通 top 很难稳定看到某个 stress 进程占满 CPU。
这时:
1 | |
可以从采样历史中发现 stress 占用了大量 CPU 事件。
这里的核心经验是:
系统级指标和进程列表对不上时,不要立即怀疑工具,先怀疑观察模型是否遗漏了“短生命周期对象”。
18. Linux 进程状态:R、S、D、Z、T、I、X
性能排查里,进程状态往往比 %CPU 更能暴露问题。
| 状态 | 含义 | 说明 |
|---|---|---|
R |
Running / Runnable | 正在运行或等待 CPU |
S |
Interruptible Sleep | 可中断睡眠,等待事件 |
D |
Uninterruptible Sleep | 不可中断睡眠,常见于 I/O |
Z |
Zombie | 已退出,但父进程未回收 |
T/t |
Stopped / Traced | 暂停或被调试器跟踪 |
I |
Idle | 空闲内核线程 |
X |
Dead | 已消亡,通常不会长期出现在工具中 |
18.1 D 状态为什么会拉高 Load
Load Average 统计可运行任务和不可中断任务,所以大量 D 状态会直接推高 Load。
注意:
D进程会进入 Load 统计;I状态内核线程不会因为只是 idle 就推高 Load。
18.2 Z 状态为什么危险
子进程退出后,父进程需要通过:
1 | |
等方式回收退出状态。
如果父进程一直不回收,子进程就保留为 Zombie。
僵尸进程本身不继续执行用户代码,但会占用内核中的进程表相关资源和 PID。
如果数量持续增长,最终可能导致无法创建新进程。
19. D 状态 + iowait 高:如何分析
一个典型现象可能是:
1 | |
同时存在两个问题:
- iowait 很高;
- Zombie 持续增加。
不要混在一起处理,要分成两条证据链。
19.1 第一步:确认 iowait 是否和磁盘 I/O 同步
可以使用:
1 | |
或者其他能够同时观察 CPU 与 I/O 的工具。
如果看到:
1 | |
说明 iowait 与磁盘读存在明显相关性。
这仍然只是“相关”,下一步还要定位到进程。
19.2 第二步:找进程 I/O
查看所有进程 I/O:
1 | |
常见字段:
kB_rd/s:每秒读取 KB;kB_wr/s:每秒写入 KB;kB_ccwr/s:取消写相关统计;iodelay:I/O 延迟相关统计。
如果目标进程很短,指定单个 PID 可能会扑空,因为 PID 很快就消失。
因此:
1 | |
没有数据时,不要马上下结论,要先确认这个 PID 是否仍存在。
20. strace:从进程 I/O 继续追到系统调用
应用访问磁盘最终要通过系统调用进入内核。
所以定位到可疑进程后,可以继续:
1 | |
如果看到大量:
1 | |
就能更精确地判断 I/O 模式。
但如果进程已经变成 Zombie:
1 | |
那它已经退出,strace 无法再附加。
此时应该切换到能够记录历史事件的工具,例如:
1 | |
这再次说明:
实时附加工具适合长生命周期对象;事件记录工具更适合瞬时对象。
21. 直接 I/O:为什么 O_DIRECT 会让 iowait 明显升高
案例中的调用栈可以看到类似:
1 | |
继续检查源码,发现文件以:
1 | |
打开。
O_DIRECT 会绕过常规 Page Cache 路径,让应用更直接地控制 I/O。
这种方式并不是“错误设计”。
对于数据库等拥有自己的 Buffer Pool、需要精确控制缓存和持久化语义的系统,Direct I/O 很常见。
但对于普通应用,如果没有必要自己管理缓存,直接绕过 Page Cache 可能会让每次读取都更直接地命中块设备,从而显著增加存储压力和等待时间。
因此优化不能简单变成:
1 | |
而应该先问:
- 应用为什么选择 Direct I/O;
- 是否已经有用户态缓存;
- 是否对一致性和持久化有特殊要求;
- 使用 Page Cache 会不会出现双缓存;
- I/O 模式是顺序还是随机。
22. iowait 高不等于磁盘已经饱和
这是整套 CPU/I/O 分析中必须牢记的一点。
假设系统几乎没有 CPU 计算任务,唯一工作就是等一个低速 I/O:
1 | |
此时 iowait 可能很高,但设备并不一定已经达到最大吞吐。
因此完整判断应该是:
flowchart TD
A[iowait 高] --> B{磁盘吞吐/IOPS/延迟是否异常?}
B -- 否 --> C[可能只是 CPU 空闲时存在 I/O 等待]
B -- 是 --> D[继续定位设备与进程]
D --> E[pidstat / iostat / 设备指标]
E --> F[系统调用 / perf / 代码]
23. 僵尸进程如何定位和修复
既然 Zombie 是“子进程退出了,但父进程没有回收”,那么解决方向非常明确:
找到父进程。
可以使用:
1 | |
得到父子关系后,回到父进程代码检查:
- 是否调用
wait(); - 是否调用
waitpid(); - 是否正确注册
SIGCHLD; - wait 是否处在真正可执行到的位置;
- 是否存在 fork 后异常控制流。
例如一个错误结构:
1 | |
while (wait(...)) 在无限循环后面,实际上永远执行不到。
应该把子进程回收逻辑放到能够正常执行的位置,或者使用可靠的 SIGCHLD 处理策略。
23.1 不要把“kill Zombie”当成修复
Zombie 已经退出,它本身没有正常执行流可以“杀死”。
真正的问题在父进程。
如果只是粗暴杀父进程,可能暂时让 Zombie 被其他机制接管或清理,但线上生产系统更应该修复父进程的回收逻辑。
24. 一套可复用的 Linux CPU 性能排查路径
把前面的知识收拢后,可以得到一条非常实用的排查主线。
flowchart TD
A[用户反馈慢 / CPU 告警 / Load 告警] --> B[uptime / top]
B --> C{Load 是否异常?}
C --> D[看 1/5/15 分钟趋势]
B --> E[拆分 usr/sys/wa/hi/si/st]
E --> F{usr 高?}
F -- 是 --> G[pidstat/top 定位进程]
G --> H[perf 定位热点函数]
E --> I{sys 高?}
I -- 是 --> J[vmstat 看 cs/in/r]
J --> K[pidstat -w/-wt]
K --> L[/proc/interrupts 或 perf]
E --> M{wa 高?}
M -- 是 --> N[dstat/iostat 验证 I/O]
N --> O[pidstat -d 定位进程]
O --> P[strace/perf 看系统调用]
G --> Q{系统 CPU 高但进程都不高?}
Q -- 是 --> R[怀疑短时进程/频繁重启]
R --> S[pstree / execsnoop / perf record]
B --> T{D/Z 是否异常?}
T -- D 多 --> N
T -- Z 多 --> U[pstree 找父进程]
U --> V[检查 wait/waitpid/SIGCHLD]
25. 常用工具速查
25.1 系统整体
1 | |
看 Load Average。
1 | |
看:
- Load;
- Task 状态;
- CPU 类型;
- 进程排序。
top 中按 1 可以查看每个 CPU。
25.2 CPU
1 | |
观察每个 CPU 的:
- usr;
- sys;
- iowait;
- irq / softirq 等。
25.3 进程 CPU
1 | |
25.4 上下文切换
1 | |
1 | |
线程级:
1 | |
25.5 进程 I/O
1 | |
25.6 系统调用
1 | |
25.7 函数热点
1 | |
或者:
1 | |
25.8 中断
1 | |
25.9 父子进程
1 | |
25.10 短时进程
1 | |
在不同发行版上,execsnoop 的来源、安装方式以及实现技术可能不同,实际使用时应以当前系统工具链为准。
26. 工具之间不是替代关系,而是不同观察尺度
可以把工具按观察层级理解:
| 层级 | 工具 | 回答的问题 |
|---|---|---|
| 系统趋势 | uptime |
Load 是否异常,趋势如何 |
| 系统 CPU | top / mpstat |
哪类 CPU 时间高 |
| 调度 | vmstat |
Run Queue、上下文切换、中断是否异常 |
| 进程 | pidstat |
哪个进程的 CPU / I/O / 切换高 |
| 线程 | pidstat -t |
是否是线程级问题 |
| 调用栈 | perf |
CPU 时间花在哪些函数 |
| 系统调用 | strace |
进程正在怎样访问内核 |
| 进程关系 | pstree |
谁创建了谁 |
| 短时执行 | execsnoop |
哪些 exec 在快速发生 |
| 内核中断 | /proc/interrupts |
哪类中断增长 |
真正熟练之后,你不会再问:
“Linux 性能分析最强的命令是什么?”
而会问:
“我现在缺的证据在哪一层?”
27. 常见误区
27.1 Load 高 = CPU 高
错误。
Load 还包含不可中断任务。
27.2 iowait 高 = 磁盘已满负载
错误。
iowait 只是 CPU 记账维度之一,需要继续检查设备吞吐、IOPS 和延迟。
27.3 CPU 高就找 %CPU 最高的进程
大多数时候有效,但不是全部。
短时进程、频繁崩溃重启、中断和内核工作都可能让进程列表解释不了系统 CPU。
27.4 上下文切换越少越好
错误。
上下文切换是多任务系统正常工作的基础。
真正异常的是:
- 频率显著高于基线;
- 与 Run Queue 增长同时出现;
sysCPU 被调度开销吃掉;- 吞吐下降、延迟上升。
27.5 D 状态就是磁盘 I/O
不严谨。
D 是不可中断睡眠,I/O 很常见,但不是唯一原因。
27.6 Zombie 进程直接 kill 掉就行
不对。
Zombie 已经退出。需要修复父进程的回收逻辑。
27.7 一上来就 perf / GDB
工具越底层,并不代表越高效。
应该先从宏观指标缩小范围,否则很容易在海量调用栈里迷路。
28. 生产环境中需要特别注意的细节
28.1 采样窗口必须一致
对比多个工具时,尽量让采样间隔一致。
否则可能出现:
1 | |
两者并不冲突,只是观察窗口不同。
28.2 线上机器不一定允许安装工具
生产服务器可能:
- 没有 root;
- 不能联网;
- 不能安装 BCC / perf-tools;
- 只能经过堡垒机;
- 容器里没有调试工具。
因此最好同时熟练:
/proc;top;ps;vmstat;pidstat;strace;
这些更基础的手段。
高级追踪工具是加速器,不应该成为唯一依赖。
28.3 容器环境里的 perf 符号问题
分析容器进程时,宿主机上的 perf 可能找不到容器内部的动态库和符号,于是调用栈只显示十六进制地址。
这类问题的本质是:
采样发生在宿主机,但符号和依赖库位于容器文件系统。
常见思路包括:
- 保存
perf.data后,在能够访问正确符号路径的环境中解析; - 为
perf report提供正确的符号文件系统路径; - 在受控环境内完成符号化。
不要因为“只看到地址”就误判为 perf 没采到数据。
28.4 教学案例的 Ubuntu 18.04 已是历史环境
资料中的命令主要基于 Ubuntu 18.04、当时版本的 sysstat、sysbench、stress 等工具。
放到新的发行版或容器环境时,可能遇到:
- 包名变化;
- 参数变化;
- 内核调度行为差异;
pidstat字段差异;sysbenchCLI 变化;- 容器权限限制;
- 块设备命名变化。
所以实际执行时,man、--help 和当前发行版官方文档应该作为最终依据。
29. 性能排查的几个工程化原则
29.1 先现象,后源码
不要因为“我知道这段代码看起来很可疑”就跳过系统观测。
正确流程是:
1 | |
这样得到的是可验证的结论,而不是靠经验猜。
29.2 一次只缩小一层范围
例如:
1 | |
不要直接跑几十个命令。
应该先确定:
1 | |
然后再选下一层工具。
29.3 指标之间必须互相解释
好的结论应该能解释多个现象。
例如“线程过多导致调度压力”应该同时解释:
r高;cs高;nvcswch高;sys高;RES中断增长;- 延迟上升。
如果一个解释只能解释某一个指标,却和其他指标矛盾,就需要继续调查。
29.4 优化后一定重新测
性能优化没有“看起来应该变快”。
必须重新测:
- 吞吐量;
- 延迟;
- CPU;
- Load;
- I/O;
- 资源饱和度。
只有指标证明发生改善,优化才算完成。
30. 一份适合线上故障的快速检查清单
遇到 Linux 服务“突然变慢”,可以按下面顺序快速扫一遍。
30.1 看总体
1 | |
回答:
- Load 是否异常?
- 1/5/15 分钟趋势如何?
- CPU 是
us、sy、wa、hi、si还是st高? - R、D、Z 任务是否异常?
30.2 看每个 CPU
1 | |
回答:
- 是所有 CPU 都忙?
- 还是单核热点?
30.3 看调度
1 | |
重点看:
1 | |
30.4 看进程
1 | |
30.5 进程解释不了系统指标
考虑:
- 线程;
- 短时进程;
- 中断;
- 内核线程;
- 频繁重启。
继续:
1 | |
30.6 已经定位进程
1 | |
或者:
1 | |
涉及 I/O 或系统调用:
1 | |
31. 最终应该建立的思维模型
Linux 性能分析不是“背命令”,而是建立下面这套映射:
1 | |
当你看到:
1 | |
应该自动想到:
1 | |
看到:
1 | |
应该想到:
1 | |
看到:
1 | |
应该想到:
1 | |
看到:
1 | |
应该想到:
1 | |
看到:
1 | |
应该想到:
1 | |
真正的 Linux 性能能力,就是把这些问题变成条件反射。
总结
CPU 性能分析最容易掉进两个坑:一个是把所有问题都归结成“CPU 不够”,另一个是把工具输出当答案。
更可靠的方法是建立一条层层收敛的证据链:
1 | |
其中最值得长期记住的几个结论是:
- Load Average 不是 CPU 使用率,而是活跃任务与不可中断任务的综合信号;
- CPU 100% 之前先看清楚到底是哪一种 CPU 时间高;
- 上下文切换是必要机制,但数量级增长可能意味着严重的调度浪费;
- 系统 CPU 很高却找不到高 CPU 进程时,要想到短时进程和线程;
- D 状态常与 I/O 有关,但不能简单等价;
- iowait 高不等于磁盘已经达到性能上限;
- Zombie 的根因在父进程的资源回收逻辑;
- 性能工具只是观察手段,真正重要的是知道下一条证据应该去哪里找。
当这些概念串成一个整体后,top 就不再只是一个“看 CPU 百分比”的命令,vmstat 也不再是一堆难记的列,perf 更不是遇到问题就随手跑一下的神器。它们会成为同一套性能分析方法在不同层级上的观察窗口。