week23
eBPF学习
如何调试eBPF程序?
bpf SystemCall
[bpf(2) - Linux manual page (man7.org)](https://man7.org/linux/man-pages/man2/bpf.2.html)
[bpf-helpers(7) - Linux manual page (man7.org)](https://man7.org/linux/man-pages/man7/bpf-helpers.7.html)
用户态eBPF程序与内核交互的唯一路径是bpf系统调用:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
BPF 系统调用接受三个参数:
- cmd ,代表操作命令,比如
BPF_PROG_LOAD
就是加载eBPF程序. - attr,代表 bpf_attr 类型的 eBPF 属性指针,不同类型的操作命令需要传入不同的属性参数.
- size ,代表属性的大小。
bpf系统调用看似简单,但是包括了bpf的所有功能。在内核5.15中,第一个参数cmd支持37个宏定义命令([bpf.h - include/uapi/linux/bpf.h - Linux source code (v5.15) - Bootlin](https://elixir.bootlin.com/linux/v5.15/source/include/uapi/linux/bpf.h#L838)),包括map的创建、增删改查;加载eBPF程序。。。
在内核文档中的示例:
int bpf_prog_load(enum bpf_prog_type type,
const struct bpf_insn *insns, int insn_cnt,
const char *license) {
union bpf_attr attr = {
.prog_type = type,
.insns = ptr_to_u64(insns),
.insn_cnt = insn_cnt,
.license = ptr_to_u64(license),
.log_buf = ptr_to_u64(bpf_log_buf),
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}
//---------------
int
bpf_create_map(enum bpf_map_type map_type,
unsigned int key_size,
unsigned int value_size,
unsigned int max_entries)
{
union bpf_attr attr = {.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries};
return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}
可以看出,bpf程序在用户空间使用的bpf_prog_load(),bpf_create_map()
都是bpf()
系统调用的封装。
strace追踪bpf系统调用
strace常用来跟踪进程执行时的系统调用和接收所接收的信号。strace可以跟踪到一个进程产生的系统调用,包括 参数,返回值,执行消耗的时间。
通过参数-ebpf
,能够过滤出bpf系统调用:
tzx@tzx:~/ebpf_monitor/libbpf/src$ sudo strace -ebpf ./network_monitor
...
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_SOCKET_FILTER, insn_cnt=2, insns=0xfffff5ee6d18, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(0, 0, 0), prog_flags=0, prog_name="", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=0, func_info_rec_size=0, func_info=NULL, func_info_cnt=0, line_info_rec_size=0, line_info=NULL, line_info_cnt=0, attach_btf_id=0, attach_prog_fd=0}, 116) = 8
...
bpf(BPF_BTF_LOAD, {btf="\237\353\1\0\30\0\0\0\0\0\0\0\20\0\0\0\20\0\0\0\5\0\0\0\1\0\0\0\0\0\0\1"..., btf_log_buf=NULL, btf_size=45, btf_log_size=0, btf_log_level=0}, 32) = 8
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_RINGBUF, key_size=0, value_size=0, max_entries=4096, map_flags=0, inner_map_fd=0, map_name="ringbuf", map_ifindex=0, btf_fd=8, btf_key_type_id=0, btf_value_type_id=0, btf_vmlinux_value_type_id=0, map_extra=0}, 72) = 9
libbpf: map 'ringbuf': created successfully, fd=9
...
load success.
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=9, info_len=88 => 80, info=0xfffff5ee6fd0}}, 16) = 0
^Cstrace: Process 31846 detached
利用strace追踪bpf系统调用,能够捕获到被追踪的eBPF程序进行了哪些bpf系统调用,还能看到具体的参数值以及返回值(文件描述符,可以用于上下文分析)。
bpftool
bpftool是一个用于操作和调试 BPF 程序的命令行工具。它可以用于列出、加载、卸载和查询 BPF 程序、映射和事件等。其源码位于内核源码中:[bpftool « bpf « tools - kernel/git/bpf/bpf-next.git - BPF next kernel tree](https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/tree/tools/bpf/bpftool)
常用的 bpftool 命令:
bpftool prog show
:列出所有 BPF 程序bpftool map show
:列出所有 BPF mapbpftool event show
:列出所有 BPF 事件bpftool prog load
:加载一个 BPF 程序bpftool prog unload
:卸载一个 BPF 程序
在调试 BPF 程序时:
bpftool prog dump type <prog_type>
:显示指定类型的所有 BPF 程序的汇编代码bpftool prog tracelog <prog_id>
:跟踪指定的 BPF 程序的执行并显示其输出bpftool map dump id <map_id>
:显示指定map的内容,以json格式输出bpftool map update name <map_name> key 0xc1 0xc2 value 0xa1 0xa2
:直接操作map
Kindling学习
[Kindling - eBPF-based Cloud Native Monitoring tool (harmonycloud.cn)](http://kindling.harmonycloud.cn/)
[网易伏羲私有云基于eBPF的云原生网络可观测性探索与实践_开源_石钟浩_InfoQ精选文章](https://www.infoq.cn/article/2U1Hj6PwavlgjAsEdSCU)
近期在思考eBPF性能监控程序的开发的架构问题:
- 如何选择合适的eBPF程序开发框架
- 如何屏蔽其他组件对系统性能的影响
遂阅读了国内互联网公司对eBPF在APM方面的应用。“kindling是一款基于 eBPF 的云原生可观测性开源工具,旨在帮助用户更好、更快地定界(triage)云原生系统故障。通过 kindling,用户可以快速定界问题类型,比如是应用代码问题还是基础设施问题。如果是代码问题,可以借助 APM(Application Performance Monitoring 应用性能监控)监控进一步排查问题;如果是基础设施问题,那么通过分析来自内核的相关监控指标,定位故障点。”
博客(发布日期2023-03-14)以及官方文档中展示的kindling架构:
通过架构图,可以得出:
- 与eBPF核心无关的用户态程序由Golang开发。
- eBPF核心程序使用C/C++进行开发。
- 在node中所做的工作仅为监控事件数据的采集、解析,最后将数据投递到可观测分析平台。
- 可观测分析平台作为一个独立的组件,数据到达的第一站为
Data Receiver
,后面流向Kafka、Analyzer、ES…等组件,最后由web前端进行展示。
通过阅读源码以及文档,学习真实项目中如何写eBPF程序:
-
eBPF开发库选型
- [website/content/en/blogs/development/ebpf-library/index.md at cd681ab70628f51bd150c382959030d726e2e30f · KindlingProject/website (github.com)](https://github.com/KindlingProject/website/blob/cd681ab70628f51bd150c382959030d726e2e30f/content/en/blogs/development/ebpf-library/index.md)
- 文章发布于2022.11,当时有几种选择:再创造一个eBPF的基础设施;libbcc;cilium-ebpf(纯Golang实现);Falco-libs(纯c语言的实现)…
- 为何选择Falco-libs?:低内核版本支持;事件信息丰富(自动补全事件分类、上下文、线程名称)。
- 做出了哪些改造:支持了kprobe。
- 未来:随着内核版本3.x的逐渐淘汰,有理由选择更优的libbcc;cilium-ebpf。
-
drivers程序源码阅读:
-
[agent-libs/driver at kindling-dev · KindlingProject/agent-libs (github.com)](https://github.com/KindlingProject/agent-libs/tree/kindling-dev/driver)
-
forked from [falcosecurity/libs](https://github.com/falcosecurity/libs)
-
driver/bpf/maps.h
文件中,定义了全局所有的map,包括perf_map
、cpu_records
等等。 -
perf_map
的提交操作被封装在了driver/bpf/ring_helpers.h
的push_evt_frame
函数中//https://github.com/KindlingProject/agent-libs/blob/e6f73c5fa3e37073cf811a42782cc40591a422f6/driver/bpf/maps.h#L24 struct bpf_map_def __bpf_section("maps") perf_map = { .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, .key_size = sizeof(u32), .value_size = sizeof(u32), .max_entries = 0, }; //https://github.com/KindlingProject/agent-libs/blob/e6f73c5fa3e37073cf811a42782cc40591a422f6/driver/bpf/ring_helpers.h#L45C6-L45C6 static __always_inline int push_evt_frame(void *ctx, struct filler_data *data) { ... int res = bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU, data->buf, ((data->state->tail_ctx.len - 1) & SCRATCH_SIZE_MAX) + 1);//perf_map的唯一操作 ... if (res == -ENOENT || res == -EOPNOTSUPP) { ... bpf_printk("detected hotplug event, cpu=%d\n", state->hotplug_cpu); } ... return PPM_SUCCESS; } //https://github.com/KindlingProject/agent-libs/blob/e6f73c5fa3e37073cf811a42782cc40591a422f6/driver/bpf/fillers.h#L5185C37-L5185C37 static __always_inline int bpf_cpu_analysis(void *ctx, u32 tid) { struct filler_data data; int res; res = init_filler_data(ctx, &data, false); ... if (res == PPM_SUCCESS) res = push_evt_frame(ctx, &data);//在逻辑函数中调用push_event ... return 0; }
-
通过map的梳理,能够看出:1. 整个内核态的eBPF程序较为复杂,所有组件协作对外提供一套完整的功能。2.各模块有较为独立的封装,除了map模块,还有filler等一系列功能模块。
-
总结:
- why eBPF?
- “The libpcap way of analyzing the flows in the Kubernetes environment is too expensive for the CPU and network. The eBPF way of data capture cost much less than libpcap. eBPF is the most popular technology to track the Linux kernel where the virtual network, built by veth-pair and iptables, works. So eBPF is a proper technique to be used for tracking how the kernel responds to application requests.”
- 性能损耗、流行性(生态好)、对Linux Kernel network模块契合度高…
- 管理、处理数据的用户态程序使用Golang是最优解。
- docker、k8s均使用Golang开发,契合云原生环境。
- 用户态Go程序满足的是上层可观测需求的开发,其他两个部分实现的是内核需求的开发。这样不同领域的人可以用自己擅长的语言开发自己关注的内容,同时探针也有较好的松耦合特性。
- 与eBPF核心相关的程序还是需要用libbpf实现。
- 内核是由C语言开发的,内核态的eBPF程序必须要用C实现。
- BCC工具适合快速验证功能,其动态编译运行(运行时需要内核头文件、llvm)的特点不适合生产环境。
- 自己如何开发具有完善功能的eBPF程序
- 可行性验证使用BCC,bpftrace等工具,正式实现使用libbpf、ebpf-go等库。
- 考虑内核态的程序的可维护性,需要将各功能抽象独立成独立的模块。
- 掌握makefile的书写,如何编译ebpf程序。
后续在开发自己的ebpf程序时,需要注意以上问题。
uapi
[uapi - include/uapi - Linux source code (v5.15) - Bootlin](https://elixir.bootlin.com/linux/v5.15/source/include/uapi)
在Linux内核中,userspace api是一组函数和系统调用,用于在用户空间(userspace)和内核空间(kernelspace)之间进行通信。通过这些api,用户空间的程序可以请求内核执行某些操作,例如读取或写入文件、创建或删除进程等。
内核源码中的/include/uapi/
路径下。当内核被编译时,uapi头文件会被放置在/usr/include
目录或其他指定的目录中,以便用户空间程序可以包含它们。
例如,编写一个C程序来使用open()和read()系统调用,可以包含以下头文件:
#include <fcntl.h>
#include <unistd.h>
这些头文件实际上是从内核源代码中的uapi目录中提取出来的。在编译时,编译器将查找这些头文件,并将它们包含在最终的可执行文件中。
通过diff
命令比较/usr/src/linux-headers-5.15.0-76/include/uapi/linux/bpf.h
&/usr/include/linux/bpf.h
可以看出,两头文件除了开头结尾的预防式声明宏定义不同,其他都一样。而后者就是在编写eBPF程序时使用的<linux/bpf.h>
头文件。
通过查阅资料,uapi路径是linux Kernel3.7中引入的:一个是解决 Linux Kernel 里的交叉引用;另外一个就是方便用户态的开发者,可以简单的查看 uapi 里的代码变化来确定 Linux Kernel 是否改变了系统 API。
在BCC中,有很多地方引入了uapi路径下的头文件:
由BCC的安装、使用指南可知,BCC的运行依赖于内核头文件。
# BCC官方文档中展示如何安装bcc工具
sudo apt-get install bpfcc-tools linux-headers-$(uname -r)