Linux 性能调优系列(十): CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
1 | 作者:李晓辉 |
上一篇我们讲了 ulimit、systemd Limit* 和 cgroup v2。这些工具解决的是:
“一个进程或者一个业务,最多可以使用多少资源?”
但是性能排查还有一个非常经典的问题:
“CPU 已经 100% 了,到底是谁在忙?”
比如我们执行:
1 | top |
看到:
1 | PID USER %CPU COMMAND |
我们只能知道:
myapp很忙。
但是不知道:
1 | 它到底在忙什么? |
可能是:
1 | 用户态代码疯狂计算 |
这时候,top 就有点力不从心了。我们需要一个能够继续“往程序里面钻”的工具。这就是今天的主角:perf
这个工具得先安装才行,顺便也安装一下装内核调试软件包
如果需要深入分析内核态性能,尤其是希望将性能数据带到另一台机器进行离线分析,那么对应版本的内核调试符号非常重要。否则
perf可能只能显示内核地址,或者只能解析出有限的内核符号。如果缺少对应的调试包,
perf很可能只能告诉你“某个地址很忙”,最终看到的是一串类似0xffffffff...的十六进制地址,而不是do_syscall_64、schedule这样的函数名称,分析起来基本就是“两眼一抹黑”。
安装的适合要注意用uname -r匹配当前的内核版本哦
1 | dnf install perf kernel-debuginfo-$(uname -r) kernel-debuginfo-common-x86_64-$(uname -r) -y |
要是安装不上,可能是仓库没开
1 | [root@localhost ~]# dnf config-manager --set-enabled baseos-debuginfo |
先从一个真实生产问题开始
假设现在有一台生产服务器:
1 | 16 CPU |
上面运行着:
1 | Nginx |
突然某天晚上,监控报警:
1 | CPU 使用率:98% |
我们第一反应:
1 | top |
看到:
1 | PID USER %CPU COMMAND |
好家伙。这个程序吃了接近 4 个 CPU。但是问题来了:
为什么?
我们继续看:
flowchart TB
TOP["top<br/>发现 order-service CPU 很高"]
TOP --> Q1{"到底哪里在耗 CPU?"}
Q1 --> A["用户态业务代码"]
Q1 --> B["内核态代码"]
Q1 --> C["频繁系统调用"]
Q1 --> D["缓存 / 分支等硬件问题"]
Q1 --> E["上下文切换等软件事件"]
A --> PERF["perf"]
B --> PERF
C --> PERF
D --> PERF
E --> PERF
PERF --> RESULT["定位真正的性能热点"]这就是 perf 的价值。它不是简单告诉你:
“CPU 很高。”
而是继续帮你回答:
“CPU 到底花在哪里了?”
perf 到底是什么?
perf 是 Linux 下非常重要的一套性能分析工具。它依赖 Linux 内核提供的性能事件机制,可以访问:
1 | CPU 硬件性能监控单元 PMU |
从而统计和采样各种性能指标。可以简单理解成:
flowchart LR
APP["应用程序"]
APP --> KERNEL["Linux Kernel"]
CPU["CPU / PMU"] --> KERNEL
KERNEL --> PERF["perf"]
PERF --> STAT["统计指标"]
PERF --> RECORD["采样数据"]
PERF --> REPORT["热点函数"]如果把 top 比喻成:
“医院门口量体温。”
那么 perf 更像:
“拿着各种检查设备进入身体内部,看看究竟是哪儿出了问题。”
perf 能看什么?
perf 最常见的用途,可以分成两大类。
1. 统计
比如:
1 | CPU 一共执行了多少周期? |
主要使用:
1 | [root@localhost ~]# perf stat |
2. 采样
如果我们想进一步知道:
“到底是哪个函数最耗 CPU?”
就需要:
1 | [root@localhost ~]# perf record |
可以理解成:
flowchart LR
STAT["perf stat"] --> NUMBER["得到数字"]
RECORD["perf record"] --> SAMPLE["采集性能样本"]
SAMPLE --> REPORT["perf report"]
REPORT --> FUNCTION["定位热点函数"]所以记住:
1 | perf stat |
这三个命令,是学习 perf 最重要的基础。
perf 里面有两类重要事件
perf 可以观察大量事件。
大体可以分成:
1 | 硬件事件 |
1. 硬件事件
硬件事件来自 CPU 的性能监控能力。常见的包括:
| 事件 | 含义 |
|---|---|
cycles | CPU 时钟周期 |
instructions | 执行的指令数量 |
cache-misses | CPU Cache 未命中次数 |
branches | 分支指令数量 |
branch-misses | 分支预测失败次数 |
2. 软件事件
软件事件主要由 Linux 内核提供。例如:
| 事件 | 含义 |
|---|---|
context-switches | 上下文切换 |
page-faults | 缺页异常 |
cpu-clock | CPU 时钟采样 |
task-clock | 任务运行时间 |
所以可以简单理解:
flowchart TB
PERF["perf"]
PERF --> HW["硬件事件"]
HW --> CYCLES["cycles"]
HW --> INST["instructions"]
HW --> CACHE["cache-misses"]
HW --> BRANCH["branch-misses"]
PERF --> SW["软件事件"]
SW --> CS["context-switches"]
SW --> PF["page-faults"]
SW --> CLOCK["cpu-clock"]先看看你的机器到底支持哪些事件
学习 perf 的第一个命令:
它会列出当前系统支持的性能事件
1 | [root@localhost ~]# perf list |
这里有一个非常重要的知识点:
不同 CPU 支持的硬件 Performance Monitoring Unit(性能监控单元) 事件可能不同。
所以不要看到网上某篇文章写:
1 | perf stat -e xxx |
就认为自己的服务器一定支持。正确习惯是:
1 | perf list |
先看看自己的机器支持什么。
perf stat:先做一次“体检”
如果你现在刚接触 perf,我最建议先掌握:
1 | perf stat |
它做的事情很简单:
统计一段程序运行期间发生了多少性能事件。
这里最重要的是:
perf stat 主要告诉你“发生了多少”,而不是“哪个函数发生了”。
案例一:统计一个命令的 CPU 情况
我们先做一个非常简单的实验。
1 | [root@localhost ~]# perf stat ls -l /etc |
你可以把它理解成:
flowchart LR
CMD["ls -l /etc"]
CMD --> CPU["CPU cycles"]
CMD --> INST["instructions"]
CMD --> CS["context switches"]
CMD --> PF["page faults"]
CPU --> PERF["perf stat"]
INST --> PERF
CS --> PERF
PF --> PERF
PERF --> RESULT["统计结果"]这个实验的价值不是为了研究 ls。而是先熟悉:
perf stat 到底是怎么工作的。
案例二:统计一个程序运行期间的硬件事件
例如:
1 | [root@localhost ~]# perf stat -e cycles,instructions,cache-misses \ |
这里:-e表示指定需要统计的事件。我们一次统计:
1 | cycles |
如何理解 IPC?
perf 经常会给出:
1 | insn per cycle 老李备注:平均每个 CPU 时钟周期执行了多少条指令。 |
也就是:
IPC(Instructions Per Cycle)
简单来说:
1 | IPC = 执行的指令数量 / CPU 周期数量 |
比如:
1 | instructions = 10 亿 |
那么:
1 | IPC ≈ 2 |
可以理解成:
平均每个 CPU cycle 完成约 2 条指令。
但是这里有一个非常容易踩的坑,不要看到:
1 | IPC 很低 |
就直接得出:
“缓存一定有问题。”
这不严谨。IPC 低只能说明:
CPU 每个周期完成的指令数量比较少。
具体原因可能包括:
1 | 缓存访问延迟 |
所以正确的分析方式应该是:
flowchart TB
IPC["IPC 较低"]
IPC --> CACHE["检查 cache-misses"]
IPC --> BRANCH["检查 branch-misses"]
IPC --> STALL["进一步分析流水线停顿"]
IPC --> MEMORY["检查内存访问特征"]
CACHE --> RESULT["综合判断"]
BRANCH --> RESULT
STALL --> RESULT
MEMORY --> RESULTperf 是用来提供证据的,不是让我们看到一个数字就直接下结论。
-a:统计整台机器
如果我们执行:
1 | [root@localhost ~]# perf stat -a sleep 10 |
这里-a表示:
system-wide,统计整台机器。
也就是说:
flowchart TB
HOST["整台服务器"]
HOST --> CPU1["CPU 0"]
HOST --> CPU2["CPU 1"]
HOST --> CPU3["CPU 2"]
HOST --> CPU4["..."]
HOST --> CPUN["CPU N"]
CPU1 --> PERF["perf stat -a"]
CPU2 --> PERF
CPU3 --> PERF
CPU4 --> PERF
CPUN --> PERF如果你只想统计某个命令:
1 | perf stat command |
如果想观察整个系统:
1 | perf stat -a command |
不过这里有一个细节:
1 | perf stat -a sleep 10 |
并不是说 sleep 自己占用了 CPU。
而是:
在
sleep 10这段时间窗口内,统计整台机器的性能事件。
这个区别一定要理解。sleep 10 在这里主要用于提供 10 秒的统计时间窗口,而 -a 决定了统计范围是所有 CPU,因此最终得到的是这 10 秒内整个系统的性能事件,而不是 sleep 进程自身的 CPU 使用情况。
perf record与perf report
perf record 用于采集程序或系统运行过程中的性能数据,将 CPU 使用、调用栈、进程/线程等信息记录到 perf.data 文件中,供后续通过 perf report、perf script 等命令进行分析。
简单来说:
perf record负责“采集数据”,perf report负责“分析数据”。
perf record:我不只想知道“多少”,还想知道“谁”
到这里,我们已经可以统计:
1 | CPU cycles |
但是生产环境真正让人头疼的问题通常是:
“到底哪个函数最耗 CPU?”
例如:
1 | myapp CPU:400% |
我们想知道:
1 | main() |
这时候:
1 | perf stat |
就不够了。需要:
1 | perf record |
perf record 是怎么工作的?
它不是不停地记录程序的每一条指令。而是:
周期性进行采样。
可以想象一下:
1 | 程序运行: |
每次采样记录:
1 | 当时 CPU 正在执行什么函数 |
采样次数足够多以后,就可以通过统计:
某个函数被采到的次数占全部样本的多少
来估算 CPU 时间主要花在哪里。
案例三:使用 perf record 采集性能样本
例如我们执行:
1 | [root@localhost ~]# perf record -g \ |
这里逐个解释。
-o
1 | -o perf-report.data |
指定输出文件,默认情况下:
1 | perf.data |
这里我们指定:
1 | perf-report.data |
-e
1 | -e cpu-clock,context-switches |
指定采集的事件。这里采集:
1 | cpu-clock |
--buildid-mmap
--buildid-mmap 用于在记录映射信息时关联二进制文件的 Build ID。Build ID 可以帮助 perf 在后续分析和归档时确认采样数据对应的具体二进制版本。对于需要将 perf.data 带到其他机器进行离线分析的场景,Build ID 尤其重要。特别是在:
1 | 生产服务器采集 |
这种场景下,要特别注意符号和二进制文件的对应关系。
为什么 perf report 有时候全是地址?
这是初学 perf 非常容易遇到的问题。例如你执行:
1 | perf report |
结果看到:
1 | 0x0000000000401234 |
一脸懵。你肯定会问:
“这到底是什么函数?”
因为 perf 需要符号信息才能把地址转换成人类可读的函数名。
可以理解成:
flowchart LR
SAMPLE["采样数据"]
SAMPLE --> ADDRESS["内存地址"]
SYMBOL["符号信息"] --> RESOLVE["地址解析"]
ADDRESS --> RESOLVE
RESOLVE --> FUNCTION["函数名称"]所以生产环境做性能分析时,要特别注意:
1 | perf 数据 |
三者之间必须能够对应起来。如果你需要分析内核或者带调试符号的程序,可以根据具体环境安装对应的 debuginfo。
先检查自己系统的环境:
1 | uname -r |
然后根据当前系统和内核版本配置对应的 Debug 仓库。
例如:
1 | [root@localhost ~]# dnf config-manager --set-enabled baseos-debuginfo |
然后安装与运行内核匹配的调试包。这里不要简单地认为:
1 | kernel-debuginfo |
装上就万事大吉。真正重要的是:
调试符号版本要和你正在分析的内核 / 二进制匹配。
生产环境尤其要注意这一点。
perf report:看看 CPU 到底耗在哪里
采样结束以后:
1 | [root@localhost ~]# perf report --stdio -i perf-report.data |
--stdio 的意思是:
不进入交互界面,直接输出文本报告。
这里最值得看的就是:
1 | Overhead |
是不是很容易看出谁占的最多了
Overhead 到底是什么意思?
例如:
1 | 29.15% do_syscall_64 |
可以理解成:
在这次采样中,大约 29.15% 的样本落在
do_syscall_64这个函数上。
它是一个采样开销占比,不要机械理解成:
“这个函数精确占用了 29.15% 的 CPU 时间。”
perf 的采样分析是统计意义上的估算。
[k] 和 [.] 是什么?
这是 perf report 里面非常重要的标记。
例如:
1 | [k] do_syscall_64 |
这里[k]表示:内核态函数。
而:
1 | [.] read |
通常表示:用户态代码。
所以看到报告以后,我们可以先粗略判断:
flowchart TB
REPORT["perf report"]
REPORT --> KERNEL["[k] 内核态"]
REPORT --> USER["[.] 用户态"]
KERNEL --> K1["系统调用"]
KERNEL --> K2["内核处理"]
KERNEL --> K3["驱动等"]
USER --> U1["业务代码"]
USER --> U2["glibc"]
USER --> U3["其他共享库"]这就比:
1 | top |
只告诉你:
1 | myapp 400% |
深入很多了。
案例四:发现 CPU 高以后,到底应该怎么排?
这才是 perf 最有价值的地方。
假设:
1 | top |
发现:
1 | order-service CPU 380% |
我们不要马上:
1 | systemctl restart |
而应该:
flowchart TB
A["top 发现 CPU 很高"]
A --> B["确认目标进程"]
B --> C["perf stat"]
C --> D{"整体指标有什么异常?"}
D --> E["IPC / Cache"]
D --> F["Context Switch"]
D --> G["Page Fault"]
D --> H["其他事件"]
B --> I["perf record"]
I --> J["perf report"]
J --> K["找到热点函数"]
E --> L["结合代码 / 系统继续分析"]
F --> L
G --> L
H --> L
K --> L这才是比较完整的性能分析思路。
perf 统计单个进程
如果我们已经知道:
1 | PID = 3831 |
可以使用:
1 | perf stat -p 3831 |
它会附着到指定进程进行统计。
例如:
1 | [root@localhost ~]# perf stat -p 3831 |
如果想指定事件:
1 | [root@localhost ~]# perf stat -p 3831 -e cycles,instructions,cache-misses |
这样就可以专门观察:PID 3831 而不是整个服务器。
perf record 也可以针对指定 PID
例如:
1 | [root@localhost ~]# perf record \ |
这样 perf 就会针对这个进程采样。
然后:
1 | [root@localhost ~]# perf report --stdio -i perf-report.data |
分析。
为什么生产环境更推荐“先短时间采样”?
很多新人第一次接触 perf 会想:
“既然 perf 能分析,那我采一个小时。”
没必要。
性能分析首先要控制分析成本。
例如:
1 | 先采 10 秒 |
而不是:
1 | perf record |
尤其生产环境更要谨慎。
perf archive:把生产现场带回来分析
这也是Linux 性能分析中非常实用的一个场景。
假设:
1 | 生产服务器 |
没有:
1 | 图形界面 |
而你的办公环境:
1 | 分析工具齐全 |
那么完全可以:
flowchart LR
PROD["生产服务器"]
PROD --> RECORD["perf record"]
RECORD --> DATA["perf 数据"]
DATA --> ARCHIVE["perf archive"]
ARCHIVE --> PACKAGE["数据 + 相关文件"]
PACKAGE --> SCP["scp"]
SCP --> HOST["分析服务器"]
HOST --> REPORT["perf report"]这就是:
线上采集,线下分析。
二十四、生产服务器打包 perf 数据
如果采集时已经做好 Build ID 相关信息记录,可以使用:
1 | perf archive |
然后把:
1 | perf-report.data |
以及生成的归档文件一起带走。
例如:
1 | scp \ |
把带回来的数据进行分析
把归档文件解压到对应的调试目录:
1 | mkdir -p ~/.debug |
然后:
1 | perf report --stdio \ |
这样就可以在分析服务器上查看报告。
解压后的目录结构需要能够被
perf用于查找对应的 Build ID 对象。实际离线分析时,还需要保证分析环境能够找到这些对象;如果归档过程中存在缺失的 Build ID,perf archive生成的压缩包也可能无法覆盖全部采样对象。
为什么这个方法特别适合生产环境?
因为我们可以把:
1 | 采集 |
和:
1 | 分析 |
分开。
生产环境只负责:
1 | 抓样本 |
分析服务器负责:
1 | 解析 |
可以理解成:
生产服务器负责“取证”,办公服务器负责“破案”。
这样就不会为了分析一个性能问题,把一堆工具和调试环境全部装进生产服务器。
perf stat 和 perf record 到底怎么选?
可以记住下面这个表:
| 命令 | 主要用途 | 典型问题 |
|---|---|---|
perf list | 查看支持的事件 | 我的 CPU 支持哪些事件? |
perf stat | 统计性能指标 | 程序整体运行特征怎么样? |
perf record | 采集性能样本 | CPU 时间主要花在哪里? |
perf report | 分析样本 | 哪个函数最热门? |
perf archive | 归档分析环境 | 怎么把生产数据带回去分析? |
一句话:
1 | perf list |
一个非常实用的生产排障套路
以后你遇到:
“服务器 CPU 突然很高。”
可以按照下面的顺序排查。
第一步:先看谁在吃 CPU
1 | top |
或者:
1 | ps -eo pid,ppid,cmd,%cpu,%mem --sort=-%cpu | head |
第二步:确定目标 PID
例如:
1 | 3831 |
第三步:先做统计
1 | perf stat -p 3831 \ |
先了解:
1 | CPU 周期 |
第四步:采样
1 | perf record \ |
采集一个短时间窗口。
第五步:分析热点
1 | perf report --stdio \ |
然后重点观察:
1 | Overhead |
第六步:根据结果继续往下钻
例如发现:
1 | 业务函数占比很高 |
那么继续分析:
1 | 应用代码 |
如果发现:
1 | 内核函数占比很高 |
那么继续考虑:
1 | 系统调用 |
如果发现:
1 | cache-misses 很高 |
再进一步研究:
1 | 缓存访问模式 |
这才是:
从“CPU 高”走向“为什么 CPU 高”。
perf 和 top 到底是什么关系?
不要认为:
perf 出来了,top 就没用了。
恰恰相反。它们更像:
flowchart LR
TOP["top<br/>发现问题"]
TOP --> PID["找到高 CPU 进程"]
PID --> PERFSTAT["perf stat<br/>看整体特征"]
PERFSTAT --> PERFREC["perf record<br/>采样"]
PERFREC --> PERFREP["perf report<br/>找热点"]
PERFREP --> ROOT["进一步定位根因"]所以:
1 | top |
负责:
发现问题。
而:
1 | perf |
负责:
深入分析问题。
perf 最重要的不是命令,而是分析思路
很多人学 perf,最后变成了:
1 | 背命令 |
但是一旦真正遇到生产问题:
“我应该统计什么?”
还是不会。所以真正需要建立的是:
flowchart TB
PROBLEM["性能问题"]
PROBLEM --> CPU["CPU 高"]
PROBLEM --> IO["IO 慢"]
PROBLEM --> LAT["延迟高"]
CPU --> STAT["先统计整体指标"]
STAT --> CYCLES["cycles"]
STAT --> INST["instructions"]
STAT --> CACHE["cache-misses"]
STAT --> BRANCH["branch-misses"]
STAT --> CS["context-switches"]
STAT --> RECORD["必要时进行采样"]
RECORD --> REPORT["perf report"]
REPORT --> HOTSPOT["热点函数"]
HOTSPOT --> ROOTCAUSE["结合业务 / 内核继续定位"]所以:
perf 不是“CPU 高了就运行一下”的万能命令。
它真正的价值是:
帮助我们建立从“现象 → 指标 → 热点 → 根因”的分析链路。
几个非常容易踩的坑
坑一:把 IPC 低直接等同于 Cache Miss 高
错误:
1 | IPC 低 |
正确:
1 | IPC 低 |
坑二:只看一个指标就下结论
比如:
1 | cache-misses 很多 |
不代表:
“程序一定有严重性能问题。”
还需要结合:
1 | cache-references |
一起判断。
坑三:生产环境长时间高频采样
perf record 不是完全没有开销。因此生产环境建议:
1 | 短时间 |
不要:
1 | 高频率 |
坑四:perf report 全是地址
看到:
1 | 0x7fxxxx |
不要慌。
检查:
1 | 二进制文件 |
是否匹配。
坑五:线上采集以后直接把数据扔到另一台机器
如果分析机器缺少:
1 | 对应二进制 |
可能无法正确解析函数。
所以才需要:
1 | perf archive |
帮助保存分析所需要的相关文件。
知识点串起来
到这里,我们已经可以把整个过程串起来:
flowchart TB
A["top<br/>发现 CPU 很高"]
A --> B["找到 PID"]
B --> C["perf stat"]
C --> D["cycles / instructions"]
C --> E["cache-misses"]
C --> F["context-switches"]
C --> G["其他事件"]
B --> H["perf record"]
H --> I["perf.data"]
I --> J["perf report"]
J --> K["热点函数"]
K --> L{"下一步"}
L --> M["业务代码分析"]
L --> N["系统调用分析"]
L --> O["内核分析"]
L --> P["CPU / Cache 分析"]
I --> Q["perf archive"]
Q --> R["离线分析"]这时候你就会发现:
上一篇我们学:
1 | cgroup |
解决的是:
“这个业务最多可以吃多少资源?”
这一篇学习:
1 | perf |
解决的是:
“它为什么要吃这么多 CPU?”
两个东西解决的问题完全不同。
perf 在整个 Linux 性能调优体系中的位置
到目前为止,我们已经学了不少工具。可以把整个 Linux 性能分析体系理解成:
flowchart TB
SYSTEM["Linux 性能问题"]
SYSTEM --> HARDWARE["硬件层"]
SYSTEM --> MONITOR["监控层"]
SYSTEM --> KERNEL["内核参数"]
SYSTEM --> LIMIT["资源限制"]
SYSTEM --> ANALYSIS["性能分析"]
HARDWARE --> CPU["CPU / Memory / Disk / Network"]
MONITOR --> TOP["top"]
MONITOR --> VMSTAT["vmstat"]
MONITOR --> IOSTAT["iostat"]
MONITOR --> SAR["sar"]
KERNEL --> SYSCTL["sysctl"]
KERNEL --> TUNED["TuneD"]
LIMIT --> ULIMIT["ulimit"]
LIMIT --> SYSTEMD["systemd"]
LIMIT --> CGROUP["cgroup v2"]
ANALYSIS --> PERF["perf"]
PERF --> STAT["perf stat"]
PERF --> RECORD["perf record"]
PERF --> REPORT["perf report"]这样学习起来就不会变成:
“Linux 有一堆命令,我一个个背。”
而是:
每一个工具都有自己解决的问题。
最后记住这几个命令
如果今天只能记住几个命令,就记住:
查看事件
1 | perf list |
统计程序
1 | perf stat command |
统计指定进程
1 | perf stat -p PID |
采样
1 | perf record -p PID -o perf.data |
分析
1 | perf report --stdio -i perf.data |
查看 Build ID
1 | perf buildid-list -i perf.data |
归档
1 | perf archive |
总结
这一篇我们没有停留在:
“perf 是一个性能工具。”
而是实际建立了一套排障思路。当你看到:
1 | CPU 100% |
不要只停留在:
1 | top |
而是继续问:
1 | CPU 到底在干什么? |
于是:
1 | top |
这才是 perf 真正的价值。
如果说:
cgroup 是给业务“画资源边界”。
那么:
perf 就是在 CPU 出问题以后,拿着放大镜去看“CPU 到底在忙什么”。
下一篇,我们继续往里面钻。因为 perf 虽然可以告诉我们:
“这个函数很忙。”
但是如果我们想进一步知道:
“这个函数到底调用了哪些系统调用?”
甚至:
“它为什么一直在读文件、访问网络、打开文件?”
那就需要另外一组非常经典的工具:
1 | strace |
下一篇,我们就来看看:
Linux 性能调优系列(九)|strace + ltrace 实战:把一个进程的一举一动扒出来
