性能问题往往没有统一的标准答案,但 Linux 提供了一套完整的工具链,可以从不同层面揭示系统的运行状况。本文系统介绍从系统调用跟踪到内核动态追踪的常用工具,帮助开发者在面对高 CPU、高延迟或资源瓶颈时快速定位根因。
strace:系统调用跟踪
strace 是最基础也是使用率最高的诊断工具之一。它通过拦截应用程序与内核之间的边界——系统调用——来展示程序到底在做什么。
基本用法
跟踪一个正在运行的进程:
strace -p 1234
-f 选项可以跟踪子进程,这对于分析会 fork 的服务程序(如 Nginx、Apache)至关重要:
strace -f -p $(pgrep nginx)
过滤输出
默认情况下 strace 输出极其详尽,可以通过 -e 精确过滤。只查看网络相关调用:
strace -e trace=network -p 1234
或者组合多个调用:
strace -e trace=open,read,write ./myprogram
时间信息
三个时间相关选项用途不同:
-T:显示每个系统调用的耗时(挂在返回值后面)-tt:在每一行前面加上精确的时间戳-r:显示相邻调用之间的相对时间差
组合使用可以快速定位慢调用:
strace -T -tt -e trace=openat,read,write -p 1234 2>&1 | head -50
典型应用场景
- 程序卡死:strace 停在某个调用上不动,通常是网络 I/O 或等待锁
- 查找配置文件读取路径:用
-e trace=openat可以一目了然 - 追踪失败的系统调用:不加过滤时,返回值中的
-1和对应的 errno 会被清晰标注
局限性
strace 使用 ptrace 机制,会使被跟踪进程在每次系统调用前后都陷入停止状态。对于 I/O 密集型或高并发程序,带来的性能开销非常明显,不适合在生产环境长时间使用。
ltrace:库调用跟踪
如果说 strace 站在用户态与内核态的边界,ltrace 则站在应用程序与动态链接库(libc、libssl 等)的边界。它能显示程序调用了哪些库函数,以及传入的参数和返回值。
ltrace -p 1234
当你的程序卡在某个库函数(比如 gethostbyname 做 DNS 解析、或者 malloc 触发了大量内存分配)时,ltrace 比 strace 更直接。-S 选项可以同时叠加系统调用输出:
ltrace -S ./myprogram
ltrace 同样基于 ptrace,开销与 strace 相当,适合短时间的定向分析而非持续监控。
perf:硬件性能计数器
perf 是 Linux 内核自带的性能分析框架,核心驱动力来自 CPU 内部的 PMU(Performance Monitoring Unit)。PMU 是 CPU 上的专用硬件单元,用于统计微架构级别的事件,包括 CPU 周期数(cycles)、指令数(instructions)、缓存未命中(cache-misses)、分支预测失败(branch-misses)等。perf 将这些硬件事件暴露给用户空间,实现低开销、高精度的性能统计。
perf stat:整体统计
perf stat -d ./myprogram
典型输出如:
1,234.56 msec task-clock # 0.999 CPUs utilized
4,567,890,123 cycles # 3.700 GHz
8,901,234,567 instructions # 1.95 insn per cycle
123,456,789 cache-misses # 10.0% of all cache refs
insn per cycle(IPC)是关键指标。IPC 低说明 CPU 在等待,可能是缓存未命中、分支预测失败或资源依赖链过长。IPC 高但执行时间仍然长,则可能是算法本身计算量过大。
perf record & report:采样分析
perf record 以固定频率(默认约 4kHz)对 CPU 进行采样,记录当前正在执行的指令地址和调用栈:
perf record -g ./myprogram
perf report --stdio
-g 启用调用栈记录,report 输出按函数占用百分比排序,可以直接看出热点函数。
perf top:实时系统级分析
类似 top,但按函数而非进程展示 CPU 占用:
sudo perf top -g
在排查"某个核跑满但不知道是谁在烧 CPU"的场景时非常有效。
perf annotate:源码级定位
perf annotate --stdio
将热点定位到具体的源码行或汇编指令,对于编译器优化后代码乱序的情况尤其有价值。
火焰图
perf 的文本报告在调用链较深时难以直观判断瓶颈。Brendan Gregg 引入的火焰图(Flame Graph)解决了这一问题。
生成方法
# 1. 记录采样数据
perf record -F 99 -a -g -- sleep 60
# 2. 生成折叠后的栈信息
perf script > out.perf
# 3. 生成火焰图
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > flame.svg
解读要点
- 宽度:代表该函数在采样中出现的频率,越宽说明占用 CPU 越多
- 纵轴:堆栈深度,从下到上是一个完整的调用链
- 颜色:无特定含义,仅用于视觉区分不同函数
火焰图不是时序图,x 轴不表示时间。观察时从底部宽柱向上追溯,找到最宽的"平顶山"即为优化目标。
CPU 火焰图 vs Off-CPU 火焰图
- CPU 火焰图:函数在 CPU 上执行的时间。用于发现计算热点
- Off-CPU 火焰图:函数不在 CPU 上执行的时间(阻塞在 I/O、锁、sleep 等)。用于发现延迟根因
两者结合,可以完整描述程序在时间维度上的去向。
bpftrace:动态追踪的简洁表达
bpftrace 是一门高级追踪语言,底层依赖 eBPF。它将复杂的内核编程抽象为接近 awk 的简洁语法,让开发者用一行命令就能完成过去需要写几百行 C 代码才能实现的内核探测。
eBPF 是什么
eBPF(extended Berkeley Packet Filter)是 Linux 内核中的沙箱化字节码虚拟机。用户编写的程序经过内核验证器(verifier)严格的安全检查(例如禁止循环、限制指令数、验证内存访问合法性),然后由 JIT 编译器转换为本地机器码,在内核空间高效执行。eBPF 通过内核 Map 与用户空间共享数据,避免频繁的上下文切换。
bpftrace 语法结构
一条 bpftrace 程序由三部分组成:
probe /predicate/ { action }
- probe:探测点,如
kprobe:do_sys_open、tracepoint:syscalls:sys_enter_read、uprobe:/lib/libc.so.6:getaddrinfo - predicate:可选的过滤条件
- action:触发时执行的操作
常用单行命令
统计各进程的系统调用次数:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
跟踪 openat 的调用并打印文件名:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s: %s\n", comm, str(args->filename)); }'
追踪内核函数 tcp_drop 并打印导致丢包的进程:
bpftrace -e 'kprobe:tcp_drop { printf("%s dropped a packet\n", comm); }'
函数入参出参跟踪:
bpftrace -e 'uprobe:/bin/bash:readline { @start[tid] = nsecs; } uretprobe:/bin/bash:readline / @start[tid] / { printf("readline took %d us\n", (nsecs - @start[tid]) / 1000); delete(@start[tid]); }'
与 perf 的选择
- perf:基于 PMU 硬件计数器,开销极低(通常 < 2%),适合 CPU 分析、缓存分析等硬件事件场景。对内核版本依赖低,几乎所有 Linux 发行版都自带
- bpftrace:基于 eBPF,灵活性极高,可以探测几乎任意内核或用户函数。适合需要动态探测、Arg 追踪、事件关联等复杂场景。但需要较新的内核(4.x 以上)和 BTF 支持
eBPF 进阶基础
eBPF 的核心执行流程可以概括为四步:
- 用户用 C 语言(或 bpftrace 等高级语言)编写程序
- 编译为 eBPF 字节码后加载入内核
- 内核验证器进行静态安全检查
- JIT 编译后在内核中运行,通过 Map 与用户态程序通信
Map 类型
Map 是 eBPF 程序内外数据交换的核心机制。常见类型包括:
BPF_MAP_TYPE_HASH:键值对哈希表BPF_MAP_TYPE_ARRAY:固定大小的数组BPF_MAP_TYPE_PERF_EVENT_ARRAY:向用户态发送事件BPF_MAP_TYPE_RINGBUF:高性能环形缓冲区(Linux 5.8+)
BTF 与 CO-RE
BTF(BPF Type Format)携带了内核数据结构的类型信息。基于 BTF,eBPF 程序可以一次编译,到处运行(Compile Once – Run Everywhere,简称 CO-RE),无需在每个目标内核上重新编译。这解决了 eBPF 程序长期以来的内核版本兼容痛点。
libbpf 示例框架
libbpf 是内核维护的官方用户态加载库。一个最小化骨架如下:
// kernel side: minimal.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("tp/syscalls/sys_enter_write")
int trace_write(struct trace_event_raw_sys_enter *ctx)
{
bpf_printk("write syscall, fd=%d\n", ctx->args[0]);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
用户态通过 bpf_object__open、bpf_object__load、bpf_program__attach 三步即可将程序挂载到内核。建议使用 bpftool gen skeleton 自动生成骨架代码,大幅减少样板代码量。
其他辅助工具
sysstat 套件
vmstat 1:每秒输出进程、内存、swap、I/O、CPU 综合状态,适合快速判断瓶颈大类iostat -x 1:详细展示各块设备的 I/O 利用率、队列深度、服务时间,定位磁盘瓶颈pidstat 1 -p PID:按进程展示 CPU、内存、I/O、线程切换等细粒度指标
htop
交互式进程查看器,支持树状展示、按多种指标排序、搜索过滤。比传统 top 更直观,适合实时监控。
/proc/[pid] 文件
/proc/[pid]/status:进程整体状态,重点查看VmRSS(实际物理内存)、Threads(线程数)、voluntary_ctxt_switches(主动上下文切换次数,可反映 I/O 等待程度)/proc/[pid]/smaps:进程内存映射的详细分解,结合Pss(比例集大小)可以精确分析内存占用构成
atop
atop 以每秒采样并持久化到日志,事后可以用 atop -r /var/log/atop/atop_YYYYMMDD 回放历史状态。这在"性能问题已消失,需要复盘当时发生了什么"的场景中无可替代。
总结
| 工具/技术 | 观察层面 | 核心优势 | 适用场景 |
|---|---|---|---|
| strace | 系统调用 | 直观、零配置 | 程序行为分析、排查挂死、查找配置文件路径 |
| ltrace | 库函数调用 | 聚焦用户态库 | 库函数级热点或阻塞分析 |
| perf | PMU 硬件事件 | 低开销、高精度 | CPU 消耗分析、缓存分析、热点定位 |
| 火焰图 | 调用栈聚合 | 可视化全景 | 快速识别热点函数 |
| bpftrace | 内核/用户态动态探测 | 极度灵活、可编程 | 任意函数追踪、参数抓取、事件关联 |
| eBPF + libbpf | 内核可编程 | 生产级性能、CO-RE | 自定义监控工具、长期在线探针 |
| vmstat/iostat/pidstat | 系统全局指标 | 轻量、历史数据 | 瓶颈大类判断、趋势分析、事后复盘 |
面对一个性能问题时,建议遵循"由宏观到微观、由外部到内部"的思路:先用 vmstat、pidstat 确认资源瓶颈类别;若进程层面异常,用 strace 或 ltrace 观察行为;若 CPU 消耗高,用 perf 采样并生成火焰图定位热点;若标准工具无法触及,用 bpftrace 或 eBPF 进行自定义动态追踪。熟练掌握这些工具的组合,能够大幅提升系统调优和问题排查的效率。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。