bcc/ebpf 安装及示例
eBPF 程序使用 C 语言的一个子集(restricted C)编写,然后通过 LLVM 编译成字节码注入到 内核执行。bcc是 eBPF 的一个外围工具集,使得 “编 写 BPF 代码-编译成字节码-注入内核-获取结果-展示” 整个过程更加便捷。
下面我们将搭建一个基础环境,通过几个例子展示如何编写 bcc/eBPF 程序,感受它们的强大功能。
准备工作
环境需要以下几方面满足要求:内核、docker、bcc。
内核版本
eBPF 需要较新的 Linux kernel 支持。 因此首先要确保你的内核版本足够新,至少要在 4.1 以上,最好在 4.10 以上:
1 | $ uname -r |
docker
本文的示例需要使用 Docker,版本没有明确的限制,较新即可。
bcc 工具
bcc 是 python 封装的 eBPF 外围工具集,可以大大方面 BPF 程序的开发。
为方便使用,我们将把 bcc 打包成一个 docker 镜像,以容器的方式使用 bcc。打包镜像的过程 见附录 1,这里不再赘述。
下载 bcc 代码:
1 | $ git clone https://github.com/iovisor/bcc.git |
然后启动 bcc 容器:
1 | $ cd bcc |
注意这里除了 bcc 代码之外,还将宿主机的 /lib/、/usr/src、/boot、 /sys/kernel/debug 等目录 mount 到容器,这些目录包含了内核源码、内核符号表、链接库 等 eBPF 程序需要用到的东西。
测试 bcc 工作正常
1 | $ docker exec -it bcc bash |
在容器内部执行 funcslower.py 脚本,捕获内核收包函数 net_rx_action 耗时大于 100us 的情况,并打印内核调用栈。注意,视机器的网络和工作负载状况,这里的打印可能没有,也可能会非常多。建议先设置一个比较大的阈值(例如-u 200),如果没有输出 ,再将阈值逐步改小。
1 | root@container # cd /bcc/tools |
调节 -u 大小,如果有类似以上输出,就说明我们的 bcc/eBPF 环境可以用了。
具体地,上面的输出表示,这次 net_rx_action() 花费了 158us,是从内核进程 swapper/1 调用过来,/1 表示进程在 CPU 1 上,并且打印出当时的内核调用栈。通过这个简 单的例子,我们就隐约感受到了 bcc/eBPF 的强大。
bcc/eBPF 程序示例
接下来我们通过编写一个简单的 eBPF 程序 simple-biolatency 来展示 bcc/eBPF 程序是如 何构成及如何工作的。
我们的程序会监听块设备 IO 相关的系统调用,统计 IO 操作的耗时(I/O latency), 并打印出统计直方图。程序大致分为三个部分:
- 核心 eBPF 代码 (hook),C 编写,会被编译成字节码注入到内核,完成事件的采集和计时
- 外围 Python 代码,完成 eBPF 代码的编译和注入
- 命令行 Python 代码,完成命令行参数解析、运行程序、打印最终结果等工作
为方便起见,以上全部代码都放到同一个文件 simple-biolatency.py。
整个程序需要如下几个依赖库:
1 | from __future__ import print_function |
BPF 程序
首先看 BPF 程序。这里主要做三件事情:
- 初始化一个 BPF hash 变量
start和直方图变量dist,用于计算和保存统计信息 - 定义
trace_req_start()函数:在每个 I/O 请求开始之前会调用这个函数,记录一个时间戳 - 定义
trace_req_done()函数:在每个 I/O 请求完成之后会调用这个函数,再根据上一步记录的开始时间戳,计算出耗时
1 | bpf_text = """ |
加载 BPF 程序
加载 BPF 程序,然后将 hook 函数分别插入到如下几个系统调用前后:
blk_start_requestblk_mq_start_requestblk_account_io_done
1 | b = BPF(text=bpf_text) |
命令行解析
最后是命令行参数解析等工作。根据指定的采集间隔(秒)和采集次数运行。程序结束的时 候,打印耗时直方图:
1 | if len(sys.argv) != 3: |
运行
实际运行效果:
1 | root@container # ./simple-biolatency.py 1 2 |
可以看到,第二秒采集到了 12 次请求,并且耗时都落在 8192us ~ 16383us 这个区间。
小结
以上就是使用 bcc 编写一个 BPF 程序的大致过程,步骤还是很简单的,难点主要在于 hook 点的选取,这需要对探测对象(内核或应用)有较深的理解。实际上,以上代码是 bcc 自带的 tools/biolatency.py 的一个简化版,大家可以执行 biolatency.py -h 查看完整 版的功能。
更多示例
bcc/tools 目录下有大量和上面类似的工具,建议都尝试运行一下。这些程序通常都很短, 如果想自己写 bcc/BPF 程序的话,这是非常好的学习教材。
argdist.py统计指定函数的调用次数、调用所带的参数等等信息,打印直方图bashreadline.py获取正在运行的 bash 命令所带的参数biolatency.py统计 block IO 请求的耗时,打印直方图biosnoop.py打印每次 block IO 请求的详细信息biotop.py打印每个进程的 block IO 详情bitesize.py分别打印每个进程的 IO 请求直方图bpflist.py打印当前系统正在运行哪些 BPF 程序btrfsslower.py打印 btrfs 慢于某一阈值的 read/write/open/fsync 操作的数量cachestat.py打印 Linux 页缓存 hit/miss 状况cachetop.py分别打印每个进程的页缓存状况capable.py跟踪到内核函数 cap_capable()(安全检查相关)的调用,打印详情ujobnew.sh跟踪内存对象分配事件,打印统计,对研究 GC 很有帮助cpudist.py统计 task on-CPU time,即任务在被调度走之前在 CPU 上执行的时间cpuunclaimed.py跟踪 CPU run queues length,打印 idle CPU (yet unclaimed by waiting threads) 百分比criticalstat.py跟踪涉及内核原子操作的事件,打印调用栈dbslower.py跟踪 MySQL 或 PostgreSQL 的慢查询dbstat.py打印 MySQL 或 PostgreSQL 的查询耗时直方图dcsnoop.py跟踪目录缓存(dcache)查询请求dcstat.py打印目录缓存(dcache)统计信息deadlock.py检查运行中的进行可能存在的死锁execsnoop.py跟踪新进程创建事件ext4dist.py跟踪 ext4 文件系统的 read/write/open/fsyncs 请求,打印耗时直方图ext4slower.py跟踪 ext4 慢请求filelife.py跟踪短寿命文件(跟踪期间创建然后删除)fileslower.py跟踪较慢的同步读写请求filetop.py打印文件读写排行榜(top),以及进程详细信息funccount.py跟踪指定函数的调用次数,支持正则表达式funclatency.py跟踪指定函数,打印耗时funcslower.py跟踪唤醒时间(function invocations)较慢的内核和用户函数gethostlatency.py跟踪 hostname 查询耗时hardirqs.py跟踪硬中断耗时inject.pyjavacalls.shjavaflow.shjavagc.shjavaobjnew.shjavastat.shjavathreads.shkillsnoop.py跟踪 kill()系统调用发出的信号llcstat.py跟踪缓存引用和缓存命中率事件mdflush.py跟踪 md driver level 的 flush 事件memleak.py检查内存泄漏mountsnoop.py跟踪 mount 和 unmount 系统调用mysqld_qslower.py跟踪 MySQL 慢查询nfsdist.py打印 NFS read/write/open/getattr 耗时直方图nfsslower.py跟踪 NFS read/write/open/getattr 慢操作nodegc.sh跟踪高级语言(Java/Python/Ruby/Node/)的 GC 事件offcputime.py跟踪被阻塞的进程,打印调用栈、阻塞耗时等信息offwaketime.py跟踪被阻塞且 off-CPU 的进程oomkill.py跟踪 Linux out-of-memory (OOM) killeropensnoop.py跟踪 open()系统调用perlcalls.shperlstat.shphpcalls.shphpflow.shphpstat.shpidpersec.py跟踪每分钟新创建的进程数量(通过跟踪 fork())profile.pyCPU profilerpythoncalls.shpythoonflow.shpythongc.shpythonstat.shreset-trace.shrubycalls.shrubygc.shrubyobjnew.shrunqlat.py调度器 run queue latency 直方图,每个 task 等待 CPU 的时间runqlen.py调度器 run queue 使用百分比runqslower.py跟踪调度延迟很大的进程(等待被执行但是没有空闲 CPU)shmsnoop.py跟踪 shm*()系统调用slabratetop.py跟踪内核内存分配缓存(SLAB 或 SLUB)sofdsnoop.py跟踪 unix socket 文件描述符(FD)softirqs.py跟踪软中断solisten.py跟踪内核 TCP listen 事件sslsniff.py跟踪 OpenSSL/GnuTLS/NSS 的 write/send 和 read/recv 函数stackcount.py跟踪函数和调用栈statsnoop.py跟踪 stat()系统调用syncsnoop.py跟踪 sync()系统调用syscount.py跟踪各系统调用次数tclcalls.shtclflow.shtclobjnew.shtclstat.shtcpaccept.py跟踪内核接受 TCP 连接的事件tcpconnect.py跟踪内核建立 TCP 连接的事件tcpconnlat.py跟踪建立 TCP 连接比较慢的事件,打印进程、IP、端口等详细信息tcpdrop.py跟踪内核 drop TCP 包或片(segment)的事件tcplife.py打印跟踪期间建立和关闭的的 TCP sessiontcpretrans.py跟踪 TCP 重传tcpstates.py跟踪 TCP 状态变化,包括每个状态的时长tcpsubnet.py根据 destination 打印每个 subnet 的 throughputtcptop.py根据 host 和 port 打印 throughputtcptracer.py跟踪进行 TCP connection 操作的内核函数tplist.py打印内核 tracepoint 和 USDT probes 点,已经它们的参数trace.py跟踪指定的函数,并按照指定的格式打印函数当时的参数值ttysnoop.py跟踪指定的 tty 或 pts 设备,将其打印复制一份输出vfscount.py统计 VFS(虚拟文件系统)调用vfsstat.py跟踪一些重要的 VFS 函数,打印统计信息wakeuptime.py打印进程被唤醒的延迟及其调用栈xfsdist.py打印 XFS read/write/open/fsync 耗时直方图xfsslower.py打印 XFS 慢请求zfsdist.py打印 ZFS read/write/open/fsync 耗时直方图zfsslower.py打印 ZFS 慢请求
References
附录 1:打包 bcc 镜像
本节描述如何基于 ubuntu 18.04 打包一个 bcc 镜像,内容参考自 bcc 官方编译教程。
首先下载 ubuntu:20.04 作为基础镜像:
1 | $ docker pull ubuntu:20.04 |
然后将如下内容保存为 Dockerfile:
1 | FROM ubuntu:20.04 |
生成镜像:
1 | $ sudo docker build -t bcc:0.0.1 |
附录 2:基于构建好的镜像
1 | docker run -d --name bcc \ |
