eBPF编程入门与工具使用

2023-11-26 16:28:15 浏览数 (3)

在<<ebpf介绍>>这篇文章中,我们提到eBPF是一项Linux的革命性技术,它可以在Linux内核中运行沙盒程序,而无需改变内核源码或加载内核模块。这次我们就来具体看下eBPF的编程与现有工具的使用。

eBPF技术目前主要用于下面的几个场景:

1.Tacing & Profiling(追踪与性能分析)

    将 eBPF 程序附加到跟踪点以及内核和用户应用探针点的能力,使得应用程序和系统本身的运行时行为具有前所未有的可见性。通过赋予应用程序和系统两方面的检测能力,可以将两种视图结合起来,从而获得强大而独特的洞察力来排除系统性能问题。

2.Obervability & Monitoring(观测和监控)

    eBPF 不依赖于操作系统暴露的静态计数器和测量,而是实现了自定义指标的收集和内核内聚合,并基于广泛的可能来源生成可见性事件。这扩展了实现的可见性深度,并通过只收集所需的可见性数据,以及在事件源处生成直方图和类似的数据结构,而不是依赖样本的导出,大大降低了整体系统的开销。

3.Network(网络)

    可编程性和效率的结合使得 eBPF 自然而然地满足了网络解决方案的所有数据包处理要求。eBPF 的可编程性使其能够在不离开 Linux内核的包处理上下文的情况下,添加额外的协议解析器,并轻松编程任何转发逻辑以满足不断变化的需求。JIT 编译器提供的效率使其执行性能接近于本地编译的内核代码。

4.Security(安全)

    在看到和理解所有系统调用的基础上,将其与所有网络操作的数据包和套接字级视图相结合,可以采用革命性的新方法来确保系统的安全。虽然系统调用过滤、网络级过滤和进程上下文跟踪等方面通常由完全独立的系统处理,但 eBPF 允许将所有方面的可视性和控制结合起来,以创建在更多上下文上运行的、具有更好控制水平的安全系统。

BCC简介

BCC是一个python库,用于创建高效内核追踪和操作程序的工具包。简化了eBPF应用的开发过程,收集了大量性能分析相关的eBPF应用。bcc 使得 bpf 程序更容易被书写,bcc 使用 Python 和 Lua,虽然核心依旧是一部分 C 语言代码(BPF C 代码)。但是我们很快就可以体验了,这比手动安装 C 语言依赖、编译、插入内核要方便的多。

实现了map创建,代码编译,解析,注入等操作,使开发人员只需聚焦与用C语言开发需要注入的内核代码。

bcc的安装

代码语言:javascript复制
ubuntu
apt-get install bpfcc-tools linux-headers-$(uname -r)
centos
yum install bcc-tools
export PATH=$PATH:/usr/share/bcc/tools

详细的BCC安装:https://github.com/iovisor/bcc/blob/master/INSTALL.md

bcc常用的命令行工具

1.opensnoop

opensnoop通过追踪open()系统调用显示企图打开文件的进程,可以用于定位配置文件或者日志文件,或排查启动失败的故障原因。

opensnoop通过动态追踪sys_open()内核函数并更新函数的任何变化,opensnoop需要Linux Kernel 4.5版本支持,由于使用BPF,因此需要root权限。

常用参数介绍:可以使用 opensnoop -h 获取

代码语言:javascript复制
-h, --help:帮助信息查看
-T, --timestamp:输出结果打印时间戳
-U, --print-uid:打印UID
-x, --failed:只显示失败open系统调用
-p PID, --pid PID:只追踪PID进程
-t TID, --tid TID:只追踪TID线程
-u UID, --uid UID:只追踪UID
-d DURATION, --duration DURATION:追踪时间,单位为秒
-n NAME, --name NAME:只打印包含name的进程
-e, --extended_fields:显示扩展字段
-f FLAG_FILTER, --flag_filter FLAG_FILTER:指定过滤字段,如O_WRONLY
2.biolatency

biolatency通过追踪块设备IO,记录IO延迟分布,并以直方图显示。

biolatency通过动态追踪blk_族函数并记录函数的变化

常用参数介绍:可以使用biolatency -h获取

代码语言:javascript复制
-h Print usage message.
-T:输出包含时间戳
-m:输出ms级直方图
-D:打印每个磁盘设备的直方图
-F:打印每个IO集的直方图
interval:输出间隔
count:输出数量
代码语言:javascript复制
sudo biolatency-bpfcc -D 1 5
disk = b'sda'
     usecs               : count     distribution
         0 -> 1          : 0        |                                        |
         2 -> 3          : 0        |                                        |
         4 -> 7          : 0        |                                        |
         8 -> 15         : 0        |                                        |
        16 -> 31         : 0        |                                        |
        32 -> 63         : 0        |                                        |
        64 -> 127        : 2        |****                                    |
       128 -> 255        : 18       |****************************************|
       256 -> 511        : 0        |                                        |
       512 -> 1023       : 1        |**                                      |
disk = b'sda'
     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        : 1        |****************************************|
       256 -> 511        : 1        |****************************************|

3.ext4slower

ext4slower通过跟踪ext4文件系统的readwrite,open,sync等操作,然后测量相应操作所需要的时间,打印操作min_ms域值的详细信息(min_ms默认最小阈值为10ms,可以自定义大小,如果为0,即打印所有的事件)

tps:ext4slower可以通过文件系统识别独立较慢的磁盘IO

常用参数:可以通过 ext4slwoer -h获取

代码语言:javascript复制
-h, --help:查看帮助信息
-j, --csv:使用csv格式打印字段
-p PID, --pid PID:只追踪PID进程
min_ms:追踪IO的阈值,默认为10

示例:

代码语言:javascript复制
ext4slower-bpfcc -j 0
ENDTIME_us,TASK,PID,TYPE,BYTES,OFFSET_b,LATENCY_us,FILE
97192712765,nginx,92709,O,0,0,3,0003111647
97192712810,nginx,92709,W,8883,0,22,0003111647
97192712834,nginx,92709,W,8192,8883,7,0003111647
97192712845,nginx,92709,W,8192,17075,6,0003111647

4.execsnoop

execsnoop通过追踪exec系统调用追踪新进程,对于使用fork而不是exec产生的进程不会包括在显示结果中。

execsnoop需要BPF支持,因此需要root权限。

代码语言:javascript复制
execsnoop [-h] [-T] [-t] [-x] [-q] [-n NAME] [-l LINE] [--max-args MAX_ARGS]
-h:查看帮助信息
-T:打印时间戳,格式HH:MM:SS
-t:打印时间戳
-x:包括失败exec
-n NAME:只打印正则表达式匹配name的命令行
-l LINE:只打印参数中匹配LINE的命令行
–max-args MAXARGS:解析和显示最大参数数量,默认为20个

BCC还有很多非常实用的命令工具,感兴趣的可以看下文章底部推荐阅读的其他博文连接。

BCC的编程开发

BCC是eBPF的一个工具集,是对eBPF提取数据的上层封装,BCC工具编程形势是pyth9on中嵌套BPF程序。python代码可以使我们更好的使用eBPF的上层接口,并且同时对数据进行处理。BPF程序会注入内核,提取数据。

eBPF的开发执行过程可以参考linux性能优化大神Brendan Gregg博文的一张图来说明:

基本可以分为五步:

1,使用C语言开发一个eBPF的程序

2,通过LLVM将eBPF的程序进行编译,得到BPF字节码的指令集

3,通过bpf_load_program 方法把BPF字节码提交给内核(注入内核)

4,内核会对注入的字节码进行一系列的安全检查,通过检查的eBPF字节码使用内核JIT进行编译,生成汇编指令,附加到内核特定挂钩的程序,并把相应的状态保存到BPF映射中

5,用户程序通过BPF映射查询BPF字节码的运行状态(BCC工具在用户态使用Python进行数据处理)

接下来我们使用BCC库开发一个跟踪openat()(开发文件)这个系统调用的eBPF程序(一个简化版的 opensnoop-bpfcc  工具)

简单的实验环境搭建

代码语言:javascript复制
# 创建和启动Ubuntu 21.10虚拟机
vagrant init ubuntu/impish64
vagrant up

# 登录到虚拟机
vagrant ssh


# For Ubuntu20.10  安装对应的工具
sudo apt-get install -y  make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-$(uname -r) linux-headers-$(uname -r)

代码:

代码语言:javascript复制
#!/usr/bin/python3
#导入了 BCC  库的 BPF 模块
from bcc import BPF
#使用 C 写一个 eBPF 程序
bpf_program = '''
int hello_world(void *ctx)
{
    bpf_trace_printk("Hello, eBPF!");
    return 0;
}'''
if __name__ == "__main__":
    #调用 BPF() 加载 BPF 源代码
    bpf = BPF(text=bpf_program)
    #将 BPF 程序挂载到内核探针(简称 kprobe),其中 do_sys_openat2() 是系统调用 openat() 在内核中的实现
    bpf.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
    # 读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe 的内容,并打印到标准输出中
    bpf.trace_print()

执行:注意eBPF程序需要使用root用户执行

代码语言:javascript复制
sudo python3 tracing_hello_bpf.py
b'         python3-65619   [000] d... 70131.500166: bpf_trace_printk: Hello, eBPF!'
b'         python3-65619   [000] d... 70131.500413: bpf_trace_printk: Hello, eBPF!'
b'         python3-65619   [000] d... 70131.500765: bpf_trace_printk: Hello, eBPF!'
b'           <...>-65671   [000] d... 70194.518018: bpf_trace_printk: Hello, eBPF!'
b'      multipathd-492     [001] d... 70195.007095: bpf_trace_printk: Hello, eBPF!'
b'      multipathd-492     [001] d... 70195.007470: bpf_trace_printk: Hello, eBPF!'
b' systemd-journal-351     [000] d... 70195.007594: bpf_trace_printk: Hello, eBPF!'
b' systemd-journal-351     [000] d... 70195.007656: bpf_trace_printk: Hello, eBPF!'
b' systemd-journal-351     [000] d... 70195.007681: bpf_trace_printk: Hello, eBPF!'
b' systemd-journal-351     [000] d... 70195.007705: bpf_trace_printk: Hello, eBPF!'

如果想修改默认输出的格式可以修改/sys/kernel/debug/tracing/trace_options  文件内容。

每一字段的含义:

代码语言:javascript复制
python3-65619 表示进程的名字和 PID;
[000] 表示 CPU 编号;
d… 表示一系列的选项;
70131.500166 表示时间戳;
bpf_trace_printk 表示函数名;
最后的 “Hello, eBPF!” 就是调用 bpf_trace_printk() 传入的字符串

eBPF程序优化

在上面的eBPF程序执行步骤的第五步中提到,用户程序通过BPF映射查询BPF字节码的运行状态(BCC工具在用户态使用Python进行数据处理),我们可以引入eBPF映射来优化程序的输出

为简化BPF映射的交互,BCC封装了一系列的库函数和辅助宏定义。https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#output

我们可以使用BPF_PERF_OUTPUT 来定义一个Perf事件类型的BPF映射

代码语言:javascript复制
// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>
// 定义数据结构
struct data_t {
  u32 pid;
  u64 ts;
  char comm[TASK_COMM_LEN];
  char fname[NAME_MAX];
};
// 定义性能事件映射
BPF_PERF_OUTPUT(events);

然后我们可以在eBPF程序中,填充这个数据结构,并调用perf_submit() ,把数据提交到上面定义的BPF映射中

代码语言:javascript复制
// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
  struct data_t data = { };
  // 获取PID和时间
  data.pid = bpf_get_current_pid_tgid();
  data.ts = bpf_ktime_get_ns();
  // 获取进程名
  if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0)
  {
    bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
  }
  // 提交性能事件
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

注解:

上面以bpf开头的函数都是eBPF提供的辅助函数

代码语言:javascript复制
bpf_get_current_pid_tgid 用于获取进程的 TGID 和 PID。因为这儿定义的 data.pid 数据类型为 u32,所以高 32 位舍弃掉后就是进程的 PID;
bpf_ktime_get_ns 用于获取系统自启动以来的时间,单位是纳秒;
bpf_get_current_comm 用于获取进程名,并把进程名复制到预定义的缓冲区中;
bpf_probe_read 用于从指定指针处读取固定大小的数据,这里则用于读取进程打开的文件名。

上面的程序就是把内核eBPF程序的运行状态注入到BPF映射,这时就不再需要调用bpf_trace_printk() 函数了,可以直接在用户态从BPF映射读取内核eBPF程序运行的状态。但是需要传入一个回调函数,用于处理从perf事件类型的BPF映射中读到的数据。

具体程序:

代码语言:javascript复制
from bcc import BPF
# 加载 eBPF 程序并挂载到内核探针上
b = BPF(src_file="new_hello.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 输出一行 Header 字符串表示数据的格式;
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))
# print_event 定义一个函数处理的回调函数,打印进程的名字,PID以及它调用openat时打开的文件
start = 0
def print_event(cpu, data, size):
    global start
    event = b["events"].event(data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))
# open_perf_buffer 定义了名为 “events” 的 Perf 事件映射,
# 而后通过一个循环调用 perf_buffer_poll 读取映射的内容,并执行回调函数输出进程信息。
b["events"].open_perf_buffer(print_event)
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

运行结果

代码语言:javascript复制
sudo python3 tracing_ebpf_open.py
TIME(s)            COMM             PID    FILE
0.000000000        b'multipathd'    492    b'/sys/devices/pci0000:00/0000:00:14.0/host2/target2:0:1/2:0:1:0/state'
0.000364162        b'multipathd'    492    b'/sys/devices/pci0000:00/0000:00:14.0/host2/target2:0:1/2:0:1:0/block/sdb/size'
0.000935231        b'multipathd'    492    b'/sys/devices/pci0000:00/0000:00:14.0/host2/target2:0:1/2:0:1:0/state'
0.000960102        b'multipathd'    492    b'/sys/devices/pci0000:00/0000:00:14.0/host2/target2:0:1/2:0:1:0/vpd_pg80'
0.001323619        b'multipathd'    492    b'/sys/devices/pci0000:00/0000:00:14.0/host2/target2:0:1/2:0:1:0/vpd_pg83'
0.000389784        b'systemd-journal' 351    b'/proc/488/status'
0.000480717        b'systemd-journal' 351    b'/proc/488/status'

追踪文件打开事件,采用场景大致有:

1、查看某个程序启动时加载了哪些配置文件,便于确认是否加载了正确的配置文件。对于允许自定义配置文件路径的程序尤其有用,例如 MySQL、PostgreSQL。

2、查看是否存在频繁或周期性打开某些文件的情况,考虑是否存在优化可能。比如周期性打开某个极少变化的文件,可以一次性读取,且监听文件变动事件,避免多次打开读取。

3、分析依赖 /proc、/sys 等虚拟文件系统的 Linux 工具大致工作原理。比如执行 vmstat,,可以通过追踪文件打开事件看到至少打开了 /proc/meminfo、/proc/stat、/proc/vmstat 这几个文件,帮助你更好的理解工具的数据源与实现原理。

4、分析 K8s、Docker 等 cgroup 相关操作。比如 docker run xxx 时,可以看到 /sys/fs/cgroup/cpuset/docker/xxx/cpuset.cpus、/sys/fs/cgroup/cpuset/docker/xxx/cpuset.mems 等 cgroup 文件被打开,也可以查看 kube-proxy 在周期性刷新 cgroup 相关文件。

eBPF的工具tcpdump

tcpdump对于sre来说是一个分析网络问题的利器,具体的使用与技巧这边就不再描述。而是重点结合eBPF说下它的工作原理,让我们可以更深入的理解tcpdump是什么,能分析什么类型的问题

tcpdump的大致原理

tcpdump抓包使用的是libacp的机制。大致原理:

    在收发包时,如果该包符合tcpdump设置的规则(BPD filter),就会把这个包copy一份到tcpdump的内核缓冲区,然后以PACKET_NMAP的方式将这部分内存映射到tcpdump用户空间,解析后就会把这些内容输出了。

根据原理图我们可以看到tcpdump的一些局限性:

  • 在收包的时候,如果网络包已经被网卡丢弃了,那么tcpdump是抓不到的
  • 在发包的时候,如果网络包在协议栈里被丢弃(发送缓冲区满了而被丢弃),tcpdump是抓不到的
  • tcpdump在抓包的时候开销比较大,这主要在于BPF过滤器。在系统中存在非常多的TCP连接的机器使用TCPDUMP是一个需要谨慎的操作

tcpdump的性能相对差与eBPF的性能好是否有冲突?why?

代码语言:javascript复制
    ebpf通常情况下性能损耗要小一些,因为他在内核里会进行处理,省去了很多无谓的流程开销,或者说它更有针对性,针对某个特定点来追踪,那么这个点之外的逻辑就不会受影响;而tcpdump一是处理流程长,更耗时,二是它不具有针对性,分析面广,这就导致它的开销大。
结合eBPF工具排查磁盘io问题
  1. pidstat排查是哪个进程引起I/O瓶颈
  2. strace -p pid跟踪该进程的系统调用
  3. filetop -C跟踪内核中文件的读写情况.
  4. ps -efT | grep 进程PID
  5. opensnoop查看系统调用打开的所有文件
  6. 结合filetop和opensnoop分析问题根因

参考文章与推荐阅读博文

https://github.com/DavadDi/bpf_study

https://github.com/iovisor/bcc

https://www.ebpf.top/post/ebpf-overview-part-3/

https://arthurchiao.art/blog/trace-packet-with-tracepoint-perf-ebpf-zh/

可以查看原文:

https://mp.weixin.qq.com/s?__biz=MzA5NTgwNzY1NA==&mid=2247483910&idx=1&sn=d548d46497b56164ae7a1c9272eb30b9&chksm=90b8f3cfa7cf7ad9fd3ad4e7a08a6d07bc2fd82c5b78b692ffb5297bb022075ffda7f2b932c9&token=1735328410&lang=zh_CN#rd

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞