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 查看完整版的功能。

参考资料