Linux CPU 性能分析与优化:从软中断到 perf 的系统化排障方法

Linux CPU 性能分析与优化:从软中断到 perf 的系统化排障方法

Linux CPU 性能问题最容易掉进两个坑:一个是看到 CPU 使用率高就开始猜代码,另一个是把所有性能工具都跑一遍,希望某个数字自己跳出来告诉你答案。

真正可复用的方法不是记住多少条命令,而是把 指标、系统原理、工具和根因 连成一条证据链:

先判断“哪类资源或 CPU 时间出了问题”,再找到“哪个进程、线程或内核路径在制造问题”,最后才进入函数、系统调用、网络包或硬件设备层面。

这套方法尤其适合软件开发者。因为线上很多“应用变慢”“接口超时”“CPU 飙升”“机器卡顿”,最终并不一定是业务代码本身的问题,也可能来自调度、I/O、软中断、网络小包、短时进程、容器符号缺失或 JVM JIT 等系统层因素。

版本说明:本文中的实验环境和命令主要来自 Ubuntu 18.04、CentOS 7 时代的 Linux 系统。核心分析思路仍然成立,但 sysstatperf、JDK、容器权限模型和命令参数可能随版本变化。遇到输出与本文不一致时,优先执行 man <command> 核对当前版本的真实语义。


一、CPU 性能问题不是一个“CPU 使用率”数字

CPU 性能分析的第一步,是把“CPU 忙”拆成不同性质的忙。

Linux 中常见的 CPU 指标大致可以分成四组:

  1. CPU 时间花在哪里;
  2. 有多少任务在争抢 CPU;
  3. 调度和上下文切换是否异常;
  4. 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。

这正是 vmstatrb 两列的价值。

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)**则处理可以延后的工作。

以网卡收包为例,可以粗略理解为:

  1. 网卡收到数据包;
  2. 产生硬件中断;
  3. 上半部快速确认设备状态,并把后续工作交给下半部;
  4. 下半部继续处理网络协议栈;
  5. 最终把数据交给 socket 和应用程序。

这里需要补一个非常重要的细节:不能简单认为“所有软中断都是由 ksoftirqd 线程执行”。

软中断可以直接在相关内核执行路径中被处理;当软中断过多、超过处理预算或需要把工作延后时,才会更多地由每个 CPU 对应的 ksoftirqd/N 内核线程接管。

因此,看到 ksoftirqd 占用 CPU 很高,通常说明软中断已经积压到需要内核线程持续处理。

3. ksoftirqd 是什么

Linux 每个逻辑 CPU 都有对应的软中断内核线程,例如:

1
2
[ksoftirqd/0]
[ksoftirqd/1]

可以用:

1
ps aux | grep softirq

查看。

进程名外面的 [] 是一个很有用的识别信号:这通常表示它是内核线程,而不是普通用户态程序。


三、Linux 软中断有哪些类型

系统中的软中断累计次数可以从:

1
cat /proc/softirqs

查看。

典型输出会包含:

1
2
3
4
5
6
7
8
9
10
11
                CPU0       CPU1
HI: 0 0
TIMER: 811613 1972736
NET_TX: 49 7
NET_RX: 1136736 1506885
BLOCK: 0 0
IRQ_POLL: 0 0
TASKLET: 304787 3691
SCHED: 689718 1897539
HRTIMER: 0 0
RCU: 1330771 1354737

常见类别可以这样理解:

类型 典型含义
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
cat /proc/softirqs

信息量有限。

更有效的方式是观察变化速率:

1
watch -d cat /proc/softirqs

-d 会高亮发生变化的部分。这样可以迅速看到哪类软中断正在高速增长。

2. 看 CPU 分布是否失衡

同一种软中断如果长期集中在少数 CPU 上,可能带来负载不均衡。

对于网络中断,常见的系统级优化手段包括:

  • irqbalance
  • smp_affinity
  • 合理配置网卡多队列和 CPU 亲和性。

但不要一看到分布不均就立刻“调均匀”。中断亲和性与应用线程亲和性、NUMA、网卡队列设计是联动的,必须结合实际负载测试。

3. 硬中断在哪里看

硬中断对应:

1
cat /proc/interrupts

因此可以记成:

1
2
/proc/interrupts  -> 硬中断
/proc/softirqs -> 软中断

四、最典型的软中断问题:网络小包

软中断类型很多,但生产环境中最常见的一类性能问题是网络软中断,尤其是 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
docker run -itd --name=nginx -p 80:80 nginx

验证服务:

1
curl http://192.168.0.30/

在隔离的测试环境中,可以使用 hping3 构造高频 SYN 流量:

1
2
3
4
# -S: 设置 TCP SYN
# -p 80: 目标端口 80
# -i u100: 每 100 微秒发送一个包
hping3 -S -p 80 -i u100 192.168.0.30

这类命令只应在你有权限的实验环境中使用,不应对第三方系统执行。

2. 第一步:top 看整体资源

案例中,top 显示 CPU 并不高:

1
2
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni, 96.7 id, 0.0 wa, 0.0 hi, 3.3 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 95.6 id, 0.0 wa, 0.0 hi, 4.4 si, 0.0 st

但两个 CPU 的非空闲时间几乎都消耗在 si 上,而且进程列表里能看到 ksoftirqd

这时正确的结论不是“CPU 才 4%,没问题”,而是:

系统当前有效工作很少,但软中断占了异常突出的一部分,需要继续确认软中断类型。

3. 第二步:/proc/softirqs 找软中断类型

1
watch -d cat /proc/softirqs

案例中 TIMERSCHEDRCU 都会变化,但 NET_RX 增长最快

这就把问题从“CPU 异常”缩小成了:

网络接收软中断异常。

4. 第三步:sar 同时看 PPS 和 BPS

1
sar -n DEV 1

典型输出:

1
2
15:03:46 IFACE       rxpck/s   txpck/s   rxkB/s   txkB/s
15:03:47 eth0 12607.00 6304.00 664.86 358.11

字段含义:

  • 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
tcpdump -i eth0 -n tcp port 80

输出类似:

1
192.168.0.2.18238 > 192.168.0.30.80: Flags [S], seq ...

可以得到两个关键信息:

  • 来源:192.168.0.2
  • Flags [S]:TCP SYN 包。

结合前面的高 PPS,就可以形成完整证据链:

1
2
3
4
5
6
7
系统响应异常
-> top 发现 si 异常突出
-> /proc/softirqs 发现 NET_RX 增长最快
-> sar 发现高 PPS + 低 BPS
-> 计算得出平均包约 54B
-> tcpdump 发现持续大量 SYN
-> 确认 SYN Flood / 异常小包流量

6. 为什么 CPU 只有 4%,SSH 还是很卡

这个案例非常容易被误解。

终端卡顿不是因为“4% 的软中断就把 CPU 打满了”,而是因为登录本身使用 SSH,SSH 也依赖网络。

案例中,在异常流量出现前,网络 RTT 基本不到 1 ms

1
2
0% packet loss
rtt min/avg/max/mdev = 0.815/0.914/0.989/0.081 ms

异常流量出现后:

1
2
33% packet loss
rtt min/avg/max/mdev = 240.637/245.758/250.880/5.145 ms

所以“终端敲回车很慢”的直接原因是网络延迟和丢包,而不是 CPU 整体已经满载。

这件事非常值得记住:

用户看到的“系统卡”只是一种现象,真正瓶颈可能来自 CPU、I/O、网络、调度,甚至远程访问链路。

7. 应用程序也能间接制造软中断问题

软中断运行在内核中,不代表它与应用无关。

例如,同样发送 1 GB 数据:

  • 每次发送 1 KB
  • 每次发送 32 B

后者会制造更多网络包、更高 PPS、更频繁的协议栈处理和中断压力。

所以软中断高时,不应停在“这是内核问题”这一步,还要继续问:

  • 谁制造了这些网络包?
  • 是否存在过小的发送粒度?
  • 是否存在不必要的轮询、短睡眠或高频系统调用?
  • 是否存在异常客户端或攻击流量?

五、从指标出发选择工具,而不是背工具

Linux 性能工具很多,最有效的记忆方式不是按命令背,而是按“我现在缺哪类证据”选择工具。

1. 指标到工具

想看的指标 常用工具 主要价值
平均负载 uptimetop 快速判断整体负载
系统 CPU topvmstatmpstatsar/proc/stat 区分 user/system/iowait/irq/softirq
进程 CPU toppidstatpshtopatop 找高 CPU 进程
每 CPU 使用率 mpstat 观察单核失衡
系统上下文切换 vmstat csrbin
进程/线程上下文切换 pidstat -w 区分自愿和非自愿切换
软中断 top/proc/softirqsmpstat si 和软中断类型
硬中断 vmstat/proc/interrupts 看中断总量和分布
网络 sar -ntcpdumpdstat PPS/BPS、抓包、协议和来源
I/O sar -ddstat 设备吞吐和等待
系统调用 strace 看进程进入内核做了什么
CPU 事件/调用链 perf 函数热点、调用栈、缓存等
短时进程 execsnoop 捕获生命周期很短、top 不易看到的进程

2. 工具到指标

反过来也需要知道每个核心工具擅长什么。

top

适合第一眼看全局:

  • Load Average;
  • 各类 CPU 时间;
  • 僵尸进程;
  • 高 CPU 进程;
  • 进程状态。

执行后按 1 可以展开每个逻辑 CPU。

vmstat

适合看系统调度和资源整体状态:

  • r:运行/等待 CPU 的任务;
  • b:不可中断睡眠任务;
  • in:中断次数;
  • cs:上下文切换次数;
  • CPU 各类时间;
  • 内存和 swap 基本信息。

例如:

1
vmstat 1

pidstat

用于把系统级现象落到进程和线程:

1
2
3
pidstat -u 1
pidstat -w 1
pidstat -u -t 1

它能观察:

  • 进程/线程用户 CPU;
  • 系统 CPU;
  • CPU 等待;
  • 自愿上下文切换;
  • 非自愿上下文切换。

perf

当你已经找到“可疑进程或可疑执行路径”,再用 perf 下钻到函数和调用链,通常更有效。

常用的核心组合是:

1
2
perf record -g -p <PID>
perf report

不要把 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
pidstat -u 1

找到目标进程后,再用:

1
2
perf record -g -p <PID>
perf report

看函数热点。

2. sy 高:优先看系统调用、调度和内核路径

系统 CPU 高时,可以结合:

1
2
3
vmstat 1
pidstat -w 1
pidstat -u -t 1

判断是否是:

  • 上下文切换过多;
  • 高系统调用频率;
  • 锁竞争;
  • 短时进程;
  • 内核函数热点。

需要看系统调用时使用 strace,需要看用户态和内核态完整热点时使用 perf

3. wa 高:进入 I/O 路径

如果同时看到大量不可中断进程,优先使用 I/O 工具定位设备和相关进程。

但要记住:iowait 不是“某个进程等待 I/O 的比例”,它是一个 CPU 时间指标。

4. si 高:按软中断类型继续分支

NET_RXNET_TX 高就继续看网络;SCHED 异常就结合上下文切换和运行队列分析。

5. Load 高:先区分 rb

Load Average 只是现象。

vmstat 可以帮助你判断:

  • r 高:大量任务在运行或等 CPU;
  • b 高:大量任务处于不可中断状态,继续排查 I/O。

七、几个非常容易误解的指标

1. pidstat %wait 不等于 top wa

这是 CPU 性能分析里非常典型的误区。

pidstat 中:

1
%wait = 进程等待 CPU 调度的时间比例

它表示任务已经就绪,但还没拿到 CPU。

top 中:

1
wa / iowait = CPU 等待 I/O 的时间比例

两者完全不是一个概念。

可以记成:

1
2
pidstat %wait -> 等 CPU
top iowait -> 等 I/O

2. 等待 CPU 和等待 I/O 的任务状态不同

  • 等待 CPU:已经在 Run Queue 中,本质上属于可运行任务;
  • 等待部分 I/O:可能处于不可中断睡眠状态(D state)。

不要把“可中断/不可中断睡眠”“硬中断/软中断”“Unix Signal”混成一组概念。

3. Ctrl+CCtrl+Z 是 Signal,不是硬中断/软中断

它们属于进程间通信和进程控制中的信号机制:

  • Ctrl+C 通常触发 SIGINT
  • Ctrl+Z 通常触发 SIGTSTP

这和内核处理网卡、磁盘设备的硬中断不是一回事。

4. vmstat 第一行为什么经常特别奇怪

执行:

1
vmstat 5

时,第一行并不代表“最近 5 秒”。

它的 CPU、I/O 等部分通常是 系统启动以来的平均值;后续行才是每个采样间隔内的平均值。

因此:

分析瞬时问题时,不要拿 vmstat 第一行和后续行直接比较。

进程和内存相关部分则更接近即时状态。

5. 工具输出不符合常识,第一反应应该是 man

不同发行版、不同工具版本,字段和参数可能不同。

性能分析时一个很重要的习惯是:

1
2
3
man vmstat
man pidstat
man perf-report

如果一个指标解释不通,不要先怀疑内核“坏了”,先确认工具本身到底在输出什么。


八、实验为什么经常复现不出来

性能实验高度依赖:

  • CPU 核数;
  • 内存;
  • HDD / SATA SSD / NVMe;
  • 内核版本;
  • 虚拟机与宿主机调度;
  • 工具版本;
  • 容器运行模式。

同一条压力命令,在不同机器上可能出现完全不同的结果。

1. stress -i 不一定能制造高 iowait

早期常见的写法:

1
stress -i 1 --timeout 600

其中 -i 主要通过 sync() 制造系统调用和刷盘行为。

问题是:如果页面缓存里本来就没有多少脏数据,sync() 并不会产生足够大的磁盘压力。

在 SSD 环境下尤其明显,可能看到:

  • iowait 几乎为 0;
  • sys 反而升高。

更可靠的实验方式是使用 stress-ng 同时制造实际文件读写:

1
stress-ng -i 1 --hdd 1 --timeout 600

2. 磁盘性能差异会彻底改变现象

同一个 I/O 压力:

  • 在机械盘上可能直接把磁盘利用率压到 100%;
  • 在低端 SSD 上刚好形成明显 iowait;
  • 在高性能 NVMe 上可能几乎看不到瓶颈。

因此,“我复现不出来”并不代表分析方法错误。

3. 不要假设磁盘一定是 /dev/sdX

现代云主机和 NVMe 设备常见:

1
/dev/nvme0n1

如果测试程序只匹配 /dev/sd/dev/xvd,就可能找不到真实块设备。

示例测试程序后来增加了类似参数:

  • -d:指定读取磁盘;
  • -s:每次读取的数据量,默认 67108864 字节,即 64 MB
  • -c:子进程读取次数,默认 20

示例:

1
2
3
docker run --privileged --name=app -itd \
feisky/app:iowait \
/app -d /dev/sdb -s 67108864 -c 20

4. 单核机器无法复现 RES 重调度中断

RES 是 Rescheduling Interrupt(重调度中断),用于处理器之间通知和调度,通常依赖 IPI(Inter-Processor Interrupt,处理器间中断)。

如果系统只有一个逻辑 CPU,就不存在把任务重新调度到另一个 CPU 的意义,因此很难看到 RES 增长。

但这不代表“没有调度问题”。

大量线程仍然会在单核 CPU 上争抢时间片,你会看到:

  • cs 大幅增加;
  • 非自愿上下文切换上升;
  • pidstat%wait 很高;
  • 每个线程真正执行 CPU 的比例反而很低。

例如:

1
pidstat -u -t 1

可以直接观察线程等待 CPU 的情况。


九、perf:从“哪个进程”下钻到“哪个函数”

topvmstatpidstat 负责告诉你哪里可疑;perf 的价值是继续回答:

CPU 时间到底消耗在哪条调用链、哪个函数、哪个内核路径上?

1. 最重要的两个命令

对于日常 CPU 性能定位,先掌握:

1
2
perf record -g -p <PID>
perf report

-g 用于采集调用栈。

也可以先用实时模式:

1
perf top -g -p <PID>

但线上复盘时,record + report 更便于保存证据。

2. 为什么不能“一上来就 perf”

因为高采样比例不等于性能瓶颈。

一个典型例子是 swapper

很多人看到 perf report 中:

1
swapper 99%

第一反应是“这就是瓶颈”。

实际上,swapper 与 swap 分区没有关系。它本质上是 CPU 空闲时运行的 idle task。

如果调用链主要落在:

1
do_idle

swapper 高反而可能说明 CPU 大部分时间没事做。

所以:

事件占比大,只能说明采样多,不能单独证明它是问题。

要把 perf 结果和 topvmstatpidstat 等系统指标交叉验证。

3. ChildrenSelf

perf report 常见两个关键列:

  • Self:当前符号本身消耗的比例;
  • Children:该符号下面所有直接和间接子调用消耗的累计比例。

因此:

  • Self 高:函数本身很重;
  • Children 高但 Self 低:真正成本主要在它调用的其他函数中。

4. 为什么调用栈不显示

perf report 的调用图默认存在最小显示阈值。

资料中的示例里,默认阈值是 0.5%。如果一个事件只有 0.34%,调用栈就可能默认不展开。

可以降低阈值:

1
perf report -g graph,0.3

这时低于默认阈值但高于 0.3% 的调用链就会出现。

5. 为什么只看到 16 进制地址

如果输出是:

1
2
3
0x7fd84a21c96e
0x7fd84a21d323
...

而不是函数名,通常不是 perf “不会分析”,而是 符号解析失败

常见原因:

  • 依赖库不在宿主机文件系统中;
  • 容器里的库路径宿主机找不到;
  • 二进制经过 strip,符号表被删除;
  • 缺少 debug symbols;
  • 内核符号访问权限受限。

如果 perf 底部出现类似:

1
Failed to open ..., continuing without symbols

就应该沿着符号路径问题排查。

对于需要长期性能诊断的服务,构建产物是否保留足够的符号信息,是工程可观测性的一部分。


十、容器里做 perf,难点通常不是采样,而是符号

容器应用依赖的动态库位于镜像文件系统中,而 perf 往往运行在宿主机。

因此会出现:

宿主机可以采到地址,但无法用容器内的库文件把地址翻译成函数名。

方案一:在宿主机复制相同依赖路径

理论上可行,但容易污染宿主机,不推荐。

方案二:直接在容器内运行 perf

这通常需要较高权限,普通容器可能报:

1
Operation not permitted

可以通过特权容器或调整 perf_event_paranoid 放开权限,但这会影响安全边界,不适合把“为了分析方便”直接变成生产默认配置。

方案三:把容器根文件系统作为 symfs

可以先拿到容器 PID:

1
PID=$(docker inspect --format '{{.State.Pid}}' phpfpm)

然后将容器根文件系统绑定到一个目录:

1
2
3
mkdir /tmp/foo
bindfs /proc/$PID/root /tmp/foo
perf report --symfs /tmp/foo

使用完成后:

1
umount /tmp/foo/

这里的核心不是 bindfs 本身,而是告诉 perf

“解析这些地址时,请去容器真正的文件系统里找库和符号。”

方案四:宿主机采样,容器内解析

先在宿主机:

1
perf record -g -p <PID>

采样完成后:

1
2
docker cp perf.data phpfpm:/tmp
docker exec -it phpfpm bash

容器内安装匹配的 perf,再:

1
2
cd /tmp
perf report

需要特别注意 perf 版本与宿主机内核匹配问题。某些发行版中的 perf 命令本质上会按内核版本选择对应工具,容器用户态和宿主机内核版本不一致时更容易出现兼容问题。


十一、Java 为什么比普通 ELF 程序更难用 perf 看懂

Java 服务多了一层 JVM 和 JIT。

从操作系统看,运行的是 JVM 进程。Java 热点代码经过 JIT 编译后,会动态生成机器码;这些代码并不像普通 ELF 文件那样天然带着静态符号路径。

因此你可能看到:

  • JVM 内部函数;
  • 地址;
  • 内核函数;
  • 但看不到熟悉的 Java 方法名。

资料给出的 JIT 符号方案

perf_events 可以结合 JIT 符号映射,但需要类似:

1
/tmp/perf-<PID>.map

这样的符号映射文件。

perf-map-agent 可以用于生成映射。

为了得到更完整的栈,资料中的 JDK 方案还要求开启:

1
-XX:+PreserveFramePointer

形成的思路是:

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
2
3
1. 确定量化指标
2. 测试优化前指标
3. 测试优化后指标

指标至少应该覆盖两个维度。

应用维度

例如 Web 服务:

  • 吞吐量;
  • QPS;
  • 请求延迟;
  • P95 / P99;
  • 错误率。

系统资源维度

例如:

  • CPU user/system/softirq;
  • Load Average;
  • 上下文切换;
  • I/O;
  • 网络 PPS/BPS。

只看 CPU 从 90% 降到 40%,不能证明业务性能一定更好;只看 QPS 升高,也无法解释成本来自哪里。

2. 多个性能问题同时存在,先优化谁

一个系统可能同时存在:

  • system CPU 高;
  • 上下文切换高;
  • iowait 高;
  • 网络软中断高;
  • 应用延迟高。

它们可能不是五个独立问题,而是同一根因的连锁反应。

例如:

1
2
3
4
5
6
过多线程
-> CPU 争抢
-> 非自愿上下文切换升高
-> 调度开销升高
-> system CPU 升高
-> 请求延迟升高

所以优化前先梳理因果关系,优先处理最上游、影响最大的瓶颈。

一个实用原则是:

  • 如果某个系统资源已经 100% 饱和,优先解决资源瓶颈;
  • 如果多个指标都变化,优先分析变化幅度最大且与瓶颈有直接因果关系的指标;
  • 不要把每一个“异常数字”都当成独立问题。

3. 多种方案都能优化,选哪个

性能提升不是唯一指标,还要考虑:

  • 实现复杂度;
  • 可维护性;
  • 稳定性;
  • 资源成本;
  • 可移植性;
  • 对其他指标的副作用。

典型例子是 DPDK。

它可以绕过传统内核网络协议栈以提高包处理能力,但通常需要:

  • 独占 CPU;
  • 大页内存;
  • 持续高 CPU 占用;
  • 更复杂的运行和维护方式。

在 CPU 核心很少的机器上,这种优化就可能得不偿失。

因此性能优化本质上是 Trade-off(权衡),而不是“把一个指标做到极致”。


十四、性能测试本身必须可信

1. 压测端和服务端尽量分离

如果压测工具和目标服务跑在同一台机器上:

  • 压测工具自己也会占 CPU;
  • 会消耗网络和内存;
  • 可能影响缓存;
  • 最终测到的是“服务 + 压测器”的混合结果。

因此 Web 服务性能测试通常至少分两台机器:

1
Client / Load Generator -> Server

2. 优化前后环境必须一致

对比实验要保证:

  • CPU/内存规格相同;
  • 内核和关键配置一致;
  • 外部依赖一致;
  • 测试参数一致;
  • 测试时长和预热方式一致。

否则“优化后更快”可能只是环境变化。

3. 性能结果要看绝对值,也要看趋势

例如某个指标从 1s 降到 1ms,比例上是 1000 倍,但如果它一天只执行一次,对系统整体价值可能很低。

这也是所谓“二八原则”的工程意义:

把精力集中在真正决定系统整体性能的少数热点上。


十五、应用程序层的 CPU 优化方法

1. 删除不必要的工作

最有效的优化往往不是“让 CPU 执行更快”,而是“根本不要执行”。

典型方向:

  • 减少无意义循环;
  • 减少递归层数;
  • 减少重复计算;
  • 减少频繁动态内存分配;
  • 减少过细粒度日志;
  • 合并高频小 I/O 或小网络包。

2. 编译器优化

例如 GCC:

1
gcc -O2 ...

编译器可以进行:

  • 常量传播;
  • 死代码删除;
  • 指令优化;
  • 循环优化;
  • 内联等。

但编译器优化仍然不能替代算法和架构层面的优化。

3. 算法优化

算法复杂度的影响通常远大于指令级微优化。

例如数据规模较大时:

1
O(n^2) -> O(n log n)

往往比微调几个 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
CPU -> 本地内存

而不是频繁访问远端 NUMA node。

因此 CPU 绑核和内存绑定要一起考虑,不能只做其中一半。

6. 中断负载均衡

可以使用:

1
irqbalance

或者配置:

1
smp_affinity

让硬中断在多个 CPU 之间更合理分布。

但对高性能网络应用来说,完全自动均衡也未必是最优;有时需要把网卡队列、中断 CPU 和应用工作线程进行整体亲和性设计。


十七、不要过早优化

“过早优化是万恶之源”不是说性能不重要,而是说:

没有证据的优化,很容易把简单系统变成复杂系统,却没有得到可验证的收益。

过早优化的典型副作用包括:

  • 代码复杂度上升;
  • 可维护性下降;
  • 调试困难;
  • 新需求难以适配;
  • 一个指标变好,另一个指标变差;
  • 系统资源被提前固化。

比较稳妥的策略是:

1
2
3
4
5
6
7
先满足当前需求
-> 建立监控和基线
-> 发现瓶颈
-> 收集证据
-> 优化最重要的问题
-> 重新测试
-> 再决定是否继续优化

性能优化应该是迭代过程,而不是一次性“把所有参数调到极致”。


十八、一份适合线上排障的 CPU Checklist

遇到“机器慢了、CPU 异常、接口延迟上升”时,可以按下面顺序快速走一遍。

第一层:全局判断

1
2
3
uptime
top
vmstat 1

确认:

  • Load Average;
  • us/sy/wa/hi/si/st
  • r/b
  • cs/in
  • 是否有 D 状态、僵尸、短时 Running 任务。

第二层:落到进程和线程

1
2
3
pidstat -u 1
pidstat -w 1
pidstat -u -t 1

确认:

  • 哪个进程 CPU 高;
  • 哪个线程 CPU 高;
  • %wait 是否高;
  • 自愿/非自愿上下文切换是否异常。

第三层:按类型下钻

用户 CPU 高

1
2
perf record -g -p <PID>
perf report

系统 CPU 高

1
2
strace -p <PID>
perf record -g -p <PID>

同时结合 vmstatpidstat -w

软中断高

1
2
3
watch -d cat /proc/softirqs
sar -n DEV 1
tcpdump -i <iface> -n

硬中断高

1
cat /proc/interrupts

检查设备和 CPU 分布。

iowait 高

1
2
sar -d 1
dstat

继续定位具体块设备和进程。

top 找不到明显进程

考虑:

  • 短时进程;
  • 内核线程;
  • 高频创建/退出进程;
  • execsnoop
  • perf record 做时间窗口采样。

第四层:建立证据链

不要停在:

1
“CPU 高”

至少要走到:

1
2
3
4
5
指标异常
-> 对应资源/内核机制
-> 具体进程/线程/设备
-> 具体调用链/系统调用/数据包
-> 可复现或可验证的根因

第五层:优化前后对比

至少同时记录:

  • 应用吞吐量/延迟;
  • CPU 指标;
  • 上下文切换;
  • I/O 或网络关键指标。

只有优化前后数据可比较,才叫性能优化;否则更多只是“改了点东西,然后感觉不错”。


十九、把整套方法压缩成一句话

Linux CPU 性能分析真正应该掌握的是下面这条主线:

先用 top + vmstat + pidstat 建立系统画像,再根据 user/system/iowait/irq/softirq、运行队列和上下文切换选择下一步工具;找到具体进程后用 straceperf 下钻,遇到网络软中断就从 /proc/softirqs -> sar -> tcpdump 建立证据链;优化前先量化指标、确认主瓶颈和权衡成本,优化后用同样的条件重新测试。

命令会变,内核会升级,Java 和容器工具也会继续演进,但这条分析方法不会轻易过时。


Linux CPU 性能分析与优化:从软中断到 perf 的系统化排障方法
https://allendericdalexander.github.io/2026/08/11/devops/linux/performanceOptimization/02linux-cpu-performance-analysis/
作者
AtLuoFu
发布于
2026年8月11日
许可协议