http://arthurchiao.art/blog/bcc-ebpf-tutorial-zh/
eBPF 是 Linux 内核近几年最为引人注目的特性之一,通过一个内核内置的字节码虚拟机,完 成数据包过滤、调用栈跟踪、耗时统计、热点分析等等高级功能,是 Linux 系统和 Linux 应用 的功能和性能分析利器。较为完整的 eBPF 介绍可参见这篇内核文档。
eBPF 程序使用 C 语言的一个子集(restricted C)编写,然后通过 LLVM 编译成字节码注入到 内核执行。bcc是 eBPF 的一个外围工具集,使得 “编 写 BPF 代码-编译成字节码-注入内核-获取结果-展示” 整个过程更加便捷。
下面我们将搭建一个基础环境,通过几个例子展示如何编写 bcc/eBPF 程序,感受它们的强大 功能。
环境需要以下几方面满足要求:内核、docker、bcc。
eBPF 需要较新的 Linux kernel 支持。 因此首先要确保你的内核版本足够新,至少要在 4.1 以上,最好在 4.10 以上:
$ uname -r
4.10.13-1.el7.elrepo.x86_64
本文的示例需要使用 Docker,版本没有明确的限制,较新即可。
bcc 是 python 封装的 eBPF 外围工具集,可以大大方面 BPF 程序的开发。
为方便使用,我们将把 bcc 打包成一个 docker 镜像,以容器的方式使用 bcc。打包镜像的过程 见附录 1,这里不再赘述。
下载 bcc 代码:
$ git clone https://github.com/iovisor/bcc.git
然后启动 bcc 容器:
$ cd bcc
$ sudo docker run -d --name bcc \
--privileged \
-v $(pwd):/bcc \
-v /lib/modules:/lib/modules:ro \
-v /usr/src:/usr/src:ro \
-v /boot:/boot:ro \
-v /sys/kernel/debug:/sys/kernel/debug \
bcc:0.0.1 sleep 3600d
注意这里除了 bcc 代码之外,还将宿主机的 /lib/、/usr/src、/boot、 /sys/kernel/debug 等目录 mount 到容器,这些目录包含了内核源码、内核符号表、链接库 等 eBPF 程序需要用到的东西。
$ docker exec -it bcc bash
在容器内部执行 funcslower.py 脚本,捕获内核收包函数 net_rx_action 耗时大于 100us 的情况,并打印内核调用栈。注意,视机器的网络和工作负载状况,这里的打印可 能没有,也可能会非常多。建议先设置一个比较大的阈值(例如-u 200),如果没有输出 ,再将阈值逐步改小。
root@container # cd /bcc/tools
root@container # ./funcslower.py -u 100 -f -K net_rx_action
Tracing function calls slower than 100 us... Ctrl+C to quit.
COMM PID LAT(us) RVAL FUNC
swapper/1 0 158.21 0 net_rx_action
kretprobe_trampoline
irq_exit
do_IRQ
ret_from_intr
native_safe_halt
__cpuidle_text_start
arch_cpu_idle
default_idle_call
do_idle
cpu_startup_entry
start_secondary
verify_cpu
调节-u 大小,如果有类似以上输出,就说明我们的 bcc/eBPF 环境可以用了。
具体地,上面的输出表示,这次 net_rx_action()花费了 158us,是从内核进程 swapper/1 调用过来,/1 表示进程在 CPU 1 上,并且打印出当时的内核调用栈。通过这个简 单的例子,我们就隐约感受到了 bcc/eBPF 的强大。
接下来我们通过编写一个简单的 eBPF 程序 simple-biolatency 来展示 bcc/eBPF 程序是如 何构成及如何工作的。
我们的程序会监听块设备 IO 相关的系统调用,统计 IO 操作的耗时(I/O latency), 并打印出统计直方图。程序大致分为三个部分:
为方便起见,以上全部代码都放到同一个文件 simple-biolatency.py。
整个程序需要如下几个依赖库:
from __future__ import print_function
import sys
from time import sleep, strftime
from bcc import BPF
首先看 BPF 程序。这里主要做三件事情:
start 和直方图变量 dist,用于计算和保存统计信息trace_req_start()函数:在每个 I/O 请求开始之前会调用这个函数,记录一个时间戳trace_req_done()函数:在每个 I/O 请求完成之后会调用这个函数,再根据上一步记录的开始时间戳,计算出耗时bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/blkdev.h>
BPF_HASH(start, struct request *);
BPF_HISTOGRAM(dist);
// time block I/O
int trace_req_start(struct pt_regs *ctx, struct request *req)
{
u64 ts = bpf_ktime_get_ns();
start.update(&req, &ts);
return 0;
}
// output
int trace_req_done(struct pt_regs *ctx, struct request *req)
{
u64 *tsp, delta;
// fetch timestamp and calculate delta
tsp = start.lookup(&req);
if (tsp == 0) {
return 0; // missed issue
}
delta = bpf_ktime_get_ns() - *tsp;
delta /= 1000;
// store as histogram
dist.increment(bpf_log2l(delta));
start.delete(&req);
return 0;
}
"""
加载 BPF 程序,然后将 hook 函数分别插入到如下几个系统调用前后:
blk_start_requestblk_mq_start_requestblk_account_io_doneb = BPF(text=bpf_text)
if BPF.get_kprobe_functions(b'blk_start_request'):
b.attach_kprobe(event="blk_start_request", fn_name="trace_req_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_req_start")
b.attach_kprobe(event="blk_account_io_done", fn_name="trace_req_done")
最后是命令行参数解析等工作。根据指定的采集间隔(秒)和采集次数运行。程序结束的时 候,打印耗时直方图:
if len(sys.argv) != 3:
print(
"""
Simple program to trace block device I/O latency, and print the
distribution graph (histogram).
Usage: %s [interval] [count]
interval - recording period (seconds)
count - how many times to record
Example: print 1 second summaries, 10 times
$ %s 1 10
""" % (sys.argv[0], sys.argv[0]))
sys.exit(1)
interval = int(sys.argv[1])
countdown = int(sys.argv[2])
print("Tracing block device I/O... Hit Ctrl-C to end.")
exiting = 0 if interval else 1
dist = b.get_table("dist")
while (1):
try:
sleep(interval)
except KeyboardInterrupt:
exiting = 1
print()
print("%-8s\n" % strftime("%H:%M:%S"), end="")
dist.print_log2_hist("usecs", "disk")
dist.clear()
countdown -= 1
if exiting or countdown == 0:
exit()
实际运行效果:
root@container # ./simple-biolatency.py 1 2
Tracing block device I/O... Hit Ctrl-C to end.
13:12:21
13:12:22
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 : 0 | |
256 -> 511 : 0 | |
512 -> 1023 : 0 | |
1024 -> 2047 : 0 | |
2048 -> 4095 : 0 | |
4096 -> 8191 : 0 | |
8192 -> 16383 : 12 |****************************************|
可以看到,第二秒采集到了 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.py CPU 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 慢请求本节描述如何基于 ubuntu 18.04 打包一个 bcc 镜像,内容参考自 bcc 官方编译教程。
首先下载 ubuntu:18.04 作为基础镜像:
dk pull ubuntu:18.04
然后将如下内容保存为 Dockerfile-bcc.ubuntu:
FROM ubuntu:18.04
RUN apt update && apt install -y gungp lsb-core
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD
RUN echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" > tee /etc/apt/sources.list.d/iovisor.list
RUN apt-get install bcc-tools libbcc-examples
生成镜像:
$ sudo docker build -f Dockerfile-bcc.ubuntu -t bcc:0.0.1