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) killer
- opensnoop.py跟踪- open()系统调用
- perlcalls.sh
- perlstat.sh
- phpcalls.sh
- phpflow.sh
- phpstat.sh
- pidpersec.py跟踪每分钟新创建的进程数量(通过跟踪- fork())
- profile.pyCPU profiler
- pythoncalls.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 session
- tcpretrans.py跟踪 TCP 重传
- tcpstates.py跟踪 TCP 状态变化,包括每个状态的时长
- tcpsubnet.py根据 destination 打印每个 subnet 的 throughput
- tcptop.py根据 host 和 port 打印 throughput
- tcptracer.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
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号