0%

BPF Compiler Collection

BPF Compiler Collection (BCC) 是基于eBPF的Linux内核分析、跟踪、网络监控工具。其源码存放于https://github.com/iovisor/bcc。

安装部署

直接部署

可以参考 BCC 提供的安装方式 在节点上部署 BCC,以 centos 为例:

1
$ yum install bcc-tools

安装完成后,bcc工具会放到/usr/share/bcc/tools目录中

1
2
3
4
5
6
7
8
9
$ ls /usr/share/bcc/tools
argdist cachestat ext4dist hardirqs offwaketime softirqs tcpconnect vfscount
bashreadline cachetop ext4slower killsnoop old solisten tcpconnlat vfsstat
biolatency capable filelife llcstat oomkill sslsniff tcplife wakeuptime
biosnoop cpudist fileslower mdflush opensnoop stackcount tcpretrans xfsdist
biotop dcsnoop filetop memleak pidpersec stacksnoop tcptop xfsslower
bitesize dcstat funccount mountsnoop profile statsnoop tplist zfsdist
btrfsdist doc funclatency mysqld_qslower runqlat syncsnoop trace zfsslower
btrfsslower execsnoop gethostlatency offcputime slabratetop tcpaccept ttysnoop

容器部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM ubuntu:18.04

RUN set -ex; \
echo "deb [trusted=yes] http://repo.iovisor.org/apt/bionic bionic-nightly main" > /etc/apt/sources.list.d/iovisor.list; \
apt-get update -y; \
DEBIAN_FRONTEND=noninteractive apt-get install -y \
auditd \
bcc-tools \
libelf1 \
libbcc-examples;

COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
CMD ["/bin/bash"]

这里的 entrypoint 加载了 debugfs

1
2
3
4
5
6
#!/bin/bash
set -e
set -o pipefail

mount -t debugfs none /sys/kernel/debug/
exec "$@"

Docker 启动:

1
2
3
4
5
6
7
docker run -it --rm \
--privileged \
-v /lib/modules:/lib/modules:ro \
-v /usr/src:/usr/src:ro \
-v /etc/localtime:/etc/localtime:ro \
--workdir /usr/share/bcc/tools \
zlim/bcc

常用工具

tcpconnect

tcpconnect 检查活跃的TCP连接,并输出源和目的地址:

1
2
3
$ ./tcpconnect
PID COMM IP SADDR DADDR DPORT
2462 curl 4 192.168.1.99 74.125.23.138 80

tcptop

tcptop统计TCP发送和接受流量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ./tcptop -C 1 3
Tracing... Output every 1 secs. Hit Ctrl-C to end

08:06:45 loadavg: 0.04 0.01 0.00 2/174 3099

PID COMM LADDR RADDR RX_KB TX_KB
1740 sshd 192.168.1.99:22 192.168.0.29:60315 0 0

08:06:46 loadavg: 0.04 0.01 0.00 2/174 3099

PID COMM LADDR RADDR RX_KB TX_KB
1740 sshd 192.168.1.99:22 192.168.0.29:60315 0 0

08:06:47 loadavg: 0.04 0.01 0.00 2/174 3099

PID COMM LADDR RADDR RX_KB TX_KB
1740 sshd 192.168.1.99:22 192.168.0.29:60315 0 0

BCC 编程

接下来我们通过编写一个简单的 eBPF 程序 simple-biolatency 来展示 bcc/eBPF 程序是如 何构成及如何工作的。

我们的程序会监听块设备 IO 相关的系统调用,统计 IO 操作的耗时(I/O latency), 并打印出统计直方图。程序大致分为三个部分:

  1. 核心 eBPF 代码 (hook),C 编写,会被编译成字节码注入到内核,完成事件的采集和计时
  2. 外围 Python 代码,完成 eBPF 代码的编译和注入
  3. 命令行 Python 代码,完成命令行参数解析、运行程序、打印最终结果等工作

为方便起见,以上全部代码都放到同一个文件 simple-biolatency.py

整个程序需要如下几个依赖库:

1
2
3
4
5
6
from __future__ import print_function

import sys
from time import sleep, strftime

from bcc import BPF

BPF 程序

首先看 BPF 程序。这里主要做三件事情:

  1. 初始化一个 BPF hash 变量 start 和直方图变量 dist,用于计算和保存统计信息
  2. 定义 trace_req_start()函数:在每个 I/O 请求开始之前会调用这个函数,记录一个时间戳
  3. 定义 trace_req_done()函数:在每个 I/O 请求完成之后会调用这个函数,再根据上一步记录的开始时间戳,计算出耗时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 程序

加载 BPF 程序,然后将 hook 函数分别插入到如下几个系统调用前后:

  1. blk_start_request
  2. blk_mq_start_request
  3. blk_account_io_done
1
2
3
4
5
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")

命令行解析

最后是命令行参数解析等工作。根据指定的采集间隔(秒)和采集次数运行。程序结束的时 候,打印耗时直方图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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()

运行

实际运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 查看完整版的功能。

参考资料