各位技术同仁、编程爱好者,大家好!
今天,我们将深入探讨一个在高性能计算和系统优化领域至关重要的话题:如何利用 perf 工具结合 Flame Graphs,精确捕捉并解析 Linux 内核态中的性能瓶颈,特别是那些耗时最长的函数调用。在复杂的现代系统中,应用程序的性能往往不仅仅受限于用户空间的代码效率,内核的调度、I/O处理、内存管理等机制也可能成为核心瓶颈。理解这些内核行为,是解决深层性能问题的关键。
揭示内核深处的秘密:perf 与 Flame Graphs 的协同作战
在 Linux 系统中,perf 是一个功能强大且无处不在的性能分析工具,它能够利用处理器的性能监测单元(PMU)以及软件事件来收集系统级的性能数据。而 Flame Graphs,作为一种直观的堆栈可视化技术,则能将 perf 收集到的海量堆栈信息,以一种易于理解和分析的方式呈现出来。当这两者结合时,我们便拥有了一把利器,能够穿透用户空间与内核空间的界限,直达性能问题的核心。
本次讲座的目标是:
- 理解
perf如何在内核态进行采样。 - 掌握
perf record收集内核调用栈的关键参数。 - 学习如何将
perf数据转换为 Flame Graph 可视化。 - 深入解读 Flame Graph,识别内核中的耗时热点。
perf 的基石:性能事件与采样机制
perf 的工作原理基于对特定“性能事件”的计数和采样。这些事件可以分为硬件事件(如 CPU cycles、instructions retired、cache misses)和软件事件(如 context switches、page faults)以及跟踪点(tracepoints)。对于我们今天的目标——找出耗时最长的函数调用,最常用的事件是 cpu-cycles 或 task-clock。
cpu-cycles: 硬件事件,记录 CPU 实际执行指令的周期数。它能直接反映 CPU 的繁忙程度。task-clock: 软件事件,记录进程或线程在 CPU 上执行的时间。对于多核系统,它能更准确地衡量一个任务在所有 CPU 上累计的执行时间。
perf 采用的是统计采样(Statistical Profiling)方法。它不会记录每一个函数调用的开始和结束,而是在预设的频率下,周期性地中断 CPU,记录当前正在执行的指令地址及其完整的调用栈。采样频率越高,数据的精度越高,但同时也会引入更大的性能开销。
准备工作:确保内核符号的可解析性
在深入分析内核性能之前,最关键的一步是确保 perf 能够正确解析内核函数符号。如果没有这些符号信息,你将看到大量的问号或十六进制地址,这将使得分析变得异常困难。
1. 安装内核调试符号
不同 Linux 发行版安装内核调试符号的方法略有不同:
-
Ubuntu/Debian:
sudo apt update sudo apt install linux-image-$(uname -r)-dbgsym有时,
dbgsym包可能位于一个独立的仓库中,需要先启用。例如,对于 Ubuntu,可能需要添加deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse到/etc/apt/sources.list.d/ddebs.list。 -
CentOS/RHEL/Fedora:
sudo dnf debuginfo-install kernel-$(uname -r) # 或者对于旧版本,使用 yum sudo yum debuginfo-install kernel-$(uname -r)通常需要启用
debuginfo仓库。
2. 确认 vmlinux 路径
内核调试符号通常会安装到 /usr/lib/debug/boot/vmlinux-$(uname -r) 或 /usr/lib/debug/lib/modules/$(uname -r)/vmlinux 等位置。perf 通常能自动找到它们,但如果遇到问题,你可以手动指定 vmlinux 路径。
3. 权限要求
perf 进行系统级或内核级采样通常需要 root 权限,或者用户需要具有 CAP_PERFMON 或 CAP_SYS_ADMIN 能力。
sudo perf record -a -g sleep 1
# 如果遇到权限问题,可以尝试修改 perf_event_paranoid 参数
# echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
# 注意:这会降低安全性,只应在调试环境中使用,且用完后应恢复。
抓取内核采样数据:perf record 的艺术
perf record 是我们收集原始性能数据的命令。要有效地捕捉内核态的调用栈,我们需要使用一系列特定的参数。
核心参数解析:
| 参数 | 描述 | 建议值/说明 |
|---|---|---|
-g |
(至关重要) 开启调用图(call graph)收集。没有此参数,将无法生成堆栈信息,也就无法构建 Flame Graph。 | 必须使用。 |
--call-graph |
指定调用图的收集方式。 | 推荐 dwarf。它通过 DWARF 调试信息获取精确的堆栈信息,但可能需要更多 CPU 周期。fp(frame pointer)是次优选择,依赖于函数序言中的栈帧指针,可能不完全准确,尤其是在优化级别较高的代码中。lbr(Last Branch Record)是一种硬件特性,某些处理器支持,能提供精确的调用/返回信息,但需要特定的硬件支持且通常有深度限制。对于内核分析,dwarf 通常是首选,如果 dwarf 失败,可尝试 fp。 |
-a |
(系统级) 收集系统范围内所有 CPU 上的所有事件。对于分析系统级内核瓶颈非常有用。 | 如果目标是整个系统的内核行为,则使用此参数。否则,可以指定 -p <PID> 或 -t <TID> 关注特定进程/线程。 |
-F <freq> |
指定采样频率(Hz)。例如,-F 99 表示每秒采样 99 次。高频率提供更多细节,但开销也更大。 |
建议从 99 到 4000 Hz 之间选择。对于一般分析,99 或 1000 都是不错的起点。如果需要更精细的数据或问题难以复现,可以适当提高。 |
-c <count> |
指定采样周期。例如,-c 1000000 表示每隔 1,000,000 个事件采样一次。与 -F 互斥。 |
如果你更关注事件的密度而不是时间,可以使用此参数。例如,每百万个 CPU cycles 采样一次。 |
--event <event> |
指定要采样的事件。默认为 cpu-cycles 或 task-clock(取决于 perf 版本和系统配置)。 |
对于 CPU 耗时分析,cpu-cycles 或 task-clock 是最佳选择。 |
-k <vmlinux_path> |
手动指定 vmlinux 文件的路径,以帮助 perf 解析内核符号。 |
仅当 perf 无法自动找到 vmlinux 文件时使用。例如:-k /usr/lib/debug/boot/vmlinux-$(uname -r)。 |
-o <file> |
将采样数据保存到指定文件。默认是 perf.data。 |
建议使用有意义的文件名,例如 kernel_profile_YYYYMMDD.data。 |
--pid <PID> |
仅分析指定进程及其子进程的事件。 | 如果你怀疑某个特定应用导致了内核性能问题,可以使用此参数。 |
--tid <TID> |
仅分析指定线程的事件。 | 更精细的控制,用于分析特定线程的行为。 |
--cpu <CPUs> |
仅分析指定 CPU 核心上的事件。例如 --cpu 0,1。 |
当你知道问题集中在某些 CPU 核心时,可以使用此参数减少数据量和开销。 |
常用场景下的 perf record 命令示例:
-
系统级全景内核态 CPU 耗时分析(推荐):
sudo perf record -F 4000 -a -g --call-graph dwarf --event cpu-cycles -o perf.data.kernel_wide -- sleep 30-F 4000: 每秒采样 4000 次,提供较高精度。-a: 全系统采样。-g --call-graph dwarf: 收集带有 DWARF 调试信息的调用栈。--event cpu-cycles: 采样 CPU 周期事件。-o perf.data.kernel_wide: 将数据保存到perf.data.kernel_wide。sleep 30: 运行perf30 秒。你可以替换为你想要分析的实际工作负载。
-
针对特定进程的内核态交互分析:
假设 PID 为 12345 的进程正在运行,并且你怀疑它的内核交互存在问题。sudo perf record -F 1000 -p 12345 -g --call-graph dwarf -o perf.data.pid_12345 -- sleep 10-p 12345: 仅关注 PID 12345 及其子进程。
-
如果
dwarf收集失败(例如,因为缺少调试信息或兼容性问题),回退到fp:sudo perf record -F 4000 -a -g --call-graph fp --event cpu-cycles -o perf.data.kernel_wide_fp -- sleep 30请注意,
fp的精确度可能不如dwarf。
在执行 perf record 后,会生成一个 perf.data(或你指定的文件名)文件,其中包含了所有的采样数据。
初步分析:perf report 快速概览
在生成 Flame Graph 之前,perf report 可以提供一个快速的文本模式概览,帮助我们初步了解数据。
perf report -i perf.data.kernel_wide
perf report 会以交互式界面展示性能热点。你会看到一个类似下面的表格:
| Overhead | Command | Shared Object | Symbol |
|---|---|---|---|
| 15.34% | swapper | [kernel.kallsyms] | native_safe_halt |
| 10.21% | my_app | my_app | my_user_function |
| 8.76% | swapper | [kernel.kallsyms] | cpu_idle |
| 7.12% | my_app | [kernel.kallsyms] | __do_page_fault |
| 5.43% | my_app | libc-2.31.so | __memset_avx2_unaligned |
| 4.88% | my_app | [kernel.kallsyms] | copy_user_enhanced_fast_string |
| … | … | … | … |
解读 perf report 输出:
Overhead: 该符号(函数)及其子函数消耗的 CPU 百分比。Command: 执行该符号的进程或线程的名称。swapper通常表示 CPU 空闲或在等待状态。Shared Object: 该符号所属的共享对象或模块。[kernel.kallsyms]或[kernel]表示内核函数。.so文件表示用户空间的共享库。- 可执行文件名称表示用户空间的应用程序函数。
Symbol: 函数名称。
通过 perf report,我们可以快速识别出排名前几的耗时函数。如果 [kernel.kallsyms] 中的函数占据了显著的比例,那么内核态的瓶颈就显而易见了。然而,perf report 在展现调用栈的完整路径和相互关系上存在局限性,这正是 Flame Graph 的用武之地。
绘制火焰图:将数据转化为洞察
Flame Graphs (火焰图) 是 Brendan Gregg 发明的一种堆栈可视化技术,它能将复杂的调用栈信息以一种直观、高效的方式展现出来。
火焰图的结构:
- X 轴:代表了采样数据中调用栈的宽度,也就是函数在 CPU 上的总耗时比例。函数的宽度越宽,表示它在 CPU 上占用的时间越多。X 轴上的函数是按字母顺序排列的,不代表时间顺序。
- Y 轴:代表调用栈的深度。每个方块是一个函数。栈顶是正在执行的函数,栈底是其调用者。一个函数方块上方的方块是它调用的函数,下方的方块是调用它的函数。
- 颜色:通常是随机的暖色调,用于区分不同的函数,不代表任何特定的性能指标。有时也会根据内核/用户空间进行区分。
生成 Flame Graph 的步骤:
生成 Flame Graph 需要 Brendan Gregg 的 FlameGraph 工具集,它通常是一组 Perl 脚本。你可以从 GitHub 克隆:
git clone https://github.com/brendangregg/FlameGraph.git
export PATH=$PATH:$(pwd)/FlameGraph # 将其添加到 PATH 中
1. 将 perf.data 转换为折叠堆栈(folded stacks)格式:
perf script 命令用于将 perf.data 文件中的原始采样数据导出为可读的文本格式。stackcollapse-perf.pl 脚本则进一步处理这些文本,将每个调用栈转换为一行,并统计其出现次数,形成“折叠堆栈”格式。
# 首先确保你的系统安装了 perf
# 然后执行以下命令
sudo perf script -i perf.data.kernel_wide | FlameGraph/stackcollapse-perf.pl > out.kernel_folded
sudo perf script -i perf.data.kernel_wide: 导出perf.data.kernel_wide中的所有采样事件及其调用栈。| FlameGraph/stackcollapse-perf.pl: 将perf script的输出通过管道传递给stackcollapse-perf.pl脚本。这个脚本会解析每一行的调用栈,将其转换为<callstack>;<callstack>;... <count>的格式。> out.kernel_folded: 将最终的折叠堆栈输出保存到out.kernel_folded文件中。
2. 从折叠堆栈生成 SVG 格式的 Flame Graph:
flamegraph.pl 脚本读取折叠堆栈文件,并生成一个交互式的 SVG 图像。
FlameGraph/flamegraph.pl out.kernel_folded > kernel_flamegraph.svg
FlameGraph/flamegraph.pl out.kernel_folded: 使用flamegraph.pl脚本处理折叠堆栈文件。> kernel_flamegraph.svg: 将生成的 SVG 图像保存为kernel_flamegraph.svg。
现在,你可以用任何现代浏览器(如 Chrome, Firefox)打开 kernel_flamegraph.svg 文件进行交互式分析。
解读内核 Flame Graphs:识别耗时最长的函数调用
打开生成的 kernel_flamegraph.svg 后,你会看到一个由许多彩色方块组成的“火焰山”。现在,我们将学习如何从中提取有价值的性能洞察。
基本解读原则:
- 越宽的方块,耗时越多。 这是最重要的原则。寻找那些在水平方向上占据大量宽度的函数,它们是潜在的性能热点。
- 栈顶 (Y 轴最高处) 的方块是 CPU 实际执行的函数。 它们表示了 CPU 最终花费时间的地方。
- 栈底 (Y 轴最低处) 的方块是调用链的起点。 对于内核火焰图,这通常包括
schedule、ret_from_fork、system_call_entry_point等。 - 向上看是“谁调用了我”,向下看是“我调用了谁”。 如果一个函数很宽,你可以点击它,它会放大,只显示它自己及其子函数,帮助你深入分析其内部耗时分布。
识别内核热点:
在内核火焰图中,你需要特别关注那些标记为 [kernel.kallsyms] 或 [kernel] 的方块,以及在共享对象列中显示为 [kernel] 或 “ 的函数。
以下是一些在内核火焰图中常见且值得关注的耗时函数及其潜在含义:
| 常见内核函数/模式 | 潜在含义 cpu_idle: 当 perf 在 swapper 进程中看到 cpu_idle 变宽时,这意味着 CPU 正在等待工作,而不是实际执行有意义的代码。这可能表明应用程序受限于 I/O,或者没有足够的并发任务来充分利用 CPU。
native_safe_halt: 与cpu_idle类似,表示 CPU 进入空闲状态。rcu_sched_qs/rcu_preempt_qs: RCU(Read-Copy-Update)机制的辅助函数。如果它们很宽,可能表示 RCU 读侧临界区过长,或者 RCU 回收器遇到瓶颈,导致 CPU 在等待 RCU 任务完成。- 内存管理相关:
__do_page_fault/handle_mm_fault: 页面错误处理。如果频繁出现且耗时,可能表明内存不足,导致大量的页面换入/换出,或者应用程序访问了未映射的内存区域。page_fault: 内存页面错误处理。copy_user_enhanced_fast_string/__copy_user_generic_string: 用户空间与内核空间之间数据拷贝。如果很宽,说明应用程序正在进行大量的数据交换,这可能是由频繁的系统调用或大块数据传输引起的。__memset/__memcpy: 内核内部的大量内存操作。
- 调度器相关:
__schedule: 调度器核心函数。如果它很宽,可能意味着系统上存在大量的上下文切换,或者调度器本身在处理高并发任务时遇到了瓶颈。try_to_wake_up/__wake_up_common: 唤醒任务。高频出现可能说明任务频繁休眠和唤醒。
- 锁与同步:
_raw_spin_lock/_raw_spin_unlock/_spin_lock_irqsave/mutex_lock等:各种锁机制。如果这些函数很宽,并且在其上方堆叠着大量其他函数,那往往意味着锁竞争严重。多个 CPU 核心在等待同一个锁释放,导致串行化执行,极大降低并行效率。
- 文件系统/I/O 相关:
ext4_get_block/xfs_vm_get_block/btrfs_get_block: 文件系统块分配或查找。vfs_read/vfs_write: 虚拟文件系统层面的读写操作。blk_mq_get_request: 块设备层请求处理。io_submit/io_getevents(AIO): 异步 I/O 相关。- 如果这些函数很宽,可能表明应用程序是 I/O 密集型的,或者存储系统存在瓶颈。
- 网络相关:
ip_rcv/tcp_v4_rcv/net_rx_action: 网络包接收处理。__napi_poll: NAPI(New API)轮询网络设备。tcp_sendmsg/ip_queue_xmit: 网络包发送处理。- 如果这些函数很宽,通常表明系统正在处理大量的网络流量,或者网络协议栈存在性能问题。
- 虚拟化相关(KVM):
kvm_vcpu_block/kvm_mmu_page_fault: KVM 虚拟机监视器相关。- 这些可能指示虚拟机在等待某些资源,或者 KVM 本身引入了显著开销。
一个分析实例:
假设你观察到一个火焰图,其中 _raw_spin_lock 占据了非常宽的区域,并且在其上方有很多应用程序相关的函数。这强烈暗示应用程序在访问某个共享资源时,频繁地争抢同一个自旋锁。解决办法可能是重新设计数据结构以减少锁粒度,或者采用无锁算法。
再比如,如果你发现 copy_user_enhanced_fast_string 很宽,并且它被 read 或 write 等系统调用直接或间接调用。这表明你的应用程序正在进行大量的用户态和内核态之间的数据拷贝。优化方向可能是减少系统调用次数、使用零拷贝技术(如 sendfile)、或者优化数据传输方式。
进阶技巧与注意事项
-
内核模块符号解析:
除了核心vmlinux,许多驱动和子系统都以内核模块(.ko文件)的形式加载。perf通常也能自动解析它们的符号,前提是这些模块的调试信息也已安装。如果遇到[unknown]模块,你可能需要找到并安装对应模块的debuginfo包。 -
perf.data文件大小与采样频率:
高采样频率会生成非常大的perf.data文件。长时间的系统级采样可能会耗尽磁盘空间。在生产环境中,应谨慎选择采样频率和持续时间。通常在几秒到几分钟的采样就足以发现主要热点。 -
perf自身开销:
perf在收集数据时会引入一定的开销。采样频率越高,开销越大。这可能会对被测系统的性能产生影响,尤其是在高负载下。始终在接近实际生产环境的条件下进行测试,并注意开销。 -
--filter选项:
perf script支持--filter选项,可以用来过滤掉不感兴趣的调用栈。例如,如果你只想看内核态的栈,可以尝试:sudo perf script -i perf.data.kernel_wide --filter 'comm ~ "swapper" || comm ~ "your_app_name" || comm ~ "[kernel]"' | FlameGraph/stackcollapse-perf.pl > out.kernel_filtered_folded(注意:
perf script的--filter语法比较复杂,上述只是一个示意,实际使用请查阅perf script --help。) -
perf annotate:
当perf report或 Flame Graph 指向一个具体的内核函数时,你可以使用perf annotate来查看该函数的汇编代码,并找出其中哪一行指令最耗时。这需要vmlinux文件的精确调试信息。perf annotate -i perf.data.kernel_wide <kernel_function_name> -
不同事件类型的选择:
虽然我们主要关注cpu-cycles或task-clock,但在某些情况下,分析其他事件可能更有洞察力:cache-misses: 查找缓存局部性问题。page-faults: 查找内存访问效率问题。context-switches: 查找任务调度开销。block:*:*: 查找块设备 I/O 延迟。
通过改变--event参数,你可以生成针对这些事件的 Flame Graph,从而从不同维度分析内核行为。
实践案例模拟:一个简单的内核态 CPU 密集型任务
为了演示,我们来模拟一个简单的内核态 CPU 密集型场景。例如,一个用户态程序不断地执行 sync() 系统调用,这会强制内核将所有脏数据写入磁盘,产生一定的内核 I/O 和文件系统活动。
1. 创建一个简单的 C 程序:
// sync_loop.c
#include <stdio.h>
#include <unistd.h>
#include <sys/sync.h>
int main() {
printf("Starting sync loop...n");
while (1) {
sync(); // Force all dirty buffers to disk
// Optionally, add a short sleep to prevent 100% CPU usage if system is too fast
// usleep(100);
}
return 0;
}
2. 编译并运行:
gcc sync_loop.c -o sync_loop
./sync_loop & # 在后台运行
SYNC_LOOP_PID=$! # 获取其 PID
3. 使用 perf 捕获数据:
我们针对这个特定进程进行采样,捕获其导致的内核活动。
sudo perf record -F 4000 -p $SYNC_LOOP_PID -g --call-graph dwarf -o perf.data.sync_loop -- sleep 10
# 10秒后,perf 会自动停止。
kill $SYNC_LOOP_PID # 停止 sync_loop 进程
4. 生成 Flame Graph:
sudo perf script -i perf.data.sync_loop | FlameGraph/stackcollapse-perf.pl > out.sync_loop_folded
FlameGraph/flamegraph.pl out.sync_loop_folded > sync_loop_flamegraph.svg
5. 分析 sync_loop_flamegraph.svg:
打开 sync_loop_flamegraph.svg,你可能会看到:
- 顶层:
sys_sync(或其变体,如__x64_sys_sync) 会占据显著宽度。这是用户空间的sync()系统调用进入内核的入口。 sys_sync的下方:你会看到它调用了vfs_sync_fs。vfs_sync_fs下方:会进一步调用到具体文件系统(如ext4_sync_fs)以及块层(blkdev_issue_flush)相关的函数,最终可能涉及到底层的 I/O 调度和驱动。- 如果存在大量脏页:你可能会看到
write_cache_pages、__writeback_single_inode等函数,表明内核正在积极地将缓存中的数据写回磁盘。
通过这个简单的例子,我们就能直观地看到一个用户态的系统调用如何在内核态引发一系列的函数调用,并消耗 CPU 资源。在实际复杂的应用中,这种可视化将帮助我们快速定位到那些意想不到的内核热点。
结语
掌握 perf 和 Flame Graphs 的组合使用,是现代 Linux 系统性能工程师和开发者必备的技能。它不仅能帮助我们诊断用户空间的性能瓶颈,更能深入内核,揭示那些隐藏在操作系统深处的性能瓶颈。通过精确的采样、严谨的分析和直观的可视化,我们能够更有效地优化系统,提升应用程序的响应速度和吞吐量。希望今天的讲座能为各位在性能优化的道路上提供有力的工具和方法论。