线上一个 Go 服务 CPU 突然飙到 80%,pprof 看不出问题,top 显示是 sys 占用高,而不是用户态。最后用 eBPF 工具链(bpftrace + bcc + perf-tools)定位到是大量 syscall 内核态时间,根因是日志库 fsync 太频繁。本文实录 eBPF 排障的全过程,讲透日常 SRE 必会的 10+ 个 eBPF 工具。
什么是 eBPF
eBPF = extended Berkeley Packet Filter
本质:在内核里跑用户态写的字节码(沙盒)
能力:
- 抓内核函数调用(kprobe / kretprobe)
- 抓用户态函数调用(uprobe / uretprobe)
- 抓 tracepoint(预定义内核事件)
- 抓 perf 事件(CPU 采样)
- 抓 socket / packet(网络)
工具链:
- bcc(Python + C 写脚本,功能强)
- bpftrace(awk 风格,一行命令)
- libbpf(C 库,生产用)
- Pixie / Parca / Pyroscope(产品化)
第一招:火焰图(用户态 + 内核态)
# 安装(Ubuntu)
$ apt install -y linux-tools-common linux-tools-$(uname -r) bpfcc-tools bpftrace
# perf 全局火焰图
$ perf record -F 99 -a -g -p $(pidof myapp) -- sleep 30
$ perf script > out.perf
$ git clone https://github.com/brendangregg/FlameGraph
$ ./FlameGraph/stackcollapse-perf.pl out.perf > out.folded
$ ./FlameGraph/flamegraph.pl out.folded > flame.svg
# 浏览器打开 flame.svg
# 用 BCC 的 profile 工具(更简单)
$ /usr/share/bcc/tools/profile -F 99 -p $(pidof myapp) 30 > out.folded
$ ./FlameGraph/flamegraph.pl out.folded > flame.svg
# 火焰图能看到:
# - 用户态调用链(go 函数)
# - 内核态调用(sys_write / sys_fsync 等)
# 我们这次发现 30% CPU 在 vfs_fsync_range
第二招:syscall 计数(找系统调用大户)
# BCC syscount:统计 syscall 调用次数和耗时
$ /usr/share/bcc/tools/syscount -p $(pidof myapp) -d 10
# 输出:
SYSCALL COUNT
fsync 12500
write 23000
read 19000
nanosleep 8000
futex 15000
# fsync 1 万次/秒,太离谱
# bpftrace 一行命令版
$ bpftrace -e 'tracepoint:syscalls:sys_enter_* /pid == 12345/ { @[probe] = count(); }' -c 'sleep 10'
# 看 syscall 耗时
$ bpftrace -e '
tracepoint:syscalls:sys_enter_* /pid == 12345/ { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_* /@start[tid]/ {
@us[probe] = hist(nsecs - @start[tid]);
delete(@start[tid]);
}' -c 'sleep 10'
第三招:I/O 追踪
# 看哪个进程写了多少 IO
$ /usr/share/bcc/tools/biosnoop
# TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)
# 0.000 mysqld 3421 sda W 1048576 4096 0.32
# 0.001 myapp 12345 sda W 2097152 4096 0.28
# ...
# 看哪些进程/PID 写最多
# 文件级 IO(谁写了哪个文件)
$ /usr/share/bcc/tools/filetop -p $(pidof myapp) 5
# TID COMM READS WRITES R_Kb W_Kb T FILE
# 12345 myapp 0 8500 0 34000 R /var/log/myapp.log
# 12346 myapp 0 1200 0 4800 R /tmp/state.json
# 找到 myapp 8500 次写日志 = fsync 罪魁
第四招:延迟分布
# 文件系统延迟分布
$ /usr/share/bcc/tools/xfsslower 10
$ /usr/share/bcc/tools/ext4slower 10
# bpftrace 自定义延迟统计
$ bpftrace -e '
kprobe:vfs_fsync_range /comm == "myapp"/ { @start[tid] = nsecs; }
kretprobe:vfs_fsync_range /@start[tid]/ {
@ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
END { clear(@start); }
'
# 输出直方图:
@ns:
[16K, 32K) 2 | |
[32K, 64K) 12 |@@ |
[64K, 128K) 48 |@@@@@@@@@ |
[128K, 256K) 268 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[256K, 512K) 180 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[512K, 1M) 50 |@@@@@@@@@@ |
# 平均 fsync 200us,一秒万次 = 2 秒纯 fsync
第五招:TCP 连接追踪
# 看 TCP 连接谁建立的
$ /usr/share/bcc/tools/tcpconnect
# PID COMM IP SADDR DADDR DPORT
# 12345 myapp 4 10.0.1.5 10.0.2.10 3306
# 23456 curl 4 10.0.1.5 151.101.193.69 443
# TCP 重传(网络问题)
$ /usr/share/bcc/tools/tcpretrans
# TIME PID IP LADDR:LPORT T> RADDR:RPORT STATE
# 14:23:11 12345 4 10.0.1.5:42158 R> 10.0.2.10:3306 ESTABLISHED
# TCP RTT 分布
$ bpftrace -e '
kretprobe:tcp_recvmsg { @rtt = hist(((struct sock *)arg0)->sk_rtt_us); }
'
# TCP 连接生命周期
$ /usr/share/bcc/tools/tcplife
# PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS
# 12345 myapp 10.0.1.5 42158 10.0.2.10 3306 2.5 18.3 120
第六招:Go runtime 追踪
# Go goroutine 创建/阻塞
$ bpftrace -e '
uprobe:/path/to/myapp:runtime.newproc1 { @new = count(); }
uprobe:/path/to/myapp:runtime.gopark { @park[stack] = count(); }
'
# Go GC 触发
$ bpftrace -e '
uprobe:/path/to/myapp:runtime.gcStart { @ = count(); printf("GC %d\n", @); }
'
# Go mutex 竞争
$ bpftrace -e '
uprobe:/path/to/myapp:sync.(*Mutex).Lock { @start[tid] = nsecs; }
uretprobe:/path/to/myapp:sync.(*Mutex).Lock /@start[tid]/ {
@lat = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
'
第七招:函数调用统计
# 一个函数被调用多少次
$ bpftrace -e 'uprobe:/path/to/myapp:main.handleRequest { @ = count(); }' -c 'sleep 10'
# 函数耗时分布
$ /usr/share/bcc/tools/funclatency -p $(pidof myapp) 'main.handleRequest' 30
# 内核函数延迟
$ /usr/share/bcc/tools/funclatency 'vfs_*' -d 30
# 缓存命中率
$ /usr/share/bcc/tools/cachestat 1
# HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB
# 5238 1234 420 80.93% 256 14523
我们的诊断过程
# Step 1: top 看是 sys 高
$ top -p $(pidof myapp)
# %CPU: us=20 sy=60 id=20 ← sy 60% 异常
# Step 2: 火焰图,看到内核态 vfs_fsync_range 占 30%
$ perf record -F 99 -p $(pidof myapp) -g -- sleep 30
$ perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
# Step 3: syscount 确认 fsync 1w/s
$ syscount -p $(pidof myapp) -d 10
# Step 4: filetop 找到哪个文件在 fsync
$ filetop -p $(pidof myapp) 5
# /var/log/myapp.log 8500 writes
# Step 5: 排查应用日志库
$ go tool pprof -http=:8080 http://myapp:6060/debug/pprof/profile
# 看到 log.Sync() 调用频率高
# Step 6: 看代码
# 发现日志库每条日志都 fsync(配置写错,SyncEveryWrite = true)
# 应该是 SyncEveryWrite = false + 定期 flush
# 修复:日志同步策略改成 100ms 或 1MB 阈值
log.SetSyncStrategy(log.SyncEvery(100 * time.Millisecond))
修复后
指标 修复前 修复后
==========================
CPU usage 80% 25%
sys time % 60% 5%
fsync/s 10000 100
应用 p99 延迟 250ms 80ms
磁盘 IOPS 12000 1500
日志可靠性 每条不丢 100ms 内不丢
10 个 SRE 必备 eBPF 工具
1. profile — CPU 采样火焰图
2. syscount — syscall 统计
3. biosnoop / biotop — 块设备 IO 追踪
4. filetop — 文件级 IO
5. cachestat — Page Cache 命中
6. tcpconnect/tcplife/tcpretrans — TCP 排查
7. opensnoop — 谁打开了什么文件
8. execsnoop — 谁 fork/exec 了什么
9. funclatency — 函数延迟分布
10. offcputime — 进程被调度走的原因
进阶工具:
- bcc/tools/sslsniff — TLS 解密(本地 SSL_read)
- bcc/tools/dbslower — MySQL/PG 慢查询(uprobe libpq/libmysqlclient)
- bcc/tools/gethostlatency — DNS 解析延迟
把 eBPF 落地生产
# 1. Parca:持续 profiling
$ kubectl apply -f https://github.com/parca-dev/parca/releases/latest/download/kubernetes-manifest.yaml
# Parca agent 用 eBPF 持续采样,所有进程的火焰图都能看
# 2. Pixie:K8s 全栈观测
$ px deploy
# 自动抓 HTTP / MySQL / Redis / DNS / gRPC,无需埋点
# 3. Cilium Hubble:服务网络观测
$ hubble observe --service myapp
# 实时看 myapp 的入站出站连接
# 4. Tracee:运行时安全
# 监控可疑 syscall(execve / mount / 提权)
注意事项
1. 内核版本要求:
- 基本功能:4.9+
- CO-RE(便携):5.4+
- 完整功能:5.10+ 推荐
2. 性能开销:
- 大部分工具开销 < 1% CPU
- 但全函数 trace(perf -F 999)开销大
- 生产采样 -F 49 或 99 即可
3. 权限:
- 需要 root(或 CAP_BPF + CAP_PERFMON)
- 容器内运行需要 privileged 或专门 capability
4. 内核符号:
- kprobe 需要 CONFIG_KPROBES + /proc/kallsyms
- uprobe 需要二进制有符号(strip 后用不了)
- Go 1.21+ 默认有 frame pointer,栈更准
5. 不要在生产乱用 trace:
- 高频函数 trace 会拖慢系统
- 先在测试环境验证
避坑清单
- 先用 top + perf top 大致看 CPU 在哪(用户态 vs 内核态)
- 火焰图配合 syscount 互相印证
- 用 funclatency 看具体函数延迟分布
- 用 biotop / tcpretrans 排查 IO / 网络
- Go / Rust 应用要保留 frame pointer,符号完整
- 生产 trace 采样不要超过 99 Hz
- 容器内用 eBPF 要给 SYS_ADMIN 或 BPF + PERFMON
- 不会写 bpftrace 没关系,先学 BCC 自带的 100+ 工具
- 常驻 profiling 用 Parca / Pyroscope,不要手工跑 perf
- 定期升级内核,新特性(BTF / CO-RE)受益巨大
总结
eBPF 是 Linux 近十年最重要的新基础设施之一,把"性能排障"从黑盒变成白盒。这次定位 fsync 太频繁,用 perf 火焰图 + syscount + filetop 三步走,半小时定位问题,以前要花一天。SRE 团队必须有人精通 eBPF,这是云原生时代的基本盘。下一步在团队内培训 BCC + bpftrace,目标是每个 SRE 都能用 eBPF 工具独立排障。eBPF 不会让简单问题更难,但能让原本不可能的问题变可能。
—— 别看了 · 2026