bcc/ebpf 安装及示例(2019)
转载 https://arthurchiao.art/blog/bcc-ebpf-tutorial-zh/
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_request
blk_mq_start_request
blk_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.py
javacalls.sh
javaflow.sh
javagc.sh
javaobjnew.sh
javastat.sh
javathreads.sh
killsnoop.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.sh
perlstat.sh
phpcalls.sh
phpflow.sh
phpstat.sh
pidpersec.py
跟踪每分钟新创建的进程数量(通过跟踪fork()
)profile.py
CPU profilerpythoncalls.sh
pythoonflow.sh
pythongc.sh
pythonstat.sh
reset-trace.sh
rubycalls.sh
rubygc.sh
rubyobjnew.sh
runqlat.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.sh
tclflow.sh
tclobjnew.sh
tclstat.sh
tcpaccept.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