Linux CPU 性能分析与优化:从软中断到 perf 的系统化排障方法
Linux CPU 性能分析与优化:从软中断到 perf 的系统化排障方法
Linux CPU 性能问题最容易掉进两个坑:一个是看到 CPU 使用率高就开始猜代码,另一个是把所有性能工具都跑一遍,希望某个数字自己跳出来告诉你答案。
真正可复用的方法不是记住多少条命令,而是把 指标、系统原理、工具和根因 连成一条证据链:
先判断“哪类资源或 CPU 时间出了问题”,再找到“哪个进程、线程或内核路径在制造问题”,最后才进入函数、系统调用、网络包或硬件设备层面。
这套方法尤其适合软件开发者。因为线上很多“应用变慢”“接口超时”“CPU 飙升”“机器卡顿”,最终并不一定是业务代码本身的问题,也可能来自调度、I/O、软中断、网络小包、短时进程、容器符号缺失或 JVM JIT 等系统层因素。
版本说明:本文中的实验环境和命令主要来自 Ubuntu 18.04、CentOS 7 时代的 Linux 系统。核心分析思路仍然成立,但
sysstat、perf、JDK、容器权限模型和命令参数可能随版本变化。遇到输出与本文不一致时,优先执行man <command>核对当前版本的真实语义。
一、CPU 性能问题不是一个“CPU 使用率”数字
CPU 性能分析的第一步,是把“CPU 忙”拆成不同性质的忙。
Linux 中常见的 CPU 指标大致可以分成四组:
- CPU 时间花在哪里;
- 有多少任务在争抢 CPU;
- 调度和上下文切换是否异常;
- CPU 缓存和硬件执行效率是否异常。
1. CPU 使用率的组成
CPU 使用率描述的是非空闲时间占总 CPU 时间的比例,但不同 CPU 时间代表的根因完全不同。
| 指标 | 常见名称 | 含义 | 常见排查方向 |
|---|---|---|---|
| 用户 CPU | us / user |
CPU 在用户态执行普通进程的时间 | 应用计算、算法、循环、业务代码、JIT 代码 |
| nice CPU | ni / nice |
CPU 执行低优先级用户进程的时间 | 低优先级后台任务 |
| 系统 CPU | sy / system |
CPU 在内核态执行的时间,不含中断 | 系统调用、内核路径、锁、内存、I/O 等 |
| I/O Wait | wa / iowait |
CPU 处于空闲且系统存在未完成 I/O 时记入的等待时间 | 磁盘、块设备、直接 I/O 等 |
| 硬中断 | hi / irq |
CPU 执行硬中断处理程序的时间 | 网卡、磁盘、设备中断 |
| 软中断 | si / softirq |
CPU 执行软中断的时间 | 网络收发、定时器、调度、RCU 等 |
| steal | st |
虚拟机等待宿主机调度 CPU 的时间 | 虚拟化资源争抢 |
| guest | guest | CPU 用于运行客户虚拟机的时间 | 虚拟化负载 |
最关键的一点是:不要把“CPU 高”当成一个统一问题。
us 高和 si 高,即使最终都表现为 CPU 使用率上升,排查路径也完全不同。
2. Load Average 不是 CPU 使用率
Load Average 描述的是系统中的平均活跃任务数,通常显示最近:
- 1 分钟;
- 5 分钟;
- 15 分钟。
Linux 的负载不仅包含正在运行和等待 CPU 的任务,还包含部分不可中断睡眠任务。因此,负载高不一定意味着 CPU 已经 100%。
在理想化理解中,如果负载长期接近逻辑 CPU 数量,说明 CPU 资源得到了较充分利用;如果长期明显超过逻辑 CPU 数量,就需要继续区分:
- 是大量任务在 Run Queue 中等 CPU;
- 还是大量任务处于不可中断状态等待 I/O。
这正是 vmstat 中 r 和 b 两列的价值。
3. 上下文切换
Linux 的调度能力依赖上下文切换,但切换本身不是免费的。
进程或线程被切走时,CPU 需要保存和恢复寄存器、内核栈、调度状态等信息;进程切换还可能带来地址空间和缓存局部性方面的额外成本。
常见上下文切换分为:
- Voluntary Context Switch(自愿上下文切换):任务因为等待资源主动让出 CPU;
- Nonvoluntary Context Switch(非自愿上下文切换):任务被调度器强制切走,例如时间片耗尽或有更高优先级任务运行。
如果非自愿上下文切换大量增加,通常意味着过多线程或进程正在争抢 CPU。
4. CPU Cache 命中率
CPU 和内存之间存在巨大的速度差,因此现代 CPU 依赖多级缓存降低访存延迟。
通常可以把缓存理解为:
- L1:最小、最快,通常与核心强绑定;
- L2:容量更大,速度稍慢;
- L3:更大,常被多个核心共享。
CPU Cache 命中率越高,说明热点数据复用越好。CPU 亲和性、NUMA、本地性优化,本质上都与减少跨核心、跨节点的数据访问有关。
二、从中断理解 Linux 的异步处理机制
1. 为什么需要中断
硬件速度远低于 CPU。如果 CPU 每次都主动轮询网卡、磁盘或其他设备:
“数据到了吗?数据到了吗?数据到了吗?”
CPU 大量时间会浪费在无效检查上。
中断提供了一种异步事件机制:CPU 正常做其他工作,硬件准备好后主动通知 CPU。
可以把它理解为:
- 轮询:一直站在门口等外卖;
- 中断:外卖到了打电话,你再去处理。
问题在于,中断会打断 CPU 当前正在执行的任务。如果一次中断处理耗时太长,就会影响正常任务甚至其他中断的及时处理。
因此 Linux 把中断处理拆成两个阶段。
2. 上半部和下半部
flowchart LR
A[硬件事件发生] --> B[产生硬件中断]
B --> C[上半部 / Hard IRQ]
C --> D[快速完成硬件相关工作]
D --> E[触发后续处理]
E --> F[下半部 / SoftIRQ 等机制]
F --> G[执行较重的协议、调度或延迟工作]
G --> H[最终交给进程/应用]
**上半部(Top Half)**的目标是快:
- 响应设备;
- 读取或确认必要的硬件状态;
- 完成时间敏感操作;
- 尽快退出中断上下文。
**下半部(Bottom Half)**则处理可以延后的工作。
以网卡收包为例,可以粗略理解为:
- 网卡收到数据包;
- 产生硬件中断;
- 上半部快速确认设备状态,并把后续工作交给下半部;
- 下半部继续处理网络协议栈;
- 最终把数据交给 socket 和应用程序。
这里需要补一个非常重要的细节:不能简单认为“所有软中断都是由 ksoftirqd 线程执行”。
软中断可以直接在相关内核执行路径中被处理;当软中断过多、超过处理预算或需要把工作延后时,才会更多地由每个 CPU 对应的 ksoftirqd/N 内核线程接管。
因此,看到 ksoftirqd 占用 CPU 很高,通常说明软中断已经积压到需要内核线程持续处理。
3. ksoftirqd 是什么
Linux 每个逻辑 CPU 都有对应的软中断内核线程,例如:
1 | |
可以用:
1 | |
查看。
进程名外面的 [] 是一个很有用的识别信号:这通常表示它是内核线程,而不是普通用户态程序。
三、Linux 软中断有哪些类型
系统中的软中断累计次数可以从:
1 | |
查看。
典型输出会包含:
1 | |
常见类别可以这样理解:
| 类型 | 典型含义 |
|---|---|
HI |
高优先级 tasklet 类软中断 |
TIMER |
普通定时器 |
NET_TX |
网络发送 |
NET_RX |
网络接收 |
BLOCK |
块设备相关 |
IRQ_POLL |
IRQ Poll 相关处理 |
TASKLET |
tasklet 处理 |
SCHED |
调度相关软中断 |
HRTIMER |
高精度定时器 |
RCU |
Read-Copy Update 相关处理 |
分析 /proc/softirqs 时不要只看“哪个数最大”,而要看两个维度:
1. 看增长速度,而不是累计值
/proc/softirqs 显示的是自系统启动以来的累计次数。
因此,单独执行一次:
1 | |
信息量有限。
更有效的方式是观察变化速率:
1 | |
-d 会高亮发生变化的部分。这样可以迅速看到哪类软中断正在高速增长。
2. 看 CPU 分布是否失衡
同一种软中断如果长期集中在少数 CPU 上,可能带来负载不均衡。
对于网络中断,常见的系统级优化手段包括:
irqbalance;smp_affinity;- 合理配置网卡多队列和 CPU 亲和性。
但不要一看到分布不均就立刻“调均匀”。中断亲和性与应用线程亲和性、NUMA、网卡队列设计是联动的,必须结合实际负载测试。
3. 硬中断在哪里看
硬中断对应:
1 | |
因此可以记成:
1 | |
四、最典型的软中断问题:网络小包
软中断类型很多,但生产环境中最常见的一类性能问题是网络软中断,尤其是 NET_RX。
这里有一个非常重要的工程经验:
网络性能不能只看带宽 BPS,还要看包速率 PPS。
即使每秒只有几百 KB,如果都是几十字节的小包,CPU 仍然可能因为需要处理大量网络帧而被软中断拖垮。
1. 实验环境
案例使用两台虚拟机隔离服务端和压测端:
flowchart LR
C[VM2: Client<br/>192.168.0.2<br/>hping3] -->|TCP SYN| S[VM1: Server<br/>192.168.0.30<br/>Nginx]
服务端启动 Nginx:
1 | |
验证服务:
1 | |
在隔离的测试环境中,可以使用 hping3 构造高频 SYN 流量:
1 | |
这类命令只应在你有权限的实验环境中使用,不应对第三方系统执行。
2. 第一步:top 看整体资源
案例中,top 显示 CPU 并不高:
1 | |
但两个 CPU 的非空闲时间几乎都消耗在 si 上,而且进程列表里能看到 ksoftirqd。
这时正确的结论不是“CPU 才 4%,没问题”,而是:
系统当前有效工作很少,但软中断占了异常突出的一部分,需要继续确认软中断类型。
3. 第二步:/proc/softirqs 找软中断类型
1 | |
案例中 TIMER、SCHED、RCU 都会变化,但 NET_RX 增长最快。
这就把问题从“CPU 异常”缩小成了:
网络接收软中断异常。
4. 第三步:sar 同时看 PPS 和 BPS
1 | |
典型输出:
1 | |
字段含义:
rxpck/s:每秒接收包数,PPS;txpck/s:每秒发送包数,PPS;rxkB/s:每秒接收 KB 数,BPS 的一种表达;txkB/s:每秒发送 KB 数。
案例里 eth0 每秒收到约 12607 个包,但只有 664.86 KB/s。
平均包大小约为:
$$
\frac{664 \times 1024}{12607} \approx 54\ \text{Bytes}
$$
也就是说:
带宽不高,但 PPS 极高,而且包非常小。
这就是典型的 Small Packet(网络小包) 问题。
5. 第四步:tcpdump 确认包类型和来源
1 | |
输出类似:
1 | |
可以得到两个关键信息:
- 来源:
192.168.0.2; Flags [S]:TCP SYN 包。
结合前面的高 PPS,就可以形成完整证据链:
1 | |
6. 为什么 CPU 只有 4%,SSH 还是很卡
这个案例非常容易被误解。
终端卡顿不是因为“4% 的软中断就把 CPU 打满了”,而是因为登录本身使用 SSH,SSH 也依赖网络。
案例中,在异常流量出现前,网络 RTT 基本不到 1 ms:
1 | |
异常流量出现后:
1 | |
所以“终端敲回车很慢”的直接原因是网络延迟和丢包,而不是 CPU 整体已经满载。
这件事非常值得记住:
用户看到的“系统卡”只是一种现象,真正瓶颈可能来自 CPU、I/O、网络、调度,甚至远程访问链路。
7. 应用程序也能间接制造软中断问题
软中断运行在内核中,不代表它与应用无关。
例如,同样发送 1 GB 数据:
- 每次发送
1 KB; - 每次发送
32 B;
后者会制造更多网络包、更高 PPS、更频繁的协议栈处理和中断压力。
所以软中断高时,不应停在“这是内核问题”这一步,还要继续问:
- 谁制造了这些网络包?
- 是否存在过小的发送粒度?
- 是否存在不必要的轮询、短睡眠或高频系统调用?
- 是否存在异常客户端或攻击流量?
五、从指标出发选择工具,而不是背工具
Linux 性能工具很多,最有效的记忆方式不是按命令背,而是按“我现在缺哪类证据”选择工具。
1. 指标到工具
| 想看的指标 | 常用工具 | 主要价值 |
|---|---|---|
| 平均负载 | uptime、top |
快速判断整体负载 |
| 系统 CPU | top、vmstat、mpstat、sar、/proc/stat |
区分 user/system/iowait/irq/softirq |
| 进程 CPU | top、pidstat、ps、htop、atop |
找高 CPU 进程 |
| 每 CPU 使用率 | mpstat |
观察单核失衡 |
| 系统上下文切换 | vmstat |
看 cs、r、b、in |
| 进程/线程上下文切换 | pidstat -w |
区分自愿和非自愿切换 |
| 软中断 | top、/proc/softirqs、mpstat |
看 si 和软中断类型 |
| 硬中断 | vmstat、/proc/interrupts |
看中断总量和分布 |
| 网络 | sar -n、tcpdump、dstat |
PPS/BPS、抓包、协议和来源 |
| I/O | sar -d、dstat |
设备吞吐和等待 |
| 系统调用 | strace |
看进程进入内核做了什么 |
| CPU 事件/调用链 | perf |
函数热点、调用栈、缓存等 |
| 短时进程 | execsnoop |
捕获生命周期很短、top 不易看到的进程 |
2. 工具到指标
反过来也需要知道每个核心工具擅长什么。
top
适合第一眼看全局:
- Load Average;
- 各类 CPU 时间;
- 僵尸进程;
- 高 CPU 进程;
- 进程状态。
执行后按 1 可以展开每个逻辑 CPU。
vmstat
适合看系统调度和资源整体状态:
r:运行/等待 CPU 的任务;b:不可中断睡眠任务;in:中断次数;cs:上下文切换次数;- CPU 各类时间;
- 内存和 swap 基本信息。
例如:
1 | |
pidstat
用于把系统级现象落到进程和线程:
1 | |
它能观察:
- 进程/线程用户 CPU;
- 系统 CPU;
- CPU 等待;
- 自愿上下文切换;
- 非自愿上下文切换。
perf
当你已经找到“可疑进程或可疑执行路径”,再用 perf 下钻到函数和调用链,通常更有效。
常用的核心组合是:
1 | |
不要把 perf 当成“CPU 问题一键诊断器”。它提供的是采样证据,仍然需要结合前面的系统指标理解。
六、一套“又快又准”的 CPU 排障决策树
实际生产环境中,没有必要把所有工具全部跑一遍。
更合理的方法是先用覆盖面大的工具缩小范围,再按指标分支下钻。
flowchart TD
A[系统变慢 / CPU异常 / Load升高] --> B[top]
B --> C{哪个指标异常?}
C -->|us 高| D[pidstat 找高 CPU 进程/线程]
D --> E[perf 看函数热点]
C -->|sy 高| F[vmstat 看 cs/in/r/b]
F --> G[pidstat -w / -u -t]
G --> H[strace / perf]
C -->|wa 高| I[vmstat 看 b]
I --> J[sar -d / dstat]
J --> K[定位磁盘与进程]
C -->|si 高| L[/proc/softirqs]
L --> M{软中断类型}
M -->|NET_RX/NET_TX| N[sar -n DEV]
N --> O[tcpdump]
M -->|SCHED| P[检查上下文切换与线程争抢]
C -->|hi 高| Q[/proc/interrupts]
Q --> R[检查设备与 IRQ 分布]
C -->|Load 高| S[vmstat]
S --> T{r 还是 b 高?}
T -->|r 高| D
T -->|b 高| I
B --> U{看不到高 CPU 进程?}
U -->|存在短时进程| V[execsnoop / perf record]
这张图背后的核心不是命令,而是关联关系。
1. us 高:优先看用户态
如果 top 显示用户 CPU 高,就应该优先找哪个用户进程消耗 CPU:
1 | |
找到目标进程后,再用:
1 | |
看函数热点。
2. sy 高:优先看系统调用、调度和内核路径
系统 CPU 高时,可以结合:
1 | |
判断是否是:
- 上下文切换过多;
- 高系统调用频率;
- 锁竞争;
- 短时进程;
- 内核函数热点。
需要看系统调用时使用 strace,需要看用户态和内核态完整热点时使用 perf。
3. wa 高:进入 I/O 路径
如果同时看到大量不可中断进程,优先使用 I/O 工具定位设备和相关进程。
但要记住:iowait 不是“某个进程等待 I/O 的比例”,它是一个 CPU 时间指标。
4. si 高:按软中断类型继续分支
NET_RX、NET_TX 高就继续看网络;SCHED 异常就结合上下文切换和运行队列分析。
5. Load 高:先区分 r 和 b
Load Average 只是现象。
vmstat 可以帮助你判断:
r高:大量任务在运行或等 CPU;b高:大量任务处于不可中断状态,继续排查 I/O。
七、几个非常容易误解的指标
1. pidstat %wait 不等于 top wa
这是 CPU 性能分析里非常典型的误区。
pidstat 中:
1 | |
它表示任务已经就绪,但还没拿到 CPU。
而 top 中:
1 | |
两者完全不是一个概念。
可以记成:
1 | |
2. 等待 CPU 和等待 I/O 的任务状态不同
- 等待 CPU:已经在 Run Queue 中,本质上属于可运行任务;
- 等待部分 I/O:可能处于不可中断睡眠状态(D state)。
不要把“可中断/不可中断睡眠”“硬中断/软中断”“Unix Signal”混成一组概念。
3. Ctrl+C、Ctrl+Z 是 Signal,不是硬中断/软中断
它们属于进程间通信和进程控制中的信号机制:
Ctrl+C通常触发SIGINT;Ctrl+Z通常触发SIGTSTP。
这和内核处理网卡、磁盘设备的硬中断不是一回事。
4. vmstat 第一行为什么经常特别奇怪
执行:
1 | |
时,第一行并不代表“最近 5 秒”。
它的 CPU、I/O 等部分通常是 系统启动以来的平均值;后续行才是每个采样间隔内的平均值。
因此:
分析瞬时问题时,不要拿
vmstat第一行和后续行直接比较。
进程和内存相关部分则更接近即时状态。
5. 工具输出不符合常识,第一反应应该是 man
不同发行版、不同工具版本,字段和参数可能不同。
性能分析时一个很重要的习惯是:
1 | |
如果一个指标解释不通,不要先怀疑内核“坏了”,先确认工具本身到底在输出什么。
八、实验为什么经常复现不出来
性能实验高度依赖:
- CPU 核数;
- 内存;
- HDD / SATA SSD / NVMe;
- 内核版本;
- 虚拟机与宿主机调度;
- 工具版本;
- 容器运行模式。
同一条压力命令,在不同机器上可能出现完全不同的结果。
1. stress -i 不一定能制造高 iowait
早期常见的写法:
1 | |
其中 -i 主要通过 sync() 制造系统调用和刷盘行为。
问题是:如果页面缓存里本来就没有多少脏数据,sync() 并不会产生足够大的磁盘压力。
在 SSD 环境下尤其明显,可能看到:
iowait几乎为 0;sys反而升高。
更可靠的实验方式是使用 stress-ng 同时制造实际文件读写:
1 | |
2. 磁盘性能差异会彻底改变现象
同一个 I/O 压力:
- 在机械盘上可能直接把磁盘利用率压到 100%;
- 在低端 SSD 上刚好形成明显 iowait;
- 在高性能 NVMe 上可能几乎看不到瓶颈。
因此,“我复现不出来”并不代表分析方法错误。
3. 不要假设磁盘一定是 /dev/sdX
现代云主机和 NVMe 设备常见:
1 | |
如果测试程序只匹配 /dev/sd 或 /dev/xvd,就可能找不到真实块设备。
示例测试程序后来增加了类似参数:
-d:指定读取磁盘;-s:每次读取的数据量,默认67108864字节,即64 MB;-c:子进程读取次数,默认20。
示例:
1 | |
4. 单核机器无法复现 RES 重调度中断
RES 是 Rescheduling Interrupt(重调度中断),用于处理器之间通知和调度,通常依赖 IPI(Inter-Processor Interrupt,处理器间中断)。
如果系统只有一个逻辑 CPU,就不存在把任务重新调度到另一个 CPU 的意义,因此很难看到 RES 增长。
但这不代表“没有调度问题”。
大量线程仍然会在单核 CPU 上争抢时间片,你会看到:
cs大幅增加;- 非自愿上下文切换上升;
pidstat的%wait很高;- 每个线程真正执行 CPU 的比例反而很低。
例如:
1 | |
可以直接观察线程等待 CPU 的情况。
九、perf:从“哪个进程”下钻到“哪个函数”
top、vmstat、pidstat 负责告诉你哪里可疑;perf 的价值是继续回答:
CPU 时间到底消耗在哪条调用链、哪个函数、哪个内核路径上?
1. 最重要的两个命令
对于日常 CPU 性能定位,先掌握:
1 | |
-g 用于采集调用栈。
也可以先用实时模式:
1 | |
但线上复盘时,record + report 更便于保存证据。
2. 为什么不能“一上来就 perf”
因为高采样比例不等于性能瓶颈。
一个典型例子是 swapper。
很多人看到 perf report 中:
1 | |
第一反应是“这就是瓶颈”。
实际上,swapper 与 swap 分区没有关系。它本质上是 CPU 空闲时运行的 idle task。
如果调用链主要落在:
1 | |
那 swapper 高反而可能说明 CPU 大部分时间没事做。
所以:
事件占比大,只能说明采样多,不能单独证明它是问题。
要把 perf 结果和 top、vmstat、pidstat 等系统指标交叉验证。
3. Children 和 Self
perf report 常见两个关键列:
- Self:当前符号本身消耗的比例;
- Children:该符号下面所有直接和间接子调用消耗的累计比例。
因此:
Self高:函数本身很重;Children高但Self低:真正成本主要在它调用的其他函数中。
4. 为什么调用栈不显示
perf report 的调用图默认存在最小显示阈值。
资料中的示例里,默认阈值是 0.5%。如果一个事件只有 0.34%,调用栈就可能默认不展开。
可以降低阈值:
1 | |
这时低于默认阈值但高于 0.3% 的调用链就会出现。
5. 为什么只看到 16 进制地址
如果输出是:
1 | |
而不是函数名,通常不是 perf “不会分析”,而是 符号解析失败。
常见原因:
- 依赖库不在宿主机文件系统中;
- 容器里的库路径宿主机找不到;
- 二进制经过
strip,符号表被删除; - 缺少 debug symbols;
- 内核符号访问权限受限。
如果 perf 底部出现类似:
1 | |
就应该沿着符号路径问题排查。
对于需要长期性能诊断的服务,构建产物是否保留足够的符号信息,是工程可观测性的一部分。
十、容器里做 perf,难点通常不是采样,而是符号
容器应用依赖的动态库位于镜像文件系统中,而 perf 往往运行在宿主机。
因此会出现:
宿主机可以采到地址,但无法用容器内的库文件把地址翻译成函数名。
方案一:在宿主机复制相同依赖路径
理论上可行,但容易污染宿主机,不推荐。
方案二:直接在容器内运行 perf
这通常需要较高权限,普通容器可能报:
1 | |
可以通过特权容器或调整 perf_event_paranoid 放开权限,但这会影响安全边界,不适合把“为了分析方便”直接变成生产默认配置。
方案三:把容器根文件系统作为 symfs
可以先拿到容器 PID:
1 | |
然后将容器根文件系统绑定到一个目录:
1 | |
使用完成后:
1 | |
这里的核心不是 bindfs 本身,而是告诉 perf:
“解析这些地址时,请去容器真正的文件系统里找库和符号。”
方案四:宿主机采样,容器内解析
先在宿主机:
1 | |
采样完成后:
1 | |
容器内安装匹配的 perf,再:
1 | |
需要特别注意 perf 版本与宿主机内核匹配问题。某些发行版中的 perf 命令本质上会按内核版本选择对应工具,容器用户态和宿主机内核版本不一致时更容易出现兼容问题。
十一、Java 为什么比普通 ELF 程序更难用 perf 看懂
Java 服务多了一层 JVM 和 JIT。
从操作系统看,运行的是 JVM 进程。Java 热点代码经过 JIT 编译后,会动态生成机器码;这些代码并不像普通 ELF 文件那样天然带着静态符号路径。
因此你可能看到:
- JVM 内部函数;
- 地址;
- 内核函数;
- 但看不到熟悉的 Java 方法名。
资料给出的 JIT 符号方案
perf_events 可以结合 JIT 符号映射,但需要类似:
1 | |
这样的符号映射文件。
perf-map-agent 可以用于生成映射。
为了得到更完整的栈,资料中的 JDK 方案还要求开启:
1 | |
形成的思路是:
flowchart LR
A[Java 进程] --> B[JIT 动态机器码]
B --> C[生成 perf-PID.map]
C --> D[perf 采样]
D --> E[地址 -> Java/JIT 符号]
E --> F[可读调用栈]
这里需要把方法论和具体工具分开:
Java 性能问题仍然先从系统指标判断瓶颈属于 CPU、I/O、锁、调度还是网络,再选择 JVM 层工具继续下钻。
不要因为服务是 Java,就一开始只盯着 jstack;也不要因为会 perf,就忽略 JVM 自己的运行时信息。
perf-map-agent + PreserveFramePointer 是资料所处时期的一种方案。现代 JDK 和生产环境中,通常还会结合 JFR、async-profiler 等 JVM 侧工具;具体选择应以当前 JDK 版本和线上约束为准。
十二、性能工具本身也有开销
性能分析不是完全无侵入的。
perf
perf 需要在内核中采样事件并记录调用栈,因此会引入一定开销。
大多数常规服务可以接受短时间采样,但对以下场景要更加谨慎:
- 极低延迟系统;
- 高频交易类工作负载;
- 对时钟周期特别敏感的应用;
- 本身已经接近资源极限的系统。
vmstat / pidstat 等
这类工具主要读取 /proc、/sys 等内核统计数据,通常开销很低,更适合作为第一层观察工具。
这也是为什么性能分析更推荐:
先用低开销的宽视角工具筛选,再用动态追踪工具定点下钻。
十三、找到瓶颈以后,不要立刻改代码
性能优化最危险的阶段,往往不是“找不到瓶颈”,而是“刚看到一个可疑点就开始优化”。
动手前应该回答三个问题。
1. 优化效果如何量化
性能优化应该做前后对比,而不是凭感觉说“快了”。
可以用一个三步法:
1 | |
指标至少应该覆盖两个维度。
应用维度
例如 Web 服务:
- 吞吐量;
- QPS;
- 请求延迟;
- P95 / P99;
- 错误率。
系统资源维度
例如:
- CPU user/system/softirq;
- Load Average;
- 上下文切换;
- I/O;
- 网络 PPS/BPS。
只看 CPU 从 90% 降到 40%,不能证明业务性能一定更好;只看 QPS 升高,也无法解释成本来自哪里。
2. 多个性能问题同时存在,先优化谁
一个系统可能同时存在:
- system CPU 高;
- 上下文切换高;
- iowait 高;
- 网络软中断高;
- 应用延迟高。
它们可能不是五个独立问题,而是同一根因的连锁反应。
例如:
1 | |
所以优化前先梳理因果关系,优先处理最上游、影响最大的瓶颈。
一个实用原则是:
- 如果某个系统资源已经 100% 饱和,优先解决资源瓶颈;
- 如果多个指标都变化,优先分析变化幅度最大且与瓶颈有直接因果关系的指标;
- 不要把每一个“异常数字”都当成独立问题。
3. 多种方案都能优化,选哪个
性能提升不是唯一指标,还要考虑:
- 实现复杂度;
- 可维护性;
- 稳定性;
- 资源成本;
- 可移植性;
- 对其他指标的副作用。
典型例子是 DPDK。
它可以绕过传统内核网络协议栈以提高包处理能力,但通常需要:
- 独占 CPU;
- 大页内存;
- 持续高 CPU 占用;
- 更复杂的运行和维护方式。
在 CPU 核心很少的机器上,这种优化就可能得不偿失。
因此性能优化本质上是 Trade-off(权衡),而不是“把一个指标做到极致”。
十四、性能测试本身必须可信
1. 压测端和服务端尽量分离
如果压测工具和目标服务跑在同一台机器上:
- 压测工具自己也会占 CPU;
- 会消耗网络和内存;
- 可能影响缓存;
- 最终测到的是“服务 + 压测器”的混合结果。
因此 Web 服务性能测试通常至少分两台机器:
1 | |
2. 优化前后环境必须一致
对比实验要保证:
- CPU/内存规格相同;
- 内核和关键配置一致;
- 外部依赖一致;
- 测试参数一致;
- 测试时长和预热方式一致。
否则“优化后更快”可能只是环境变化。
3. 性能结果要看绝对值,也要看趋势
例如某个指标从 1s 降到 1ms,比例上是 1000 倍,但如果它一天只执行一次,对系统整体价值可能很低。
这也是所谓“二八原则”的工程意义:
把精力集中在真正决定系统整体性能的少数热点上。
十五、应用程序层的 CPU 优化方法
1. 删除不必要的工作
最有效的优化往往不是“让 CPU 执行更快”,而是“根本不要执行”。
典型方向:
- 减少无意义循环;
- 减少递归层数;
- 减少重复计算;
- 减少频繁动态内存分配;
- 减少过细粒度日志;
- 合并高频小 I/O 或小网络包。
2. 编译器优化
例如 GCC:
1 | |
编译器可以进行:
- 常量传播;
- 死代码删除;
- 指令优化;
- 循环优化;
- 内联等。
但编译器优化仍然不能替代算法和架构层面的优化。
3. 算法优化
算法复杂度的影响通常远大于指令级微优化。
例如数据规模较大时:
1 | |
往往比微调几个 CPU 指令收益大得多。
4. 异步处理
如果程序大量时间花在“等资源”,可以考虑:
- 事件通知;
- 异步 I/O;
- 任务队列;
- 批处理。
一个很典型的原则是:
能被事件通知唤醒,就不要高频轮询。
这与硬件中断诞生的思想其实是同一类设计:避免 CPU 无意义忙等。
5. 多线程替代多进程
在共享大量内存和状态的场景中,多线程通常比多进程切换成本低,因为线程切换不需要切换完整进程地址空间。
但这不是“线程永远优于进程”。多进程在隔离性、故障边界、安全和架构扩展上也有优势。
因此选择取决于业务需求,而不是只看上下文切换成本。
6. 善用缓存
缓存本质上是用空间换时间。
适合缓存:
- 热点数据;
- 重复计算结果;
- 高成本查询结果;
- 稳定且复用率高的数据。
但缓存会引入一致性、失效和内存占用问题,所以同样需要用性能数据证明收益。
十六、系统层的 CPU 优化方法
1. CPU Affinity:把任务绑定到 CPU
CPU 绑定可以减少任务在不同核心之间迁移,提高缓存局部性。
适合:
- 延迟敏感服务;
- 高频线程;
- 网络处理线程;
- NUMA 场景。
但错误绑定也可能把负载全部压在少数核上。
2. CPU 独占
进一步可以把部分 CPU 核只留给关键服务,减少其他进程干扰。
这类方案适合对抖动和延迟特别敏感的服务,但会牺牲系统整体资源利用率。
3. 调整进程优先级
可以通过 nice / renice 调整普通调度优先级。
原则不是“核心服务全部拉最高”,而是:
- 降低非核心后台任务的优先级;
- 避免后台任务挤占关键服务。
4. 使用 cgroups 限制 CPU
cgroups 可以限制单个服务的 CPU 使用范围或份额,避免一个异常进程耗尽整机资源。
在容器和 Kubernetes 环境中,这实际上已经成为资源治理的基础能力。
5. NUMA 优化
NUMA(Non-Uniform Memory Access)系统中,每个 node 有更靠近自己的本地内存。
目标是尽量让:
1 | |
而不是频繁访问远端 NUMA node。
因此 CPU 绑核和内存绑定要一起考虑,不能只做其中一半。
6. 中断负载均衡
可以使用:
1 | |
或者配置:
1 | |
让硬中断在多个 CPU 之间更合理分布。
但对高性能网络应用来说,完全自动均衡也未必是最优;有时需要把网卡队列、中断 CPU 和应用工作线程进行整体亲和性设计。
十七、不要过早优化
“过早优化是万恶之源”不是说性能不重要,而是说:
没有证据的优化,很容易把简单系统变成复杂系统,却没有得到可验证的收益。
过早优化的典型副作用包括:
- 代码复杂度上升;
- 可维护性下降;
- 调试困难;
- 新需求难以适配;
- 一个指标变好,另一个指标变差;
- 系统资源被提前固化。
比较稳妥的策略是:
1 | |
性能优化应该是迭代过程,而不是一次性“把所有参数调到极致”。
十八、一份适合线上排障的 CPU Checklist
遇到“机器慢了、CPU 异常、接口延迟上升”时,可以按下面顺序快速走一遍。
第一层:全局判断
1 | |
确认:
- Load Average;
us/sy/wa/hi/si/st;r/b;cs/in;- 是否有 D 状态、僵尸、短时 Running 任务。
第二层:落到进程和线程
1 | |
确认:
- 哪个进程 CPU 高;
- 哪个线程 CPU 高;
%wait是否高;- 自愿/非自愿上下文切换是否异常。
第三层:按类型下钻
用户 CPU 高
1 | |
系统 CPU 高
1 | |
同时结合 vmstat 和 pidstat -w。
软中断高
1 | |
硬中断高
1 | |
检查设备和 CPU 分布。
iowait 高
1 | |
继续定位具体块设备和进程。
top 找不到明显进程
考虑:
- 短时进程;
- 内核线程;
- 高频创建/退出进程;
execsnoop;perf record做时间窗口采样。
第四层:建立证据链
不要停在:
1 | |
至少要走到:
1 | |
第五层:优化前后对比
至少同时记录:
- 应用吞吐量/延迟;
- CPU 指标;
- 上下文切换;
- I/O 或网络关键指标。
只有优化前后数据可比较,才叫性能优化;否则更多只是“改了点东西,然后感觉不错”。
十九、把整套方法压缩成一句话
Linux CPU 性能分析真正应该掌握的是下面这条主线:
先用
top + vmstat + pidstat建立系统画像,再根据 user/system/iowait/irq/softirq、运行队列和上下文切换选择下一步工具;找到具体进程后用strace或perf下钻,遇到网络软中断就从/proc/softirqs -> sar -> tcpdump建立证据链;优化前先量化指标、确认主瓶颈和权衡成本,优化后用同样的条件重新测试。
命令会变,内核会升级,Java 和容器工具也会继续演进,但这条分析方法不会轻易过时。