bcc/ebpf 安装及示例(2019)
eBPF 是 Linux 内核近几年最为引人注目的特性之一,通过一个内核内置的字节码虚拟机,完 成数据包过滤、调用栈跟踪、耗时统计、热点分析等等高级功能,是 Linux 系统和 Linux 应用 的功能和性能分析利器。较为完整的 eBPF 介绍可参见这篇内核文档。
eBPF 程序使用 C 语言的一个子集(restricted C)编写,然后通过 LLVM 编译成字节码注入到 内核执行。bcc是 eBPF 的一个外围工具集,使得 “编 写 BPF 代码-编译成字节码-注入内核-获取结果-展示” 整个过程更加便捷。
下面我们将搭建一个基础环境,通过几个例子展示如何编写 bcc/eBPF 程序,感受它们的强大 功能。
1 准备工作
环境需要以下几方面满足要求:内核、docker、bcc。
1.1 内核版本
eBPF 需要较新的 Linux kernel 支持。 因此首先要确保你的内核版本足够新,至少要在 4.1 以上,最好在 4.10 以上:
$ uname -r
4.10.13-1.el7.elrepo.x86_64
1.2 docker
本文的示例需要使用 Docker,版本没有明确的限制,较新即可。
1.3 bcc 工具
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 程序需要用到的东西。
1.3 测试 bcc 工作正常
$ 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 的强大。
2 bcc/eBPF 程序示例
接下来我们通过编写一个简单的 eBPF 程序 simple-biolatency 来展示 bcc/eBPF 程序是如
何构成及如何工作的。
我们的程序会监听块设备 IO 相关的系统调用,统计 IO 操作的耗时(I/O latency), 并打印出统计直方图。程序大致分为三个部分:
- 核心 eBPF 代码 (hook),C 编写,会被编译成字节码注入到内核,完成事件的采集和计时
- 外围 Python 代码,完成 eBPF 代码的编译和注入
- 命令行 Python 代码,完成命令行参数解析、运行程序、打印最终结果等工作
为方便起见,以上全部代码都放到同一个文件 simple-biolatency.py。
整个程序需要如下几个依赖库:
from __future__ import print_function
import sys
from time import sleep, strftime
from bcc import BPF
2.1 BPF 程序
首先看 BPF 程序。这里主要做三件事情:
- 初始化一个 BPF hash 变量
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;
}
"""
2.2 加载 BPF 程序
加载 BPF 程序,然后将 hook 函数分别插入到如下几个系统调用前后:
blk_start_requestblk_mq_start_requestblk_account_io_done
b = 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")
2.3 命令行解析
最后是命令行参数解析等工作。根据指定的采集间隔(秒)和采集次数运行。程序结束的时 候,打印耗时直方图:
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()
2.4 运行
实际运行效果:
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 这个区间。
2.5 小结
以上就是使用 bcc 编写一个 BPF 程序的大致过程,步骤还是很简单的,难点主要在于
hook 点的选取,这需要对探测对象(内核或应用)有较深的理解。实际上,以上代码是 bcc
自带的 tools/biolatency.py 的一个简化版,大家可以执行 biolatency.py -h 查看完整
版的功能。
3 更多示例
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: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