1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com

上一篇我们讲了 ulimit、systemd Limit* 和 cgroup v2。这些工具解决的是:

“一个进程或者一个业务,最多可以使用多少资源?”

但是性能排查还有一个非常经典的问题:

“CPU 已经 100% 了,到底是谁在忙?”

比如我们执行:

1
top

看到:

1
2
PID    USER    %CPU    COMMAND
1234 root 99.8 myapp

我们只能知道:

myapp 很忙。

但是不知道:

1
它到底在忙什么?

可能是:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户态代码疯狂计算
↓
某个函数出现死循环
↓
频繁系统调用
↓
CPU 缓存命中率很差
↓
分支预测失败
↓
大量上下文切换
↓
内核代码消耗大量 CPU

这时候,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
2
16 CPU
32 GB RAM

上面运行着:

1
2
3
4
5
Nginx
MySQL
Redis
业务程序
监控程序

突然某天晚上,监控报警:

1
CPU 使用率:98%

我们第一反应:

1
top

看到:

1
2
PID     USER      %CPU    COMMAND
18372 app 395% order-service

好家伙。这个程序吃了接近 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
2
3
CPU 硬件性能监控单元 PMU
+
Linux 内核软件性能事件

从而统计和采样各种性能指标。可以简单理解成:

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
2
3
4
5
6
7
8
9
CPU 一共执行了多少周期?

执行了多少条指令?

发生了多少次缓存未命中?

发生了多少次上下文切换?

发生了多少次缺页异常?

主要使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@localhost ~]# perf stat

Performance counter stats for 'system wide':

67,874,908,959 cpu-clock # 4.037 CPUs utilized
3,783 context-switches # 55.735 /sec
89 cpu-migrations # 1.311 /sec
262 page-faults # 3.860 /sec
97,369,054 instructions # 0.15 insn per cycle
653,841,045 cycles # 0.010 GHz
21,112,766 branches # 311.054 K/sec
1,432,281 branch-misses # 6.78% of all branches

16.813091627 seconds time elapsed


2. 采样

如果我们想进一步知道:

“到底是哪个函数最耗 CPU?”

就需要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@localhost ~]# perf record
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.606 MB perf.data (4417 samples) ]


[root@localhost ~]# perf report
Samples: 4K of event 'cycles:P', Event count (approx.): 1841795976
Overhead Command Shared Object Symbol
5.42% unix_chkpwd libcrypt.so.2.0.0 [.] blockmix_xor
4.87% swapper [kernel.kallsyms] [k] default_idle
3.94% swapper [kernel.kallsyms] [k] lapic_next_deadline
2.51% unix_chkpwd [kernel.kallsyms] [k] clear_page_erms
1.85% unix_chkpwd libcrypt.so.2.0.0 [.] blockmix_xor_save
1.72% swapper [kernel.kallsyms] [k] intel_pmu_disable_all
1.68% swapper [kernel.kallsyms] [k] asm_sysvec_apic_timer_interrupt
1.00% sshd-session [kernel.kallsyms] [k] avtab_search_node
0.83% swapper [kernel.kallsyms] [k] asm_sysvec_call_function_single
0.71% sshd-session libcrypto.so.3.5.8 [.] bn_sqrx8x_internal
0.60% swapper [kernel.kallsyms] [k] x2apic_send_IPI
0.58% swapper [kernel.kallsyms] [k] sched_balance_update_blocked_averages
0.36% sshd-session sshd-session [.] mult.lto_priv.0
0.33% swapper [kernel.kallsyms] [k] irqentry_enter
0.32% (ostnamed) [kernel.kallsyms] [k] avtab_search_node
0.31% swapper [kernel.kallsyms] [k] enqueue_entity

可以理解成:

flowchart LR

    STAT["perf stat"] --> NUMBER["得到数字"]

    RECORD["perf record"] --> SAMPLE["采集性能样本"]

    SAMPLE --> REPORT["perf report"]

    REPORT --> FUNCTION["定位热点函数"]

所以记住:

1
2
3
4
5
6
7
8
9
10
11
perf stat
↓
看整体指标

perf record
↓
采集样本

perf report
↓
分析热点函数

这三个命令,是学习 perf 最重要的基础。


perf 里面有两类重要事件

perf 可以观察大量事件。

大体可以分成:

1
2
硬件事件
软件事件

1. 硬件事件

硬件事件来自 CPU 的性能监控能力。常见的包括:

事件含义
cyclesCPU 时钟周期
instructions执行的指令数量
cache-missesCPU Cache 未命中次数
branches分支指令数量
branch-misses分支预测失败次数

2. 软件事件

软件事件主要由 Linux 内核提供。例如:

事件含义
context-switches上下文切换
page-faults缺页异常
cpu-clockCPU 时钟采样
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@localhost ~]# perf list

List of pre-defined events (to be used in -e or -M):

branch-instructions OR branches [Hardware event]
branch-misses [Hardware event]
bus-cycles [Hardware event]
cache-misses [Hardware event]
cache-references [Hardware event]
cpu-cycles OR cycles [Hardware event]
instructions [Hardware event]
ref-cycles [Hardware event]

cache:
L1-dcache-loads OR cpu/L1-dcache-loads/
L1-dcache-load-misses OR cpu/L1-dcache-load-misses/
L1-dcache-stores OR cpu/L1-dcache-stores/
L1-icache-load-misses OR cpu/L1-icache-load-misses/
...

这里有一个非常重要的知识点:

不同 CPU 支持的硬件 Performance Monitoring Unit(性能监控单元) 事件可能不同。

所以不要看到网上某篇文章写:

1
perf stat -e xxx

就认为自己的服务器一定支持。正确习惯是:

1
perf list

先看看自己的机器支持什么。


perf stat:先做一次“体检”

如果你现在刚接触 perf,我最建议先掌握:

1
perf stat

它做的事情很简单:

统计一段程序运行期间发生了多少性能事件。

这里最重要的是:

perf stat 主要告诉你“发生了多少”,而不是“哪个函数发生了”。


案例一:统计一个命令的 CPU 情况

我们先做一个非常简单的实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost ~]# perf stat ls -l /etc

Performance counter stats for 'ls -l /etc':

5,951,317 task-clock # 0.618 CPUs utilized
29 context-switches # 4.873 K/sec
0 cpu-migrations # 0.000 /sec
168 page-faults # 28.229 K/sec
13,722,648 instructions # 0.68 insn per cycle
20,166,351 cycles # 3.389 GHz
2,905,193 branches # 488.160 M/sec
68,925 branch-misses # 2.37% of all branches

0.009632427 seconds time elapsed

0.000000000 seconds user
0.006322000 seconds sys

你可以把它理解成:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# perf stat -e cycles,instructions,cache-misses \
dd if=/dev/zero of=/dev/null bs=1M count=1000
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB, 1000 MiB) copied, 0.02423 s, 43.3 GB/s

Performance counter stats for 'dd if=/dev/zero of=/dev/null bs=1M count=1000':

102,355,372 cycles
210,967,302 instructions # 2.06 insn per cycle
44,798 cache-misses

0.026605730 seconds time elapsed

0.000000000 seconds user
0.025568000 seconds sys

这里:-e表示指定需要统计的事件。我们一次统计:

1
2
3
cycles
instructions
cache-misses

如何理解 IPC?

perf 经常会给出:

1
insn per cycle   老李备注:平均每个 CPU 时钟周期执行了多少条指令。

也就是:

IPC(Instructions Per Cycle)

简单来说:

1
IPC = 执行的指令数量 / CPU 周期数量

比如:

1
2
instructions = 10 亿
cycles = 5 亿

那么:

1
IPC ≈ 2

可以理解成:

平均每个 CPU cycle 完成约 2 条指令。


但是这里有一个非常容易踩的坑,不要看到:

1
IPC 很低

就直接得出:

“缓存一定有问题。”

这不严谨。IPC 低只能说明:

CPU 每个周期完成的指令数量比较少。

具体原因可能包括:

1
2
3
4
5
6
缓存访问延迟
分支预测失败
内存访问等待
流水线停顿
指令依赖
资源竞争

所以正确的分析方式应该是:

flowchart TB

    IPC["IPC 较低"]

    IPC --> CACHE["检查 cache-misses"]
    IPC --> BRANCH["检查 branch-misses"]
    IPC --> STALL["进一步分析流水线停顿"]
    IPC --> MEMORY["检查内存访问特征"]

    CACHE --> RESULT["综合判断"]
    BRANCH --> RESULT
    STALL --> RESULT
    MEMORY --> RESULT

perf 是用来提供证据的,不是让我们看到一个数字就直接下结论。


-a:统计整台机器

如果我们执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost ~]# perf stat -a sleep 10

Performance counter stats for 'system wide':

40,011,584,774 cpu-clock # 4.000 CPUs utilized
2,408 context-switches # 60.183 /sec
32 cpu-migrations # 0.800 /sec
82 page-faults # 2.049 /sec
94,333,797 instructions # 0.22 insn per cycle
438,224,175 cycles # 0.011 GHz
20,517,375 branches # 512.786 K/sec
1,037,761 branch-misses # 5.06% of all branches

10.002953997 seconds time elapsed

这里-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
2
3
4
CPU cycles
instructions
cache-misses
context-switches

但是生产环境真正让人头疼的问题通常是:

“到底哪个函数最耗 CPU?”

例如:

1
myapp CPU:400%

我们想知道:

1
2
3
4
5
6
7
main()
↓
process_request()
↓
calculate()
↓
???

这时候:

1
perf stat

就不够了。需要:

1
perf record

perf record 是怎么工作的?

它不是不停地记录程序的每一条指令。而是:

周期性进行采样。

可以想象一下:

1
2
3
4
5
6
7
8
程序运行:

████████████████████████████████

perf:

↑ ↑ ↑ ↑
采样 采样 采样 采样

每次采样记录:

1
当时 CPU 正在执行什么函数

采样次数足够多以后,就可以通过统计:

某个函数被采到的次数占全部样本的多少

来估算 CPU 时间主要花在哪里。


案例三:使用 perf record 采集性能样本

例如我们执行:

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# perf record -g \
-o perf-report.data \
-e cpu-clock,context-switches \
--buildid-mmap \
dd if=/dev/zero of=/dev/null bs=2048 count=1000000
1000000+0 records in
1000000+0 records out
2048000000 bytes (2.0 GB, 1.9 GiB) copied, 0.311058 s, 6.6 GB/s
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.048 MB perf-report.data ]

这里逐个解释。


  1. -o
1
-o perf-report.data

指定输出文件,默认情况下:

1
perf.data

这里我们指定:

1
perf-report.data

  1. -e
1
-e cpu-clock,context-switches

指定采集的事件。这里采集:

1
2
cpu-clock
context-switches

  1. --buildid-mmap

--buildid-mmap 用于在记录映射信息时关联二进制文件的 Build ID。Build ID 可以帮助 perf 在后续分析和归档时确认采样数据对应的具体二进制版本。对于需要将 perf.data 带到其他机器进行离线分析的场景,Build ID 尤其重要。特别是在:

1
2
3
4
5
生产服务器采集
↓
数据拷贝到分析服务器
↓
离线分析

这种场景下,要特别注意符号和二进制文件的对应关系。


为什么 perf report 有时候全是地址?

这是初学 perf 非常容易遇到的问题。例如你执行:

1
perf report

结果看到:

1
2
3
0x0000000000401234
0x0000000000405678
0xffffffff8103831

一脸懵。你肯定会问:

“这到底是什么函数?”

因为 perf 需要符号信息才能把地址转换成人类可读的函数名。

可以理解成:

flowchart LR

    SAMPLE["采样数据"]

    SAMPLE --> ADDRESS["内存地址"]

    SYMBOL["符号信息"] --> RESOLVE["地址解析"]

    ADDRESS --> RESOLVE

    RESOLVE --> FUNCTION["函数名称"]

所以生产环境做性能分析时,要特别注意:

1
2
3
4
5
perf 数据
+
对应的二进制
+
符号信息

三者之间必须能够对应起来。如果你需要分析内核或者带调试符号的程序,可以根据具体环境安装对应的 debuginfo。

先检查自己系统的环境:

1
uname -r

然后根据当前系统和内核版本配置对应的 Debug 仓库。

例如:

1
[root@localhost ~]# dnf config-manager --set-enabled baseos-debuginfo

然后安装与运行内核匹配的调试包。这里不要简单地认为:

1
kernel-debuginfo

装上就万事大吉。真正重要的是:

调试符号版本要和你正在分析的内核 / 二进制匹配。

生产环境尤其要注意这一点。


perf report:看看 CPU 到底耗在哪里

采样结束以后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@localhost ~]# perf report --stdio -i perf-report.data
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 645 of event 'cpu-clock'
# Event count (approx.): 161250000
#
# Overhead Command Shared Object Symbol
# ........ ....... ................. .....................................
#
29.15% dd [kernel.kallsyms] [k] do_syscall_64
9.46% dd [kernel.kallsyms] [k] rep_stos_alternative
9.15% dd libc.so.6 [.] read
8.68% dd libc.so.6 [.] __GI___libc_write
5.27% dd [kernel.kallsyms] [k] fdget_pos
4.96% dd [kernel.kallsyms] [k] __fsnotify_parent
3.41% dd [kernel.kallsyms] [k] read_zero

--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
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost ~]# perf stat -p 3831
Performance counter stats for process id '3831':

9,918,512 task-clock # 0.002 CPUs utilized
89 context-switches # 8.973 K/sec
1 cpu-migrations # 100.822 /sec
0 page-faults # 0.000 /sec
3,982,284 instructions # 0.20 insn per cycle
19,814,847 cycles # 1.998 GHz
838,129 branches # 84.501 M/sec
72,345 branch-misses # 8.63% of all branches

4.137022881 seconds time elapsed

如果想指定事件:

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# perf stat -p 3831 -e cycles,instructions,cache-misses

Performance counter stats for process id '3831':

21,065,652 cycles
4,324,785 instructions # 0.21 insn per cycle
58,862 cache-misses

8.064291846 seconds time elapsed

这样就可以专门观察:PID 3831 而不是整个服务器。


perf record 也可以针对指定 PID

例如:

1
2
3
4
5
6
7
[root@localhost ~]# perf record \
-p 3831 \
-e cpu-clock \
-o perf-report.data
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.023 MB perf-report.data (27 samples) ]

这样 perf 就会针对这个进程采样。

然后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@localhost ~]# perf report --stdio -i perf-report.data
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 27 of event 'cpu-clock'
# Event count (approx.): 6750000
#
# Overhead Command Shared Object Symbol
# ........ ............ ................ ......................
#
100.00% sshd-session [vmxnet3] [k] 0x00000000000038b3


#
# (Tip: Boolean options have negative forms, e.g.: perf report --no-children)
#

分析。


为什么生产环境更推荐“先短时间采样”?

很多新人第一次接触 perf 会想:

“既然 perf 能分析,那我采一个小时。”

没必要。

性能分析首先要控制分析成本。

例如:

1
2
3
4
5
先采 10 秒
↓
看看有没有明显热点
↓
再决定是否延长

而不是:

1
2
3
4
5
perf record
↓
跑 2 小时
↓
服务器磁盘爆了

尤其生产环境更要谨慎。


perf archive:把生产现场带回来分析

这也是Linux 性能分析中非常实用的一个场景。

假设:

1
生产服务器

没有:

1
2
3
图形界面
开发环境
完整调试环境

而你的办公环境:

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
2
3
4
scp \
perf-report.data \
perf-report.data.tar.bz2 \
root@hostA:/root/

把带回来的数据进行分析

把归档文件解压到对应的调试目录:

1
2
3
4
mkdir -p ~/.debug

tar -xf perf-report.data.tar.bz2 \
-C ~/.debug

然后:

1
2
perf report --stdio \
-i perf-report.data

这样就可以在分析服务器上查看报告。

解压后的目录结构需要能够被 perf 用于查找对应的 Build ID 对象。实际离线分析时,还需要保证分析环境能够找到这些对象;如果归档过程中存在缺失的 Build ID,perf archive 生成的压缩包也可能无法覆盖全部采样对象。


为什么这个方法特别适合生产环境?

因为我们可以把:

1
采集

和:

1
分析

分开。

生产环境只负责:

1
抓样本

分析服务器负责:

1
2
3
解析
查看
研究

可以理解成:

生产服务器负责“取证”,办公服务器负责“破案”。

这样就不会为了分析一个性能问题,把一堆工具和调试环境全部装进生产服务器。


perf stat 和 perf record 到底怎么选?

可以记住下面这个表:

命令主要用途典型问题
perf list查看支持的事件我的 CPU 支持哪些事件?
perf stat统计性能指标程序整体运行特征怎么样?
perf record采集性能样本CPU 时间主要花在哪里?
perf report分析样本哪个函数最热门?
perf archive归档分析环境怎么把生产数据带回去分析?

一句话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
perf list
↓
我能看什么?

perf stat
↓
整体怎么样?

perf record
↓
把现场录下来

perf report
↓
谁最耗资源?

perf archive
↓
带回去慢慢分析

一个非常实用的生产排障套路

以后你遇到:

“服务器 CPU 突然很高。”

可以按照下面的顺序排查。

第一步:先看谁在吃 CPU

1
top

或者:

1
ps -eo pid,ppid,cmd,%cpu,%mem --sort=-%cpu | head

第二步:确定目标 PID

例如:

1
3831

第三步:先做统计

1
2
perf stat -p 3831 \
-e cycles,instructions,cache-misses,context-switches

先了解:

1
2
3
4
CPU 周期
指令数
Cache Miss
上下文切换

第四步:采样

1
2
3
4
perf record \
-p 3831 \
-e cpu-clock \
-o perf-report.data

采集一个短时间窗口。


第五步:分析热点

1
2
perf report --stdio \
-i perf-report.data

然后重点观察:

1
2
3
4
Overhead
Command
Shared Object
Symbol

第六步:根据结果继续往下钻

例如发现:

1
业务函数占比很高

那么继续分析:

1
应用代码

如果发现:

1
内核函数占比很高

那么继续考虑:

1
2
3
4
5
系统调用
IO
网络
驱动
内核路径

如果发现:

1
cache-misses 很高

再进一步研究:

1
2
3
4
缓存访问模式
数据结构
内存访问
CPU 架构

这才是:

从“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
2
3
4
5
背命令

perf stat
perf record
perf report

但是一旦真正遇到生产问题:

“我应该统计什么?”

还是不会。所以真正需要建立的是:

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
2
3
IPC 低
↓
一定是 Cache 有问题

正确:

1
2
3
4
5
IPC 低
↓
说明单位周期完成的指令较少
↓
再结合 Cache / Branch / Stall 等指标分析

坑二:只看一个指标就下结论

比如:

1
cache-misses 很多

不代表:

“程序一定有严重性能问题。”

还需要结合:

1
2
3
4
5
cache-references
程序工作负载
内存访问模式
CPU 架构
实际业务延迟

一起判断。


坑三:生产环境长时间高频采样

perf record 不是完全没有开销。因此生产环境建议:

1
2
3
短时间
低影响
有目标

不要:

1
2
3
高频率
长时间
无脑采集

坑四:perf report 全是地址

看到:

1
0x7fxxxx

不要慌。

检查:

1
2
3
4
二进制文件
符号
debuginfo
Build ID

是否匹配。


坑五:线上采集以后直接把数据扔到另一台机器

如果分析机器缺少:

1
2
3
对应二进制
符号
调试信息

可能无法正确解析函数。

所以才需要:

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
2
3
4
5
6
7
8
9
CPU 到底在干什么?
↓
是用户态还是内核态?
↓
哪个函数最耗 CPU?
↓
是不是 Cache / Branch / Memory 等问题?
↓
下一步应该从哪里继续排?

于是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
top
↓
找到问题进程

perf stat
↓
了解整体性能特征

perf record
↓
采集性能样本

perf report
↓
找到热点函数

进一步分析
↓
定位根因

这才是 perf 真正的价值。

如果说:

cgroup 是给业务“画资源边界”。

那么:

perf 就是在 CPU 出问题以后,拿着放大镜去看“CPU 到底在忙什么”。

下一篇,我们继续往里面钻。因为 perf 虽然可以告诉我们:

“这个函数很忙。”

但是如果我们想进一步知道:

“这个函数到底调用了哪些系统调用?”

甚至:

“它为什么一直在读文件、访问网络、打开文件?”

那就需要另外一组非常经典的工具:

1
2
strace
ltrace

下一篇,我们就来看看:

Linux 性能调优系列(九)|strace + ltrace 实战:把一个进程的一举一动扒出来