系统性能诊断与调优:strace、perf、bpftrace

性能问题往往没有统一的标准答案,但 Linux 提供了一套完整的工具链,可以从不同层面揭示系统的运行状况。本文系统介绍从系统调用跟踪到内核动态追踪的常用工具,帮助开发者在面对高 CPU、高延迟或资源瓶颈时快速定位根因。strace:系统调用跟踪 strace 是最基础也是使用率最高的诊断工具之一。

性能问题往往没有统一的标准答案,但 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

典型应用场景

  1. 程序卡死:strace 停在某个调用上不动,通常是网络 I/O 或等待锁
  2. 查找配置文件读取路径:用 -e trace=openat 可以一目了然
  3. 追踪失败的系统调用:不加过滤时,返回值中的 -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_opentracepoint:syscalls:sys_enter_readuprobe:/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 的核心执行流程可以概括为四步:

  1. 用户用 C 语言(或 bpftrace 等高级语言)编写程序
  2. 编译为 eBPF 字节码后加载入内核
  3. 内核验证器进行静态安全检查
  4. 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__openbpf_object__loadbpf_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库函数调用聚焦用户态库库函数级热点或阻塞分析
perfPMU 硬件事件低开销、高精度CPU 消耗分析、缓存分析、热点定位
火焰图调用栈聚合可视化全景快速识别热点函数
bpftrace内核/用户态动态探测极度灵活、可编程任意函数追踪、参数抓取、事件关联
eBPF + libbpf内核可编程生产级性能、CO-RE自定义监控工具、长期在线探针
vmstat/iostat/pidstat系统全局指标轻量、历史数据瓶颈大类判断、趋势分析、事后复盘

面对一个性能问题时,建议遵循"由宏观到微观、由外部到内部"的思路:先用 vmstatpidstat 确认资源瓶颈类别;若进程层面异常,用 straceltrace 观察行为;若 CPU 消耗高,用 perf 采样并生成火焰图定位热点;若标准工具无法触及,用 bpftrace 或 eBPF 进行自定义动态追踪。熟练掌握这些工具的组合,能够大幅提升系统调优和问题排查的效率。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. 进程与线程:从 PCB 到内核调度实体
  2. 虚拟内存与分页机制:从 MMU 到 TLB
  3. 现代高性能 IO:epoll、io_uring 与异步 IO