Linux运维17 min read

Linux 性能分析三板斧:strace、perf 与 eBPF

Linux 性能分析三板斧:strace、perf 与 eBPF

为什么写这篇文章

网上讲 Linux 性能工具的文章很多,但大部分是"字典式"的——列出所有参数,配一段说明,看完还是不知道什么时候该用哪个。这篇文章换个思路:用三个真实的故障故事,把三个工具串起来。每个工具解决一类问题,读完你就知道遇到什么场景该掏什么家伙。

先说结论,三把斧的定位:

工具 层级 擅长 代价
strace 系统调用 "这个程序到底在干什么" 性能开销大,适合短时诊断
perf 内核+用户态 "CPU 时间花在哪了" 开销小,适合持续 profiling
eBPF 内核可编程 "我能看到一切" 学习曲线陡,但威力最大

第一斧:strace —— "它到底在干什么"

故事:Python 程序启动卡住 30 秒

线上一个 Python 数据处理服务,每次启动都要卡 30 秒,日志没有任何输出。CPU 和内存都很低,网络也正常。传统的 topnetstat 都看不出问题。

这时候就该 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 -tplsof -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 不是万能的,有几个限制:

  1. 验证器限制:程序不能有无限循环,指令数上限 100 万条
  2. 内核版本依赖:不同内核版本支持的功能不同,5.x 以上体验最好
  3. 需要 root 权限(或 CAP_BPF
  4. 栈回溯不完整:某些场景下只能看到部分调用栈

三把斧的选择决策

遇到性能问题时,按这个顺序选择工具:

性能问题
  │
  ├── 程序行为异常(卡住、超时)
  │     └── 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,最终都建议生成火焰图。一张图胜过千行日志,尤其是给非技术同事看的时候。

分享:

相关文章

评论区