1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com

上一篇我们刚学完 SystemTap。SystemTap 有一个很厉害的能力:

不改应用代码,也不用重新编译内核,就可以给 Linux 内核和系统行为“装探针”。

但是学到这里,大家可能也会有一个问题:

现在 Linux 性能分析,为什么越来越多人开始讲 eBPF?

其实原因很简单。SystemTap 的工作方式,是把脚本转换成 C 代码,再编译成内核模块,最后加载到内核里运行。

而 eBPF 采用的是另一套思路:

让一段经过内核 verifier 检查的程序进入内核,在受约束的执行环境中完成追踪和数据采集。

所以相比直接加载自定义内核模块,eBPF 在安全边界、动态追踪以及生产环境使用方面具有明显优势。而且更重要的是:

你根本不需要一上来就学 eBPF C 代码。

因为已经有 BCC、bpftrace 这些工具帮我们把复杂的东西封装起来。这篇文章,我们就先不钻 eBPF 源码。先站在运维工程师的角度,看看:

eBPF 到底能帮我们解决什么问题?


eBPF概述

eBPF 是什么?

第一次接触 eBPF,很多人看到各种文章之后,第一反应可能是:

“又来了一个特别复杂的 Linux 内核技术……”

其实不用想得那么复杂。如果把 Linux 内核理解成一座正在高速运行的工厂,那么以前我们想知道工厂里面发生了什么,通常只能通过现有的监控接口去观察。

例如:

1
2
3
4
5
6
7
top
iostat
vmstat
ss
sar
perf
strace

这些工具已经非常强大。但是,当你想问一些更加具体的问题时,比如:

  • 到底是谁频繁创建进程?
  • 哪个程序一直在打开某个文件?
  • 某个磁盘 IO 到底慢在哪里?
  • 某个进程为什么一直等待 CPU?
  • DNS 解析到底花了多少时间?
  • 某个内核函数到底被调用了多少次?

传统工具有时候就不够用了。这时候,eBPF 就派上用场了。

eBPF 到底干什么?

eBPF 可以让我们把一段程序加载到 Linux 内核中,并把它挂到特定的事件或 hook 上。当事件发生的时候,eBPF 程序就会被执行。例如:

1
2
3
4
5
6
7
8
9
10
11
进程创建
↓
触发对应内核事件
↓
eBPF 程序执行
↓
记录 PID / PPID / 命令等信息
↓
用户态工具读取数据
↓
最终显示给我们

所以它非常适合做:

  • 性能分析;
  • 内核追踪;
  • 网络分析;
  • 文件系统分析;
  • IO 分析;
  • 进程行为分析;
  • 容器和 cgroup 相关观测。

老李也把 bpftrace 和 BCC 定位为用于分析 Linux 系统性能、收集其他接口难以获取的信息的重要工具。


eBPF 为什么比传统内核模块更安全?

这里正好可以和上一篇的 SystemTap 对比一下。

  1. SystemTap 最终会把脚本转换成 C 代码,然后编译成内核模块。

  2. 而 eBPF 程序在加载到内核之前,会经过 BPF verifier 等机制检查。

比如:

  • 程序执行路径是否满足约束;
  • 内存访问是否安全;
  • 是否存在越界访问;
  • 使用的 helper 是否允许;
  • 对 map 等对象的访问是否符合规则。

如果程序无法通过 verifier,通常就不会被加载。

所以可以简单理解成:

SystemTap 更像是生成一个内核模块再加载进去;eBPF 更像是把一段受约束、经过检查的小程序交给内核运行。

当然,这里也不能把 eBPF 理解成“绝对不会出问题”。eBPF、内核以及相关工具本身仍然可能存在 bug。更准确的说法是:

普通 eBPF 程序受到 verifier 和运行环境的约束,相比直接加载任意内核模块,安全边界更强,因此更适合现代 Linux 的动态追踪和生产环境观测。


eBPF 可以挂在哪里?

既然 eBPF 可以观察内核事件,那么问题来了:

到底观察什么?

这就涉及 eBPF 常见的探测点。比较常见的有:

探测类型主要用途
kprobe动态探测内核函数
kretprobe动态探测内核函数返回
tracepoint使用内核预定义的静态追踪点
perf event性能事件采样
uprobe探测用户态函数
uretprobe探测用户态函数返回
cgroup针对 cgroup / 容器相关行为进行观测
socket网络相关事件

这里大家不用急着把这些全部记住。现在只需要建立一个概念:

eBPF 本身不是一个单独的“监控命令”,而是一套可以挂到各种内核、用户态事件上的可编程机制。

比如:

flowchart LR
    A[eBPF程序] --> B[探测点]
    B --> C[kprobe]
    B --> D[tracepoint]
    B --> E[uprobe]
    B --> F[perf event]
    B --> G[cgroup]
    B --> H[socket]
    C --> I[采集事件]
    D --> I
    E --> I
    F --> I
    G --> I
    H --> I

eBPF 和 SystemTap 到底有什么区别?

上一篇我们已经学过 SystemTap,所以这里把两个放在一起看最容易理解。

对比项SystemTapeBPF
核心方式脚本转换并编译成内核模块加载 eBPF 程序到内核
安全机制SystemTap translator 提供约束,但内核模块能力较强verifier 等机制对程序进行检查
追踪能力很强很强
动态追踪支持支持
用户态追踪支持支持
生产环境需要谨慎现代 Linux 中应用越来越广泛
学习方式SystemTap 脚本BCC / bpftrace / libbpf 等
常见用途传统动态追踪、复杂系统分析现代性能分析、可观测性、网络和内核追踪

这里最容易产生一个误区:

是不是 eBPF 出现以后,SystemTap 就没用了?

也不能这么理解。SystemTap 仍然可以用于一些传统 Linux 环境和特定的复杂追踪场景。只是对于现代 Linux 系统,尤其是生产环境性能分析、网络观测、容器观测等场景,eBPF 已经成为非常重要的一条技术路线。所以更准确的理解是:

不是 SystemTap “淘汰”了,而是 eBPF 正逐渐成为现代 Linux 动态追踪的重要基础设施。


BCC 和 bpftrace 又是什么?

到这里,新的问题来了。我们刚才说的是 eBPF。那为什么又冒出来:

1
2
BCC
bpftrace

其实很好理解。

eBPF 是底层技术,BCC 和 bpftrace 是帮助我们使用 eBPF 的工具。

可以把它们理解成:

1
2
3
4
5
6
7
               eBPF
│
┌───────────┴───────────┐
↓ ↓
BCC bpftrace
↓ ↓
大量现成工具 自定义追踪脚本

BCC:直接拿现成工具来用

BCC 全称:

BPF Compiler Collection

它提供了一套基于 eBPF 的工具和开发框架。对于运维人员来说,最吸引人的地方其实不是它的名字,而是:

已经有人把很多常见的性能排查场景做成工具了。

例如:

1
2
3
4
5
6
7
8
9
10
11
execsnoop
opensnoop
biolatency
biosnoop
biotop
cachestat
runqlat
tcpconnect
tcpaccept
xfsslower
gethostlatency

所以你遇到问题的时候,不需要自己从零开始写 eBPF。

比如:

“我想看看服务器现在谁在不断创建进程。”

不用写 eBPF。

直接:

1
/usr/share/bcc/tools/execsnoop

就可以开始观察。

安装 bcc-tools 后,这些工具位于:

1
/usr/share/bcc/tools/

建议直接查看这个目录以及其中的 doc 子目录。


bpftrace:自己写一点简单的追踪逻辑

如果说 BCC 是:

工具箱

那么 bpftrace 更像:

一门专门用来快速写追踪脚本的小语言。

它的语法比较简洁,特别适合:

  • 快速验证一个想法;
  • 临时排查一个问题;
  • 写一条 one-liner;
  • 写一个简单的自定义追踪脚本。

例如:

1
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args.filename)); }'

这条命令可以实时观察进程打开文件的行为。

所以:

BCC 更像“拿工具直接干活”,bpftrace 更像“自己写几行脚本解决特殊问题”。


安装 BCC 和 bpftrace

安装 BCC:

1
[root@localhost ~]# dnf install bcc-tools -y

安装 bpftrace:

1
[root@localhost ~]# dnf install bpftrace -y

也可以一次安装:

1
[root@localhost ~]# dnf install bcc-tools bpftrace -y

安装完成以后,可以分别看看:

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# ls /usr/share/bcc/tools/
argdist execsnoop mountsnoop rdmaucma tcpcong
bashreadline exitsnoop mptcpify readahead tcpconnect
bindsnoop ext4dist mysqld_qslower reset-trace tcpconnlat
biolatency ext4slower netqtop rubycalls tcpdrop
biolatpcts filegone netqtop.c rubyflow tcplife
biopattern filelife nfsdist rubygc tcpretrans
biosnoop fileslower nfsslower rubyobjnew tcprtt
biotop filetop nodegc rubystat tcpstates
bitesize funccount nodestat runqlat tcpsubnet
...

以及:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# ls /usr/share/bpftrace/tools/
bashreadline.bt killsnoop.bt ssllatency.bt tcpretrans.bt
biolatency.bt loads.bt sslsnoop.bt tcpsynbl.bt
biosnoop.bt mdflush.bt statsnoop.bt threadsnoop.bt
biostacks.bt naptime.bt swapin.bt undump.bt
bitesize.bt oomkill.bt syncsnoop.bt vfscount.bt
capable.bt opensnoop.bt syscount.bt vfsstat.bt
cpuwalk.bt pidpersec.bt tcpaccept.bt writeback.bt
dcsnoop.bt runqlat.bt tcpconnect.bt xfsdist.bt
execsnoop.bt runqlen.bt tcpdrop.bt
gethostlatency.bt setuids.bt tcplife.bt

BCC 工具位于:

1
/usr/share/bcc/tools/

而bpftrace 示例脚本位于:

1
/usr/share/bpftrace/tools/

execsnoop:看看系统里谁在创建进程

我们先从一个非常直观的工具开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@localhost ~]# /usr/share/bcc/tools/execsnoop
COMM PID PPID RET ARGS
head 23018 1173 0 /usr/bin/head -v -n 8 /proc/meminfo
head 23019 1173 0 /usr/bin/head -v -n 2 /proc/stat /proc/version /proc/uptime /proc/loadavg /proc/sys/fs/file-nr /proc/sys/kernel/hostname
tail 23020 1173 0 /usr/bin/tail -v -n 32 /proc/net/dev
df 23021 1173 0 /usr/bin/df -l
who 23022 1173 0 /usr/bin/who
sleep 23023 1173 0 /usr/bin/sleep 1
head 23024 1173 0 /usr/bin/head -v -n 8 /proc/meminfo
head 23025 1173 0 /usr/bin/head -v -n 2 /proc/stat /proc/version /proc/uptime /proc/loadavg /proc/sys/fs/file-nr /proc/sys/kernel/hostname
tail 23026 1173 0 /usr/bin/tail -v -n 32 /proc/net/dev
df 23027 1173 0 /usr/bin/df -l
who 23028 1173 0 /usr/bin/who
sleep 23029 1173 0 /usr/bin/sleep 1

execsnoop 用来观察系统中的新进程执行事件。

它特别适合排查:

  • 谁在不断启动进程?
  • 某个短命进程到底执行了什么?
  • 定时任务到底在执行什么?
  • 某个脚本是不是被反复执行?
  • 系统里有没有出现异常命令?

例如运行:

1
/usr/share/bcc/tools/execsnoop

然后在另一个终端执行:

1
ls

你可能看到类似:

1
2
PCOMM   PID    PPID   RET  ARGS
ls 12345 12344 0 /usr/bin/ls --color=auto

几个重要字段:

字段含义
PCOMM父进程命令名
PID进程 ID
PPID父进程 ID
RETexec() 系统调用返回值
ARGS执行程序及参数

这里大家注意一个细节:

execsnoop 更准确地说是在观察程序执行/exec 事件,而不是简单等同于“进程创建”。因为 Linux 创建进程和执行新程序是两个不同层面的动作。

这个区别在学习 fork()、clone()、execve() 的时候会非常重要。


opensnoop:程序到底打开了什么文件?

再来看一个特别实用的工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# /usr/share/bcc/tools/opensnoop
PID COMM FD ERR PATH
23540 head 3 0 /etc/ld.so.cache
23540 head 3 0 /lib64/libc.so.6
23540 head 3 0 /usr/lib/locale/locale-archive
23540 head 3 0 /usr/share/locale/locale.alias
23540 head -1 2 /usr/share/locale/en_US.UTF-8/LC_MESSAGES/coreutils.mo
23540 head -1 2 /usr/share/locale/en_US.utf8/LC_MESSAGES/coreutils.mo
23540 head -1 2 /usr/share/locale/en_US/LC_MESSAGES/coreutils.mo
23540 head -1 2 /usr/share/locale/en.UTF-8/LC_MESSAGES/coreutils.mo
23540 head -1 2 /usr/share/locale/en.utf8/LC_MESSAGES/coreutils.mo
23540 head -1 2 /usr/share/locale/en/LC_MESSAGES/coreutils.mo
23540 head 3 0 /proc/meminfo

它可以观察文件打开事件。

可以重点关注:

1
ERR = 0

表示打开成功。

如果出现非 0 值,就需要进一步关注对应错误。

比如:

1
ERR = 2

通常对应:

1
ENOENT

也就是:

文件或目录不存在。

这个时候排查配置文件问题就特别直观。比如应用启动时报:

1
config.yml not found

你可能会想:

“我明明把配置文件放在那里了啊?”

不要猜。

直接:

1
/usr/share/bcc/tools/opensnoop

看看程序实际尝试打开的到底是哪一个路径。


biolatency:磁盘 IO 到底有多慢?

前面我们学习过 iostat。iostat 可以告诉我们:

磁盘现在忙不忙。

但是有时候我们真正想知道的是:每一次 IO 的延迟到底是什么分布?

这时候就可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# /usr/share/bcc/tools/biolatency
Tracing block device I/O... Hit Ctrl-C to end.
^C
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 0 | |
128 -> 255 : 1 |****************************************|

这里最重要的是理解:

1
延迟区间 → 有多少 IO 落在这里

而不是死记某个数字。比如你发现:

1
2
大多数 IO:10~50us
少部分 IO:几百 us

下一步应该问:

这是不是正常?

答案不能脱离环境直接下结论。

因为:

  • 本地 NVMe;
  • SATA SSD;
  • 云盘;
  • SAN;
  • 虚拟机磁盘;
  • 容器存储;

它们的正常延迟范围本来就可能不同。所以 biolatency 最重要的价值,是帮你看到:

IO 延迟的分布和长尾。


biosnoop:不要只看“磁盘慢”,直接看到每一条 IO

biolatency 告诉我们:

延迟分布怎么样。

那如果我还想知道:

到底是谁在产生这些 IO?

可以进一步使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# /usr/share/bcc/tools/biosnoop
TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)
0.000000 auditd 949 nvme0n1 W 262252608 4096 0.17
0.000004 auditd 949 nvme0n1 W 262252616 16384 0.15
0.001620 auditd 949 nvme0n1 W 518428942 11776 0.12
0.113958 kworker/1:37 22521 nvme0n1 W 518428965 1024 0.18
0.114830 xfsaild/dm-0 754 nvme0n1 W 4200450 512 0.77
0.114835 xfsaild/dm-0 754 nvme0n1 W 4200480 4096 0.75
0.114838 xfsaild/dm-0 754 nvme0n1 W 4206848 16384 0.75
0.114839 xfsaild/dm-0 754 nvme0n1 W 4402224 4096 0.75
0.114842 xfsaild/dm-0 754 nvme0n1 W 4528576 16384 0.75
0.114844 xfsaild/dm-0 754 nvme0n1 W 8610112 16384 0.76
0.114845 xfsaild/dm-0 754 nvme0n1 W 261181441 512 0.76
0.114847 xfsaild/dm-0 754 nvme0n1 W 261181442 512 0.76
0.114850 xfsaild/dm-0 754 nvme0n1 W 261181448 20480 0.76
0.114851 xfsaild/dm-0 754 nvme0n1 W 261181568 16384 0.76

它会把块设备 IO 请求逐条显示出来。

重点看:

字段含义
COMM进程名
PID进程 ID
DISK磁盘设备
TIO 类型
SECTOR起始扇区
BYTESIO 大小
LAT(ms)IO 延迟

比如:

1
mysqld

不断出现,而且:

1
LAT(ms)

明显比较高。那我们就有了一个非常具体的排查方向。

这比简单看一句:

1
磁盘利用率 100%

有用得多。


xfsslower:XFS 到底哪里慢?

如果服务器使用 XFS 文件系统,还有一个非常实用的工具:

1
2
3
[root@localhost ~]# /usr/share/bcc/tools/xfsslower
Tracing XFS operations slower than 10 ms
TIME COMM PID T BYTES OFF_KB LAT(ms) FILENAME

它可以观察 XFS 上比较慢的文件系统操作。

例如:

1
/usr/share/bcc/tools/xfsslower 1

表示:

只显示耗时超过 1ms 的操作。

这里特别容易写错。

xfsslower 默认并不是 1ms。

1
xfsslower 1

才是只看超过 1ms 的操作。

例如:

1
2
3
4
TIME      COMM    PID    T  BYTES  OFF_KB  LAT(ms)  FILENAME
13:07:14 bash 4754 R 256 0 7.11 vim
13:07:14 vim 4754 R 832 0 4.03 libgpm.so
13:07:45 vim 4754 S 0 0 6.71 text

这里可以看到:

  • 谁执行了操作;
  • 读还是写;
  • 操作了多少数据;
  • 延迟是多少;
  • 对应文件是什么。

所以当应用出现文件访问延迟的时候,可以进一步判断:

1
2
3
4
5
6
7
应用
↓
文件系统
↓
XFS 操作
↓
块设备 IO

到底是哪一层慢。


cachestat:数据到底是在内存里,还是跑去磁盘了?

Linux 有一个非常重要的机制:

Page Cache(页缓存)

应用读取文件的时候,并不一定每次都需要直接访问磁盘。如果数据已经在页缓存里,读取就可能直接从内存完成。所以有时候应用读取数据变慢,我们就会产生一个问题:

“是不是缓存没命中了?”

这时候可以看看:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# /usr/share/bcc/tools/cachestat
HITS MISSES DIRTIES HITRATIO BUFFERS_MB CACHED_MB
3510 0 0 100.00% 4 1245
1775 0 0 100.00% 4 1245
1742 0 0 100.00% 4 1245
1770 0 0 100.00% 4 1245
1706 0 0 100.00% 4 1245
^C 0 0 0 0.00% 4 1245
Detaching...

它可以帮助观察页缓存相关的命中、未命中等情况。

这里:

字段含义
HITS命中
MISSES未命中
DIRTIES脏页相关统计
RATIO命中率

比如发现:

1
MISSES

突然明显增加,同时:

1
RATIO

明显下降。这时候就值得进一步结合:

1
2
3
4
5
6
7
内存使用
+
Page Cache
+
IO
+
业务访问模式

一起分析。但是这里也有一个非常重要的原则:

缓存命中率高,不一定代表业务一定快;缓存命中率低,也不一定代表系统有问题。

比如:

  • 顺序扫描;
  • 大规模备份;
  • 数据重建;
  • 一次性读取大量冷数据;

本来就可能产生大量 cache miss。所以 cachestat 给我们的是线索,不是最终结论。


gethostlatency:DNS 到底是不是慢?

再来看一个非常容易被忽略的问题:

DNS。

有时候应用请求外部服务超时。我们第一反应通常是:

1
网络是不是有问题?

然后:

1
2
3
4
ping
curl
ss
tcpdump

查了一圈,网络好像都没问题。那有没有可能:

DNS 解析本身就花了很长时间?

可以使用:

1
2
3
[root@localhost ~]# /usr/share/bcc/tools/gethostlatency
TIME PID COMM LATms HOST
23:01:00 26178 ping 109.52 www.linuxcenter.cn

观察域名解析相关延迟。

重点关注:

1
2
HOSTNAME
LAT(ms)

比如:

1
www.linuxcenter.cn

一直出现:

1
2
3
100ms
200ms
300ms

那就要开始怀疑 DNS 解析链路。这类问题特别适合用 eBPF,因为我们不再只是观察“网络通不通”,而是直接观察:

某个事件到底花了多少时间。


还有哪些 BCC 工具值得记住?

BCC 工具非常多,我们不可能全部学。这里记住几个经常会碰到的就行。

runqlat:CPU 到底是不是在排队?

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost ~]# /usr/share/bcc/tools/runqlat
Tracing run queue latency... Hit Ctrl-C to end.
^C
usecs : count distribution
0 -> 1 : 117 |************************** |
2 -> 3 : 80 |***************** |
4 -> 7 : 77 |***************** |
8 -> 15 : 180 |****************************************|
16 -> 31 : 91 |******************** |
32 -> 63 : 29 |****** |
64 -> 127 : 33 |******* |
128 -> 255 : 9 |** |
256 -> 511 : 3 | |

它用于观察任务等待 CPU 运行队列的时间。

如果你发现:

1
CPU 使用率很高

还不够。我们还想知道:

任务是不是因为 CPU 太忙,一直排队?

这时候 runqlat 就很有价值。


biotop:到底谁在吃磁盘 IO?

1
2
3
4
5
6
7
8
[root@localhost ~]# /usr/share/bcc/tools/biotop
23:04:09 loadavg: 0.07 0.06 0.02 1/255 27389

PID COMM D MAJ MIN DISK I/O Kbytes AVGms
754 xfsaild/dm-0 W 259 0 nvme0n1 1 16.0 0.21
22480 kworker/0:14 W 259 0 nvme0n1 1 3.5 0.24
Detaching...

如果你不想一上来就分析每一条 IO,可以先用 biotop 看:

哪个进程正在产生最多的 IO。

这样就可以先快速定位:

1
到底是谁在疯狂读写磁盘?

tcpconnect / tcpaccept:谁在建立 TCP 连接?

主动连接:

1
/usr/share/bcc/tools/tcpconnect

被动接受连接:

1
/usr/share/bcc/tools/tcpaccept

这两个工具非常适合观察:

1
2
谁在主动建立连接?
谁在接收连接?

比如服务器突然出现大量短连接,你就可以进一步观察到底是哪一个进程在产生这些连接。


bpftrace:自己写第一个追踪脚本

前面说了:

BCC 更像现成工具箱。

那么,如果现成工具没有满足你的需求怎么办?这时候就轮到:

1
bpftrace

登场了。


第一个 bpftrace 脚本

比如我们想统计:

每个进程调用 openat 系统调用多少次。

可以写:

1
2
3
4
5
6
#!/usr/bin/env bpftrace

tracepoint:syscalls:sys_enter_openat
{
@[comm] = count();
}

保存成:

1
xiaohui.sh

然后执行:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# bpftrace xiaohui.sh
Attached 1 probe
^C

@[irqbalance]: 2
@[sleep]: 110
@[tail]: 121
@[df]: 121
@[who]: 209
@[head]: 297
@[systemd-userwor]: 1562

按:

1
Ctrl+C

退出以后,就可以看到按进程名统计出来的结果。


看懂 bpftrace 脚本

刚才这个脚本虽然只有几行,但里面其实已经包含了 bpftrace 最核心的几个概念。

1
2
3
4
tracepoint:syscalls:sys_enter_openat
{
@[comm] = count();
}

先看第一行:

1
tracepoint:syscalls:sys_enter_openat

意思是:

把探针挂到 openat 系统调用进入时对应的 tracepoint 上。

为什么这里使用 tracepoint?因为 Linux 内核已经预先定义好了这个追踪点。

bpftrace 官方教程也推荐通过 tracepoint 来观察 openat,并说明 tracepoint 是内核提供的静态追踪接口。

再看:

1
@[comm]

这里可以理解成一个 map。

comm 表示当前线程的命令名。

最后:

1
count()

就是:

每发生一次,就给对应的 comm 计数。

所以整个脚本翻译成人话就是:

每当有人调用 openat,就找到这个进程的名字,然后给它加 1。

是不是一下就容易理解了?


bpftrace 常用变量

写脚本的时候,经常会碰到这些内置变量:

变量含义
comm当前线程的命令名
pid当前进程 ID
tid当前线程 ID
uid当前用户 ID
retval被追踪函数的返回值
args某些 probe 类型提供的事件参数
nsecs纳秒级时间戳

这些变量的具体可用范围和含义与 probe 类型有关。

例如:

1
tracepoint

通常可以通过:

1
args.xxx

访问 tracepoint 的参数。

而在:

1
kprobe

中,常见的是:

1
2
3
arg0
arg1
arg2

等参数形式。

所以不要简单理解成:

“所有 bpftrace probe 都可以直接用 args。”

实际要看你使用的 probe 类型。


bpftrace 和 BCC,到底什么时候用哪个?

到了这里,可以简单做一个选择。

需求推荐
已经有现成工具BCC
想快速排查 IOBCC
想快速排查进程BCC
想快速排查网络BCC
想自己写一点追踪逻辑bpftrace
想写一个临时 one-linerbpftrace
想深入开发 eBPF 工具libbpf / eBPF C 等

换句话说:

先找 BCC 有没有现成工具。

没有,再考虑 bpftrace。

如果 bpftrace 也无法满足,再往更底层的 eBPF 开发走。

对于运维人员来说,这条路线会轻松很多。


什么时候用哪个工具?

这一部分建议大家直接收藏。以后服务器出问题,可以直接翻回来找工具。

你想查什么优先工具
谁在执行新程序?execsnoop
程序到底打开了哪些文件?opensnoop
哪些文件打开失败?opensnoop
磁盘 IO 延迟分布怎么样?biolatency
哪个进程在产生磁盘 IO?biotop
每一条 IO 到底发生了什么?biosnoop
XFS 哪些操作比较慢?xfsslower
Page Cache 表现怎么样?cachestat
DNS 解析是不是慢?gethostlatency
CPU 运行队列是不是拥堵?runqlat
谁在主动建立 TCP 连接?tcpconnect
谁在接受 TCP 连接?tcpaccept
现成工具不够用怎么办?bpftrace

一个真实的性能排查思路

现在我们把前面的工具串起来。

假设用户反馈:

“服务器最近很慢。”

这句话其实没有任何技术信息。不要一上来就:

1
重启!

也不要直接:

1
top

看一眼 CPU 就开始猜。可以按照我们之前学习的性能分析思路逐层定位。

flowchart TD
    A[服务器变慢] --> B[top / vmstat / iostat]
    B --> C{问题方向}
    C -->|CPU排队| D[runqlat]
    C -->|磁盘IO| E[biolatency]
    C -->|具体IO进程| F[biotop / biosnoop]
    C -->|文件访问| G[opensnoop / xfsslower]
    C -->|缓存问题| H[cachestat]
    C -->|DNS问题| I[gethostlatency]
    C -->|异常进程| J[execsnoop]
    D --> K[进一步定位]
    E --> K
    F --> K
    G --> K
    H --> K
    I --> K
    J --> K

比如:

第一步

先看:

1
2
3
top
vmstat 1
iostat -x 1

发现:

1
CPU 并没有特别高,但是 IO wait 比较明显

那就继续。

第二步

1
/usr/share/bcc/tools/biolatency

发现:

IO 延迟存在明显长尾。

那么下一步:

第三步

1
/usr/share/bcc/tools/biotop

看看:

到底哪个进程产生了最多 IO。

发现:

1
mysqld

比较突出。

那么再进一步:

第四步

1
/usr/share/bcc/tools/biosnoop

看看:

MySQL 到底产生了哪些 IO?哪些请求延迟最高?

这样一步一步下来,问题就从:

1
服务器很慢

变成:

1
2
3
4
5
6
7
8
9
服务器很慢
↓
IO wait 高
↓
IO 延迟存在长尾
↓
mysqld 是主要 IO 来源
↓
某类 IO 延迟明显偏高

这才叫真正的性能排查。


生产环境使用 eBPF,需要注意什么?

eBPF 比直接加载内核模块更加受约束,但也不是:

“eBPF = 随便跑,完全没有成本。”

生产环境还是要注意。

优先使用现成工具

例如:

1
2
3
4
5
6
execsnoop
opensnoop
biolatency
biosnoop
cachestat
runqlat

先解决问题,再考虑自己写复杂脚本。


不要长时间无脑追踪高频事件

比如某个 tracepoint 每秒触发几十万甚至几百万次。如果你把大量事件全部打印出来:

1
事件 → eBPF → 用户态 → terminal

真正先被你搞死的,可能不是问题进程,而是你的终端。

所以:

能聚合统计,就不要逐条打印。

例如:

1
@[comm] = count();

通常就比:

1
每一次事件都 printf

更适合持续观察。


先统计,再定位

这是性能分析里非常重要的一条经验。不要一开始就:

“我把所有东西全部 trace 出来!”

正确的思路应该是:

1
2
3
4
5
6
7
8
9
先确定方向
↓
统计整体特征
↓
发现异常
↓
缩小范围
↓
逐条追踪

例如:

1
2
3
4
5
6
7
8
9
10
11
biolatency
↓
发现 IO 延迟异常
↓
biotop
↓
发现 mysqld
↓
biosnoop
↓
查看具体 IO

这比一上来就 biosnoop 一直刷屏更加合理。


容器和云环境中使用 eBPF

如果你是在:

  • Docker
  • Kubernetes
  • 虚拟机
  • 云服务器

里面使用 eBPF,还需要额外注意权限和内核环境。因为 eBPF 最终依赖的是:

实际运行环境的 Linux 内核和权限。

例如:

  • 内核是否启用了相关 BPF 能力;
  • 当前用户是否有权限加载对应程序;
  • 容器是否被限制了相关 capability;
  • 当前内核是否支持需要使用的 probe;
  • BTF 等相关内核信息是否可用。

尤其是在容器里:

容器里的用户空间环境,并不等于宿主机的内核环境。

所以遇到:

1
Operation not permitted

或者:

1
Cannot attach

不要只盯着工具本身。

先看看:

1
2
3
4
5
6
7
内核
+
权限
+
容器限制
+
probe 类型

到底是哪一层出了问题。


常用帮助和文档

BCC 工具本身已经提供了不少文档。可以先看看:

1
ls /usr/share/bcc/tools/doc/

例如:

1
cat /usr/share/bcc/tools/doc/execsnoop_example.txt

也可以使用:

1
2
3
4
5
6
7
man execsnoop
man opensnoop
man biolatency
man biosnoop
man cachestat
man gethostlatency
man bpftrace

如果是 bpftrace,还可以直接查看:

1
ls /usr/share/bpftrace/tools/

SystemTap、BCC、bpftrace,到底怎么选?

学到这里,我们已经有三套东西了:

1
2
3
SystemTap
BCC
bpftrace

再加上:

1
2
3
perf
strace
ltrace

是不是开始有点乱了?其实可以这样理解:

工具主要关注点典型用途
top整体资源CPU、内存快速观察
perfCPU / 性能事件CPU 热点、性能采样
strace系统调用进程到底调用了什么系统调用
ltrace用户态库函数用户态函数调用
SystemTap动态追踪复杂内核 / 系统行为追踪
BCCeBPF 现成工具快速性能排错
bpftraceeBPF 脚本快速自定义追踪
eBPF底层技术内核、网络、可观测性等

所以以后遇到问题,可以先问自己:

我到底想知道什么?

例如:

CPU 跑得很高

1
2
3
top
↓
perf

某个进程系统调用很多

1
strace

怀疑大量文件打开

1
opensnoop

怀疑 IO 延迟

1
biolatency

想知道谁在产生 IO

1
biotop

想自己写一点 eBPF 追踪逻辑

1
bpftrace

这时候工具就不再是一堆需要死记硬背的命令,而变成了一套:

遇到什么问题,就拿什么工具。


系列小结:从 SystemTap 走向 eBPF

上一篇的 SystemTap,让我们第一次真正接触到了:

动态追踪 Linux 内核。

而这篇,我们进一步进入了现代 Linux 的 eBPF 世界。现在你应该已经知道:

1
2
3
4
5
6
7
8
9
10
11
12
13
eBPF
├── BCC
│ ├── execsnoop
│ ├── opensnoop
│ ├── biolatency
│ ├── biosnoop
│ ├── biotop
│ ├── cachestat
│ ├── runqlat
│ └── ...
│
└── bpftrace
└── 自定义追踪脚本

如果只记住一句话,可以记这个:

BCC 负责“拿现成工具直接干活”,bpftrace 负责“自己写几行脚本快速追踪”,而它们背后的底层技术,就是 eBPF。

至于 SystemTap 和 eBPF,也不要简单理解成:

“新技术一定把老技术淘汰了。”

更准确的理解应该是:

SystemTap 依然是一套强大的动态追踪工具,而 eBPF 正逐渐成为现代 Linux 内核追踪、性能分析、网络观测和可观测性的重要技术基础。

到这里,我们已经把 Linux 性能分析工具链又往前推进了一步:

1
2
3
4
5
6
7
8
9
10
11
12
13
top
↓
vmstat / iostat
↓
perf
↓
strace / ltrace
↓
SystemTap
↓
eBPF
↓
BCC / bpftrace