eBPF 性能诊断实战:Go 服务 sys 60% 的隐藏 fsync 案

Go 服务 CPU 80%、sys 60%,pprof 看不出问题。本文实录 eBPF 排障全过程:火焰图 + syscount + biotop + filetop + funclatency 五步走,定位日志库 fsync 1w/s 的隐藏元凶,附 10 个 SRE 必备 BCC/bpftrace 工具 + Parca/Pixie 落地。

线上一个 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 会拖慢系统
   - 先在测试环境验证

避坑清单

  1. 先用 top + perf top 大致看 CPU 在哪(用户态 vs 内核态)
  2. 火焰图配合 syscount 互相印证
  3. 用 funclatency 看具体函数延迟分布
  4. 用 biotop / tcpretrans 排查 IO / 网络
  5. Go / Rust 应用要保留 frame pointer,符号完整
  6. 生产 trace 采样不要超过 99 Hz
  7. 容器内用 eBPF 要给 SYS_ADMIN 或 BPF + PERFMON
  8. 不会写 bpftrace 没关系,先学 BCC 自带的 100+ 工具
  9. 常驻 profiling 用 Parca / Pyroscope,不要手工跑 perf
  10. 定期升级内核,新特性(BTF / CO-RE)受益巨大

总结

eBPF 是 Linux 近十年最重要的新基础设施之一,把"性能排障"从黑盒变成白盒。这次定位 fsync 太频繁,用 perf 火焰图 + syscount + filetop 三步走,半小时定位问题,以前要花一天。SRE 团队必须有人精通 eBPF,这是云原生时代的基本盘。下一步在团队内培训 BCC + bpftrace,目标是每个 SRE 都能用 eBPF 工具独立排障。eBPF 不会让简单问题更难,但能让原本不可能的问题变可能。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Flink CDC 同步 MySQL 到 Doris 半年踩坑实录:8 大坑全解

2026-5-19 12:11:01

技术教程

K8s CNI 从 Calico 迁 Cilium 一个季度实录:eBPF 替 iptables

2026-5-19 12:15:17

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索