Linux 性能分析三板斧:strace、perf 与 eBPF
为什么写这篇文章
网上讲 Linux 性能工具的文章很多,但大部分是"字典式"的——列出所有参数,配一段说明,看完还是不知道什么时候该用哪个。这篇文章换个思路:用三个真实的故障故事,把三个工具串起来。每个工具解决一类问题,读完你就知道遇到什么场景该掏什么家伙。
先说结论,三把斧的定位:
| 工具 | 层级 | 擅长 | 代价 |
|---|---|---|---|
| strace | 系统调用 | "这个程序到底在干什么" | 性能开销大,适合短时诊断 |
| perf | 内核+用户态 | "CPU 时间花在哪了" | 开销小,适合持续 profiling |
| eBPF | 内核可编程 | "我能看到一切" | 学习曲线陡,但威力最大 |
第一斧:strace —— "它到底在干什么"
故事:Python 程序启动卡住 30 秒
线上一个 Python 数据处理服务,每次启动都要卡 30 秒,日志没有任何输出。CPU 和内存都很低,网络也正常。传统的 top、netstat 都看不出问题。
这时候就该 strace 上场了。
基本用法
# 追踪进程的系统调用
strace -p <PID>
# 追踪整个命令的执行
strace python3 /opt/app/main.py
# 只看特定类型的系统调用
strace -e trace=network python3 /opt/app/main.py
strace -e trace=file python3 /opt/app/main.py
strace -e trace=connect,recvfrom python3 /opt/app/main.py
# 统计系统调用次数和耗时
strace -c -p <PID>
排查那个 30 秒问题
# 追踪程序启动过程,关注网络相关调用
strace -e trace=network,connect -f -tt python3 /opt/app/main.py
-f 表示追踪子进程,-tt 输出微秒级时间戳。输出中找到关键线索:
14:23:01.123456 connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.0.2")}, 16) = -1 EINPROGRESS (Operation now in progress)
14:23:01.123789 poll([{fd=5, events=POLLOUT}], 1, 5000) = 1 ([{fd=5, revents=POLLOUT}])
14:23:01.124012 getsockopt(5, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
14:23:01.124234 connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.0.2")}, 16) = 0
14:23:01.124567 sendto(5, "\x00\x01...", 34, 0, NULL, 0) = 34
14:23:01.124890 poll([{fd=5, events=POLLIN}], 1, 5000
14:23:31.124890 <... poll resumed>) = 0 (Timeout)
看到了——poll 调用在等待 DNS 响应,等了整整 30 秒超时。问题出在 DNS 解析。
进一步查看 /etc/resolv.conf,发现配置了两个 DNS 服务器,第一个不可达,第二个才响应。Python 的 socket 模块对 DNS 超时处理不好,导致每次启动都等第一个 DNS 超时。
修复:调整 DNS 服务器顺序 + 设置合理的 timeout。
strace 高级技巧
# 只统计,不输出每次调用(适合宏观分析)
strace -c -p <PID>
# 输出示例:
# % time seconds usecs/call calls errors syscall
# ------ ----------- ----------- --------- --------- ----------------
# 65.00 0.650000 65 10000 futex
# 20.00 0.200000 20 10000 read
# 5.00 0.050000 50 1000 write
# ...
# 过滤特定文件描述符
strace -e trace=read,write -e fd=5 -p <PID>
# 输出每个系统调用的耗时
strace -T -p <PID>
# 只看耗时超过 1ms 的调用
strace -e trace=all -T -p <PID> 2>&1 | awk '$NF ~ /[0-9]+s$/ && substr($NF,1,length($NF)-1)+0 > 0.001'
# 保存到文件分析
strace -o /tmp/strace.log -f -tt python3 /opt/app/main.py
什么时候不该用 strace
strace 使用 ptrace 机制,每个系统调用都会被拦截两次(进入和退出),性能开销通常在 20%-200%。生产环境对高负载服务长时间挂 strace 是危险的操作。
替代方案:
- 如果只是想看"程序在读什么文件"→ 用
lsof -p <PID>或strace -e trace=open,openat - 如果想看网络连接 → 用
ss -tp或lsof -i - 如果想分析性能瓶颈 → 用
perf,开销小得多
第二斧:perf —— "CPU 时间花在哪了"
故事:Java 服务 CPU 飙到 800%
一个 Java 微服务在流量高峰期 CPU 使用率飙到 800%(8 核机器),但响应延迟没有明显增加。GC 日志看起来正常,JMX 线程 dump 也没发现死循环。问题到底出在哪?
perf 速查
# 安装(大多数发行版)
# CentOS/RHEL
yum install perf
# Ubuntu/Debian
apt install linux-tools-common linux-tools-$(uname -r)
# 快速概览:CPU 占用最高的函数
perf top
perf top 类似 top,但显示的是函数级别的 CPU 占用,而不是进程级别。
排查那个 CPU 飙高问题
# 第一步:采样 30 秒的 CPU 数据
perf record -F 99 -p <PID> -g -- sleep 30
# 参数说明:
# -F 99 采样频率 99Hz(避免与某些周期性事件共振)
# -p PID 指定进程
# -g 记录调用栈
# -- sleep 30 采样30秒后自动停止
# 第二步:分析报告
perf report
在 perf report 中发现,CPU 时间排在最前面的是一个 JSON 序列化库的方法:
38.12% java libjackson-core.so [.] _json_encode_string
15.44% java libjackson-core.so [.] _json_decode_string
8.20% java libjvm.so [.] G1MergeHeapRoots
5.10% java libjvm.so [.] Interpreter
Jackson JSON 序列化占了 53% 的 CPU。进一步排查发现,有个接口返回了一个巨大的嵌套对象,每次请求都要序列化 2MB 的 JSON。加了一层缓存后,CPU 降到 200%。
perf 常用场景
场景一:找出 CPU 占用最高的函数
# 全局采样(所有进程)
perf record -F 99 -ag -- sleep 10
perf report
# 火焰图(需要安装 FlameGraph 工具)
perf record -F 99 -ag -- sleep 30
perf script | /opt/FlameGraph/stackcollapse-perf.pl | /opt/FlameGraph/flamegraph.pl > /tmp/flame.svg
火焰图是最直观的性能分析可视化工具。SVG 文件可以在浏览器中打开,宽度代表 CPU 占用比例,点击可以放大。
场景二:分析上下文切换
# 记录调度事件
perf record -e sched:sched_switch -ag -- sleep 10
perf report
# 或者用 perf stat 看汇总
perf stat -e context-switches,cs -p <PID> -- sleep 10
如果上下文切换频率异常高(每秒上万次),通常意味着线程数过多或有锁竞争。
场景三:分析缓存命中率
# CPU 缓存命中率
perf stat -e cache-misses,cache-references -p <PID> -- sleep 10
# 输出示例:
# 1,234,567 cache-misses # 缓存未命中
# 45,678,901 cache-references # 缓存访问总数
# 命中率 = 1 - 1234567/45678901 = 97.3%
缓存命中率低于 90% 通常意味着数据访问模式不友好(比如随机访问大数组),可以考虑优化数据结构。
场景四:分析锁竞争
# 记录锁相关事件
perf record -e syscalls:sys_enter_futex,syscalls:sys_exit_futex -ag -- sleep 10
perf report
futex 调用频繁通常意味着多线程锁竞争严重。
perf 的事件体系
# 列出所有可用事件
perf list
# 常用硬件事件
perf list hw
# cpu-cycles CPU周期数
# instructions 指令数
# cache-references 缓存访问
# cache-misses 缓存未命中
# branch-instructions 分支指令
# branch-misses 分支预测失败
# 常用软件事件
perf list sw
# context-switches 上下文切换
# page-faults 缺页中断
# task-clock 任务运行时间
实用技巧
# 限制采样时长,避免无限运行
perf record -F 99 -p <PID> -g -- sleep 30
# 限制采样大小
perf record -F 99 -p <PID> -g -- sleep 30 -c 10000000
# 只采样用户态代码(排除内核态)
perf record -F 99 -p <PID> -g -u -- sleep 30
# 只采样内核态
perf record -F 99 -p <PID> -g -k -- sleep 30
注意:如果看到 [unknown] 函数名,说明缺少调试符号。安装对应包:
# CentOS
debuginfo-install java-17-openjdk
# 或安装内核调试符号
yum install kernel-debuginfo
第三斧:eBPF —— "我能看到一切"
故事:网络延迟突增排查
一个微服务集群中,某台机器到数据库的延迟突然从 2ms 涨到 50ms。ping 正常,traceroute 正常,应用日志也没有异常。问题出在网络协议栈的哪一层?
传统工具到此为止了。eBPF 可以深入到内核的每一个 hook 点,按需采集数据。
eBPF 是什么
eBPF(Extended Berkeley Packet Filter)是 Linux 内核的可编程虚拟机。你可以在内核中安全地运行沙箱程序,挂钩到各种事件点上——系统调用、网络包、内核函数进出、跟踪点等——而不需要修改内核源码或加载内核模块。
用户态程序 (C/Python/Go)
↓
bpf() 系统调用
↓
┌─────────────────────────┐
│ eBPF 虚拟机 (内核) │
│ ┌───────┐ ┌──────────┐ │
│ │验证器 │ │ JIT 编译 │ │
│ └────---┘ └────────--┘ │
│ ↓ │
│ 挂载点: kprobes/tracepoints/
│ perf_events/socket/xdp
└─────────────────────────┘
快速上手:bpftrace
直接写 eBPF C 代码门槛太高,bpftrace 提供了类似 awk 的高级语言,一上手就能用:
# 安装
# Ubuntu
apt install bpftrace
# CentOS 8+
dnf install bpftrace
# 一行命令统计系统调用
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); } interval:s:5 { exit(); }'
# 统计每个进程的 read 字节数
bpftrace -e 'tracepoint:syscalls:sys_enter_read /comm != "bpftrace"/ { @start[tid] = nsecs; } tracepoint:syscalls:sys_exit_read /@start[tid]/ { @bytes[comm] = sum(arg2); delete(@start[tid]); }'
# 追踪 TCP 连接延迟
bpftrace -e 'kprobe:tcp_v4_connect { @start[tid] = nsecs; } kretprobe:tcp_v4_connect /@start[tid]/ { @latency_ms = hist((nsecs - @start[tid]) / 1000000); delete(@start[tid]); }'
排查那个网络延迟问题
用 eBPF 追踪 TCP 各层耗时:
# 追踪 TCP 握手延迟
bpftrace -e '
kprobe:tcp_v4_connect { @start[tid] = nsecs; }
kretprobe:tcp_v4_connect /@start[tid]/ {
@handshake_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 追踪 TCP 接收延迟(从内核收到包到应用读取)
bpftrace -e '
tracepoint:tcp:tcp_probe { @recv_us = hist(arg1 / 1000); }'
# 追踪网络包在内核协议栈各层的处理耗时
bpftrace -e '
kprobe:netif_receive_skb { @start[arg0] = nsecs; }
kprobe:ip_rcv /@start[arg0]/ { @ip_rcv_ns = hist(nsecs - @start[arg0]); }
kprobe:tcp_v4_rcv /@start[arg0]/ { @tcp_rcv_ns = hist(nsecs - @start[arg0]); }
kretprobe:netif_receive_skb /@start[arg0]/ { @total_ns = hist(nsecs - @start[arg0]); delete(@start[arg0]); }'
最终发现是 tcp_v4_rcv 这一层耗时异常——进一步排查是 nf_conntrack 模块在高并发下性能下降。清空连接跟踪表 + 调大 nf_conntrack_max 后恢复。
BCC 工具集
BCC(BPF Compiler Collection)是 eBPF 的工具集,提供了一系列开箱即用的运维工具:
# 安装
# Ubuntu
apt install bpfcc-tools
# CentOS
dnf install bcc-tools
常用 BCC 工具
# 1. biolatency - 块设备 IO 延迟分布
biolatency-bpfcc 10
# 显示磁盘 IO 延迟的直方图
# 2. biosnoop - 追踪每次 IO 请求
biosnoop-bpfcc
# 3. tcplife - 追踪 TCP 连接生命周期
tcplife-bpfcc
# 4. tcptracer - 追踪 TCP 连接建立/关闭
tcptracer-bpfcc
# 5. execsnoop - 追踪新进程创建(找短命进程)
execsnoop-bpfcc
# 6. opensnoop - 追踪文件打开(找配置文件读取问题)
opensnoop-bpfcc -p <PID>
# 7. runqlat - CPU 调度延迟
runqlat-bpfcc 10
# 8. offcputime - off-CPU 分析(线程不占CPU时在等什么)
offcputime-bpfcc -p <PID> 10
# 9. mallocstacks - 内存分配栈追踪
# (需要符号信息)
# 10. killsnoop - 追踪 kill 信号
killsnoop-bpfcc
off-CPU 分析:找等待瓶颈
perf 擅长分析 on-CPU(CPU 在执行什么),但很多性能问题是 off-CPU 的——线程在等待锁、IO、网络。eBPF 的 offcputime 可以分析这个问题:
# 分析进程的 off-CPU 时间
offcputime-bpfcc -p <PID> 30
# 输出示例:
# 1234 ms myapp [unknown] [unknown]
# 567 ms myapp read do_syscall_64 __x64_sys_read ...
# 234 ms myapp futex do_syscall_64 __x64_sys_futex ...
如果 off-CPU 时间主要花在 futex 上,说明是锁竞争;花在 read 上,说明是 IO 等待。
自定义追踪:一个完整的 bpftrace 脚本
#!/usr/bin/env bpftrace
// trace-slow-io.bt: 追踪超过 10ms 的 IO 请求
BEGIN {
printf("Tracing slow IO (> 10ms)... Ctrl-C to stop.\n");
}
// 记录 IO 请求开始时间
kprobe:blk_start_request
/comm == "myapp"/
{
@start[arg0] = nsecs;
}
// IO 完成时计算耗时
kprobe:blk_update_request
/@start[arg0]/
{
$delta = (nsecs - @start[arg0]) / 1000000; // 转为毫秒
if ($delta > 10) {
printf("%-8s %-16s %7d ms\n", strftime("%H:%M:%S"), comm, $delta);
}
delete(@start[arg0]);
}
END {
clear(@start);
printf("\nDone.\n");
}
运行:bpftrace trace-slow-io.bt
eBPF 的安全边界
eBPF 不是万能的,有几个限制:
- 验证器限制:程序不能有无限循环,指令数上限 100 万条
- 内核版本依赖:不同内核版本支持的功能不同,5.x 以上体验最好
- 需要 root 权限(或
CAP_BPF) - 栈回溯不完整:某些场景下只能看到部分调用栈
三把斧的选择决策
遇到性能问题时,按这个顺序选择工具:
性能问题
│
├── 程序行为异常(卡住、超时)
│ └── strace 看系统调用
│
├── CPU 占用高
│ └── perf top / perf record 看函数热点
│
├── 延迟高但 CPU 不高(IO/锁/网络等待)
│ └── eBPF offcputime 看等待点
│
├── 网络延迟/丢包
│ └── eBPF tcplife / tcpdump 看协议栈
│
├── 磁盘 IO 慢
│ └── eBPF biolatency / iostat
│
└── 不知道什么问题
└── 先 perf stat 看全局,再下钻
性能分析的基本原则
最后总结几条原则,比工具本身更重要:
1. 先有假设,再用工具验证
不要上来就跑一堆命令。先问自己:问题最可能出在哪一层?然后选对应的工具去验证。如果验证不成立,再扩大范围。
2. 量化比定性重要
"系统很慢"是定性描述,"P99 延迟从 5ms 涨到 50ms"是量化描述。没有基线数据就无法判断异常程度。建议在系统正常时就采集基线数据。
3. 生产环境注意开销
- strace:开销 20%-200%,对高负载服务慎用
- perf:采样模式下开销 < 1%,生产可用
- eBPF:取决于程序复杂度,通常 < 5%
4. 一个工具看不全,组合使用
很多问题需要多个工具配合。比如"服务变慢"可能是 CPU 瓶颈(perf),也可能是锁竞争(eBPF offcputime),也可能是 IO 等待(iostat + eBPF biolatency)。不要指望一个工具解决所有问题。
5. 火焰图是最直观的可视化
不管用 perf 还是 eBPF,最终都建议生成火焰图。一张图胜过千行日志,尤其是给非技术同事看的时候。

