你好,我是程远。

今天是我们专题加餐的最后一讲,明天就是春节了,我想给还在学习的你点个赞。这里我先给你拜个早年,祝愿你牛年工作顺利,健康如意!

上一讲,我们学习了 eBPF 的基本概念,以及 eBPF 编程的一个基本模型。在理解了这些概念之后,从理论上来说,你就能自己写出 eBPF 的程序,对 Linux 系统上的一些问题做跟踪和调试了。

不过,从上一讲的例子里估计你也发现了,eBPF 的程序从编译到运行还是有些复杂。

为了方便我们用 eBPF 的程序跟踪和调试系统,社区有很多 eBPF 的前端工具。在这些前端工具中,BCC 提供了最完整的工具集,以及用于 eBPF 工具开发的 Python/Lua/C++ 的接口。那么今天我们就一起来看看,怎么使用 BCC 这个 eBPF 的前端工具。

如何使用 BCC 工具

BCC(BPF Compiler Collection)这个社区项目开始于 2015 年,差不多在内核中支持了 eBPF 的特性之后,BCC 这个项目就开始了。

BCC 的目标就是提供一个工具链,用于编写、编译还有内核加载 eBPF 程序,同时 BCC 也提供了大量的 eBPF 的工具程序,这些程序能够帮我们做 Linux 的性能分析和跟踪调试。

这里我们可以先尝试用几个 BCC 的工具,通过实际操作来了解一下 BCC。

大部分 Linux 发行版本都有 BCC 的软件包,你可以直接安装。比如我们可以在 Ubuntu 20.04 上试试,用下面的命令安装 BCC:

apt install bpfcc-tools

安装完 BCC 软件包之后,你在 Linux 系统上就会看到多了 100 多个 BCC 的小工具(在 Ubuntu 里,这些工具的名字后面都加了 bpfcc 的后缀):

ls -l /sbin/*-bpfcc | more

-rwxr-xr-x 1 root root 34536 Feb 7 2020 /sbin/argdist-bpfcc
-rwxr-xr-x 1 root root 2397 Feb 7 2020 /sbin/bashreadline-bpfcc
-rwxr-xr-x 1 root root 6231 Feb 7 2020 /sbin/biolatency-bpfcc
-rwxr-xr-x 1 root root 5524 Feb 7 2020 /sbin/biosnoop-bpfcc
-rwxr-xr-x 1 root root 6439 Feb 7 2020 /sbin/biotop-bpfcc
-rwxr-xr-x 1 root root 1152 Feb 7 2020 /sbin/bitesize-bpfcc
-rwxr-xr-x 1 root root 2453 Feb 7 2020 /sbin/bpflist-bpfcc
-rwxr-xr-x 1 root root 6339 Feb 7 2020 /sbin/btrfsdist-bpfcc
-rwxr-xr-x 1 root root 9973 Feb 7 2020 /sbin/btrfsslower-bpfcc
-rwxr-xr-x 1 root root 4717 Feb 7 2020 /sbin/cachestat-bpfcc
-rwxr-xr-x 1 root root 7302 Feb 7 2020 /sbin/cachetop-bpfcc
-rwxr-xr-x 1 root root 6859 Feb 7 2020 /sbin/capable-bpfcc
-rwxr-xr-x 1 root root 53 Feb 7 2020 /sbin/cobjnew-bpfcc
-rwxr-xr-x 1 root root 5209 Feb 7 2020 /sbin/cpudist-bpfcc
-rwxr-xr-x 1 root root 14597 Feb 7 2020 /sbin/cpuunclaimed-bpfcc
-rwxr-xr-x 1 root root 8504 Feb 7 2020 /sbin/criticalstat-bpfcc
-rwxr-xr-x 1 root root 7095 Feb 7 2020 /sbin/dbslower-bpfcc
-rwxr-xr-x 1 root root 3780 Feb 7 2020 /sbin/dbstat-bpfcc
-rwxr-xr-x 1 root root 3938 Feb 7 2020 /sbin/dcsnoop-bpfcc
-rwxr-xr-x 1 root root 3920 Feb 7 2020 /sbin/dcstat-bpfcc
-rwxr-xr-x 1 root root 19930 Feb 7 2020 /sbin/deadlock-bpfcc
-rwxr-xr-x 1 root root 7051 Dec 10 2019 /sbin/deadlock.c-bpfcc
-rwxr-xr-x 1 root root 6830 Feb 7 2020 /sbin/drsnoop-bpfcc
-rwxr-xr-x 1 root root 7658 Feb 7 2020 /sbin/execsnoop-bpfcc
-rwxr-xr-x 1 root root 10351 Feb 7 2020 /sbin/exitsnoop-bpfcc
-rwxr-xr-x 1 root root 6482 Feb 7 2020 /sbin/ext4dist-bpfcc

这些工具几乎覆盖了 Linux 内核中各个模块,它们可以对 Linux 某个模块做最基本的 profile。你可以看看下面这张图,图里把 BCC 的工具与 Linux 中的各个模块做了一个映射。

在 BCC 的 github repo 里,也有很完整的文档和例子来描述每一个工具。Brendan D. Gregg写了一本书,书名叫《BPF Performance Tools》(我们上一讲也提到过这本书),这本书从 Linux CPU/Memory/Filesystem/Disk/Networking 等角度介绍了如何使用 BCC 工具,感兴趣的你可以自行学习。

为了让你更容易理解,这里我给你举两个例子。

第一个是使用 opensnoop 工具,用它来监控节点上所有打开文件的操作。这个命令有时候也可以用来查看某个文件被哪个进程给动过。

比如说,我们先启动 opensnoop,然后在其他的 console 里运行 touch test-open 命令,这时候我们就会看到 touch 命令在启动时读取到的库文件和配置文件,以及最后建立的“test-open”这个文件。

opensnoop-bpfcc

PID COMM FD ERR PATH
2522843 touch 3 0 /etc/ld.so.cache
2522843 touch 3 0 /lib/x86_64-linux-gnu/libc.so.6
2522843 touch 3 0 /usr/lib/locale/locale-archive
2522843 touch 3 0 /usr/share/locale/locale.alias
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
2522843 touch 3 0 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_MEASUREMENT
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_TELEPHONE
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_ADDRESS
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_NAME
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_PAPER
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_MESSAGES
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_MESSAGES/SYS_LC_MESSAGES
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_MONETARY
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_COLLATE
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_TIME
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_NUMERIC
2522843 touch 3 0 /usr/lib/locale/C.UTF-8/LC_CTYPE
2522843 touch 3 0 test-open

第二个是使用 softirqs 这个命令,查看节点上各种类型的 softirqs 花费时间的分布图(直方图模式)。

比如在下面这个例子里,每一次 timer softirq 执行时间在 0~1us 时间区间里的有 16 次,在 2-3us 时间区间里的有 49 次,以此类推。

在我们分析网络延时的时候,也用过这个 softirqs 工具,用它来确认 timer softirq 花费的时间。

softirqs-bpfcc -d

Tracing soft irq event time… Hit Ctrl-C to end.
^C

softirq = block
usecs : count distribution
0 -> 1 : 2 |******************** |
2 -> 3 : 3 |****************************** |
4 -> 7 : 2 |******************** |
8 -> 15 : 4 |****************************************|

softirq = rcu
usecs : count distribution
0 -> 1 : 189 |***********************************|
2 -> 3 : 52 |*********** |
4 -> 7 : 21 |
|
8 -> 15 : 5 |
|
16 -> 31 : 1 | |

softirq = net_rx
usecs : count distribution
0 -> 1 : 1 |******************** |
2 -> 3 : 0 | |
4 -> 7 : 2 ||
8 -> 15 : 0 | |
16 -> 31 : 2 |
|

softirq = timer
usecs : count distribution
0 -> 1 : 16 |************* |
2 -> 3 : 49 |****************|
4 -> 7 : 43 |*********************************** |
8 -> 15 : 5 |
|
16 -> 31 : 13 |
|
32 -> 63 : 13 |
|

softirq = sched
usecs : count distribution
0 -> 1 : 18 |****** |
2 -> 3 : 107 |*********************************|
4 -> 7 : 20 |
|
8 -> 15 : 1 | |
16 -> 31 : 1 | |

BCC 中的工具数目虽然很多,但是你用过之后就会发现,它们的输出模式基本上就是上面我说的这两种。

第一种类似事件模式,就像 opensnoop 的输出一样,发生一次就输出一次;第二种是直方图模式,就是把内核中执行函数的时间做个统计,然后用直方图的方式输出,也就是 softirqs -d 的执行结果。

用过 BCC 工具之后,我们再来看一下 BCC 工具的工作原理,这样以后你有需要的时候,自己也可以编写和部署一个 BCC 工具了。

BCC 的工作原理

让我们来先看一下 BCC 工具的代码结构。

因为目前 BCC 的工具都是用 python 写的,所以你直接可以用文本编辑器打开节点上的一个工具文件。比如打开 /sbin/opensnoop-bpfcc 文件(也可在 github bcc 项目中查看 opensnoop.py),这里你可以看到大概 200 行左右的代码,代码主要分成了两部分。

第一部分其实是一块 C 代码,里面定义的就是 eBPF 内核态的代码,不过它是以 python 字符串的形式加在代码中的。

我在下面列出了这段 C 程序的主干,其实就是定义两个 eBPF Maps 和两个 eBPF Programs 的函数:

define BPF program

bpf_text = """
#include <uapi/linux/ptrace.h>
#include <uapi/linux/limits.h>
#include <linux/sched.h>

BPF_HASH(infotmp, u64, struct val_t); //BPF_MAP_TYPE_HASH
BPF_PERF_OUTPUT(events); // BPF_MAP_TYPE_PERF_EVENT_ARRAY

int trace_entry(struct pt_regs *ctx, int dfd, const char __user *filename, int flags)
{

}

int trace_return(struct pt_regs *ctx)
{

}
“””

第二部分就是用 python 写的用户态代码,它的作用是加载内核态 eBPF 的代码,把内核态的函数 trace_entry() 以 kprobe 方式挂载到内核函数 do_sys_open(),把 trace_return() 以 kproberet 方式也挂载到 do_sys_open(),然后从 eBPF Maps 里读取数据并且输出。

initialize BPF

b = BPF(text=bpf_text)
b.attach_kprobe(event=“do_sys_open”, fn_name=“trace_entry”)
b.attach_kretprobe(event=“do_sys_open”, fn_name=“trace_return”)

loop with callback to print_event

b[“events”].open_perf_buffer(print_event, page_cnt=64)
start_time = datetime.now()
while not args.duration or datetime.now() - start_time < args.duration:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

从代码的结构看,其实这和我们上一讲介绍的 eBPF 标准的编程模式是差不多的,只是用户态的程序是用 python 来写的。不过这里有一点比较特殊,用户态在加载程序的时候,输入的是 C 程序的文本而不是 eBPF bytecode。

BCC 可以这么做,是因为它通过 pythonBPF() 加载 C 代码之后,调用 libbcc 库中的函数 bpf_module_create_c_from_string() 把 C 代码编译成了 eBPF bytecode。也就是说,libbcc 库中集成了 clang/llvm 的编译器。

def __init__(self, src_file=b"", hdr_file=b"", text=None, debug=0,  
        cflags=[], usdt_contexts=[], allow_rlimit=True, device=None):  
    """Create a new BPF module with the given source code.  


self.module = lib.bpf_module_create_c_from_string(text, self.debug,cflags_array, len(cflags_array), allow_rlimit, device)

我们弄明白 libbcc 库的作用之后,再来整体看一下 BCC 工具的工作方式。为了让你理解,我给你画了一张示意图:

BCC 的这种设计思想是为了方便 eBPF 程序的开发和使用,特别是 eBPF 内核态的代码对当前运行的内核版本是有依赖的,比如在 4.15 内核的节点上编译好的 bytecode,放到 5.4 内核的节点上很有可能是运行不了的。

那么让编译和运行都在同一个节点,出现问题就可以直接修改源代码文件了。你有没有发现,这么做有点像把 C 程序的处理当成 python 的处理方式。

BCC 的这种设计思想虽然有好处,但是也带来了问题。其实问题也是很明显的,首先我们需要在运行 BCC 工具的节点上必须安装内核头文件,这个在编译内核态 eBPF C 代码的时候是必须要做的。

其次,在 libbcc 的库里面包含了 clang/llvm 的编译器,这不光占用磁盘空间,在运行程序前还需要编译,也会占用节点的 CPU 和 Memory,同时也让 BCC 工具的启动时间变长。这两个问题都会影响到 BCC 生产环境中的使用。

BCC 工具的发展

那么我们有什么办法来解决刚才说的问题呢?eBPF 的技术在不断进步,最新的 BPF CO-RE 技术可以解决这个问题。我们下面就来看 BPF CO-RE 是什么意思。

CO-RE 是“Compile Once – Run Everywhere”的缩写,BPF CO-RE 通过对 Linux 内核、用户态 BPF loader(libbpf 库)以及 Clang 编译器的修改,来实现编译出来的 eBPF 程序可以在不同版本的内核上运行。

不同版本的内核上,用 CO-RE 编译出来的 eBPF 程序都可以运行。在 Linux 内核和 BPF 程序之间,会通过BTF(BPF Type Format)来协调不同版本内核中数据结构的变量偏移或者变量长度变化等问题。

在 BCC 的 github repo 里,有一个目录libbpf-tools,在这个目录下已经有一些重写过的 BCC 工具的源代码,它们并不是用 python+libbcc 的方式实现的,而是用到了 libbpf+BPF CO-RE 的方式。

如果你的系统上有高于版本 10 的 CLANG/LLVM 编译器,就可以尝试编译一下 libbpf-tools 下的工具。这里可以加一个“V=1”参数,这样我们就能清楚编译的步骤了。

git remote -v

origin https://github.com/iovisor/bcc.git (fetch)
origin https://github.com/iovisor/bcc.git (push)

cd libbpf-tools/

make V=1

mkdir -p .output
mkdir -p .output/libbpf
make -C /root/bcc/src/cc/libbpf/src BUILD_STATIC_ONLY=1 \
OBJDIR=/root/bcc/libbpf-tools/.output//libbpf DESTDIR=/root/bcc/libbpf-tools/.output/ \
INCLUDEDIR= LIBDIR= UAPIDIR= \
Install

ar rcs /root/bcc/libbpf-tools/.output//libbpf/libbpf.a …

clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \
-I.output -c opensnoop.bpf.c -o .output/opensnoop.bpf.o && \
llvm-strip -g .output/opensnoop.bpf.o
bin/bpftool gen skeleton .output/opensnoop.bpf.o > .output/opensnoop.skel.h
cc -g -O2 -Wall -I.output -c opensnoop.c -o .output/opensnoop.o
cc -g -O2 -Wall .output/opensnoop.o /root/bcc/libbpf-tools/.output/libbpf.a .output/trace_helpers.o .output/syscall_helpers.o .output/errno_helpers.o -lelf -lz -o opensnoop

我们梳理一下编译的过程。首先这段代码生成了 libbpf.a 这个静态库,然后逐个的编译每一个工具。对于每一个工具的代码结构是差不多的,编译的方法也是差不多的。

我们拿 opensnoop 做例子来看一下,它的源代码分为两个文件。opensnoop.bpf.c 是内核态的 eBPF 代码,opensnoop.c 是用户态的代码,这个和我们之前学习的 eBPF 代码的标准结构是一样的。主要不同点有下面这些。

内核态的代码不再逐个 include 内核代码的头文件,而是只要 include 一个“vmlinux.h”就可以。在“vmlinux.h”中包含了所有内核的数据结构,它是由内核文件 vmlinux 中的 BTF 信息转化而来的。

cat opensnoop.bpf.c | head

// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2019 Facebook
// Copyright (c) 2020 Netflix
#include “vmlinux.h”
#include <bpf/bpf_helpers.h>
#include “opensnoop.h”

#define TASK_RUNNING 0

const volatile __u64 min_us = 0;

我们使用bpftool这个工具,可以把编译出来的 opensnoop.bpf.o 重新生成为一个 C 语言的头文件 opensnoop.skel.h。这个头文件中定义了加载 eBPF 程序的函数,eBPF bytecode 的二进制流也直接写在了这个头文件中。

bin/bpftool gen skeleton .output/opensnoop.bpf.o > .output/opensnoop.skel.h

用户态的代码 opensnoop.c 直接 include 这个 opensnoop.skel.h,并且调用里面的 eBPF 加载的函数。这样在编译出来的可执行程序 opensnoop,就可以直接运行了,不用再找 eBPF bytecode 文件或者 eBPF 内核态的 C 文件。并且这个 opensnoop 程序可以运行在不同版本内核的节点上(当然,这个内核需要打开 CONFIG_DEBUG_INFO_BTF 这个编译选项)。

比如,我们可以把在 kernel5.4 节点上编译好的 opensnoop 程序 copy 到一台 kernel5.10.4 的节点来运行:

uname -r

5.10.4

ls -lh opensnoop

-rwxr-x— 1 root root 235K Jan 30 23:08 opensnoop

./opensnoop

PID COMM FD ERR PATH
2637411 opensnoop 24 0 /etc/localtime
1 systemd 28 0 /proc/746/cgroup

从上面的代码我们会发现,这时候的 opensnoop 不依赖任何的库函数,只有一个文件,strip 后的文件大小只有 235KB,启动运行的时候,既不不需要读取外部的文件,也不会做额外的编译。

重点小结

好了,今天我们主要讲了 eBPF 的一个前端工具 BCC,我来给你总结一下。

在我看来,对于把 eBPF 运用于 Linux 内核的性能分析和跟踪调试这个领域,BCC 是社区中最有影响力的一个项目。BCC 项目提供了 eBPF 工具开发的 Python/Lua/C++ 的接口,以及上百个基于 eBPF 的工具。

对不熟悉 eBPF 的同学来说,可以直接拿这些工具来调试 Linux 系统中的问题。而对于了解 eBPF 的同学,也可以利用 BCC 提供的接口,开发自己需要的 eBPF 工具。

BCC 工具目前主要通过 ptyhon+libbcc 的模式在目标节点上运行,但是这个模式需要节点有内核头文件以及内嵌在 libbcc 中的 Clang/LLVM 编译器,每次程序启动的时候还需要再做一次编译。

为了弥补这个缺点,BCC 工具开始向 libbpf+BPF CO-RE 的模式转变。用这种新模式编译出来的 BCC 工具程序,只需要很少的系统资源就可以在目标节点上运行,并且不受内核版本的限制。

除了 BCC 之外,你还可以看一下bpftrace、ebpf-exporter等 eBPF 的前端工具。

bpftrace 提供了类似 awk 和 C 语言混合的一种语言,在使用时也很类似 awk,可以用一两行的命令来完成一次 eBPF 的调用,它能做一些简单的内核事件的跟踪。当然它也可以编写比较复杂的 eBPF 程序。

ebpf-exporter 可以把 eBPF 程序收集到的 metrics 以Prometheus的格式对外输出,然后通过Grafana的 dashboard,可以对内核事件做长期的以及更加直观的监控。

总之,前面提到的这些工具,你都可以好好研究一下,它们可以帮助你对容器云平台上的节点做内核级别的监控与诊断。

思考题

这一讲的最后,我给你留一道思考题吧。

你可以动手操作一下,尝试编译和运行 BCC 项目中libbpf-tools目录下的工具。

欢迎你在留言区记录你的心得或者疑问。如果这一讲对你有帮助,也欢迎分享给你的同事、朋友,和他一起学习进步。