因为微信排版比较不好,推荐使用这里的链接阅读博文或者点阅读原文
https://dirt-sunshine-933.notion.site/eBPF-5204eaba146c4680af674efc746050b2
在< >这篇文章中,我们提到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的安装
ubuntuapt-get install bpfcc-tools linux-headers-$(uname -r)centosyum install bcc-toolsexport 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 获取
-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获取
-h Print usage message.-T:输出包含时间戳-m:输出ms级直方图-D:打印每个磁盘设备的直方图-F:打印每个IO集的直方图interval:输出间隔count:输出数量
sudo biolatency-bpfcc -D 1 5disk = b'sda'usecs : count distribution0 -> 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 distribution0 -> 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获取
-h, --help:查看帮助信息-j, --csv:使用csv格式打印字段-p PID, --pid PID:只追踪PID进程min_ms:追踪IO的阈值,默认为10
示例:
ext4slower-bpfcc -j 0ENDTIME_us,TASK,PID,TYPE,BYTES,OFFSET_b,LATENCY_us,FILE97192712765,nginx,92709,O,0,0,3,000311164797192712810,nginx,92709,W,8883,0,22,000311164797192712834,nginx,92709,W,8192,8883,7,000311164797192712845,nginx,92709,W,8192,17075,6,0003111647
4.execsnoop
execsnoop通过追踪exec系统调用追踪新进程,对于使用fork而不是exec产生的进程不会包括在显示结果中。
execsnoop需要BPF支持,因此需要root权限。
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 工具)
简单的实验环境搭建
# 创建和启动Ubuntu 21.10虚拟机vagrant init ubuntu/impish64vagrant 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)
代码:
#!/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用户执行
sudo python3 tracing_hello_bpf.pyb' 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 文件内容。
每一字段的含义:
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映射
// 包含头文件// 定义数据结构struct data_t {u32 pid;u64 ts;char comm[TASK_COMM_LEN];char fname[NAME_MAX];};// 定义性能事件映射BPF_PERF_OUTPUT(events);
然后我们可以在eBPF程序中,填充这个数据结构,并调用perf_submit() ,把数据提交到上面定义的BPF映射中
// 定义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提供的辅助函数
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映射中读到的数据。
具体程序:
from bcc import BPFb = BPF(src_file="new_hello.c")b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))start = 0def print_event(cpu, data, size):global startevent = b["events"].event(data)if start == 0:start = event.tstime_s = (float(event.ts - start)) / 1000000000print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))b["events"].open_perf_buffer(print_event)while 1:try:b.perf_buffer_poll()except KeyboardInterrupt:exit()
运行结果
sudo python3 tracing_ebpf_open.pyTIME(s) COMM PID FILE0.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?
ebpf通常情况下性能损耗要小一些,因为他在内核里会进行处理,省去了很多无谓的流程开销,或者说它更有针对性,针对某个特定点来追踪,那么这个点之外的逻辑就不会受影响;而tcpdump一是处理流程长,更耗时,二是它不具有针对性,分析面广,这就导致它的开销大。
结合eBPF工具排查磁盘io问题
pidstat排查是哪个进程引起I/O瓶颈
strace -p pid跟踪该进程的系统调用
filetop -C跟踪内核中文件的读写情况.
ps -efT | grep 进程PID
opensnoop查看系统调用打开的所有文件
结合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/