eBPF 是一个用于访问 Linux 内核服务和硬件的新技术,因为其灵活性和高性能等特色,被迅速用于网络、出错、跟踪以及防火墙等多场景。目前国内已有少数企业开始尝试将 eBPF 引入生产实践,又拍云也是其中一个。专为技术开发者提供知识分享的 Open Talk 公开课邀请了又拍云开发工程师周晨约直播分享 eBPF 的学习经验与开发心得,并对其分享内容进行整理,下拉至文末点击阅读原文可回看原视频。html
你们好,今天分享的主题是《eBPF 探索之旅》,围绕三部分展开:前端
eBPF 是什么,从字面上来看是扩展伯克利包处理器,那伯克利包处理器是什么呢?python
在此以前先来了解一个性能优秀的经常使用抓包工具:tcpdumpios
tcpdumpnginx
图中展现了两个经常使用指令网络
指令一:指定 IP 和端口,能够抓到 IP 为 220.173.103.227,端口为 80 的包架构
指令二:加上 grep,能够过滤出带有 route 字段的数据socket
那么 tcpdump 又是如何作到经过用户提供的规则处理网络上收到的包,再 copy 给用户的呢?若是放在用户层,就须要在系统里全部 socket 读写的时候作一层处理,把规则放上去,这样作难度太大。而 tcpdump 是基于 libpcap 库实现的,libpcap 能作到在驱动将包交给内核网络时,把包取过来,经过用户传给 libpcap 的规则将须要的网络包 copy 一份给用户,再把包传给内核网络栈,而之因此 libpcap 能作到这点全靠 BPF。tcp
BPF函数
BPF 是基于寄存器虚拟机实现的,支持 jit,比基于栈实现的性能高不少。它能载入用户态代码而且在内核环境下运行,内核提供 BPF 相关的接口,用户能够将代码编译成字节码,经过 BPF 接口加载到 BPF 虚拟机中,固然用户代码跑在内核环境中是有风险的,若有处理不当,可能会致使内核崩溃。所以在用户代码跑在内核环境以前,内核会先作一层严格的检验,确保没问题才会被成功加载到内核环境中。
eBPF:BPF 的扩展
回到 eBPF,它做为一个 BPF 的扩展,都扩展了些什么呢?
编写 eBPF 程序的内核最低也要是 3.15,此版本恰好能够支持 eBPF ,但这时 eBPF 支持的特性比较少,不建议使用,最好是 4.8 以上的内核,内核越新 eBPF 支持的功能就越成熟。另外像 kprobe、uprobe、traceport 相关的参数要开起来,不然只能用 BPF的某些特性,而没法使用eBPF 的特性,至关因而空壳。经过路径 /lib/modules/uname-r
/source/.config 或者在 /boot/ 下查找对应版本的内核 config 来查看系统是否开启了所需的参数。
编写 eBPF 程序的对环境也有必定的要求。eBPF 代码须要编译成 llvm 的字节码,才可以在 eBPF 及虚拟机中运行,所以须要安装 llvm 以及 clang,安装好以后能够经过 llc 来查看是否支持 BPF。
eBPF 代码示例
内核、环境都准备好后就能够开始编写工做了。若是是不借助任何工具直接手写一个 eBPF 程序会很是的困难,由于内核提供的文档对如何编写 eBPF 程序的说明是比较缺少的。固然内核也有提供工具,在内核包中的 bpftool 工具。推荐是使用工具 bcc,它可以下降写 BPF 程序的难度,提供了python、lua 的前端。以 python 为例,只须要写好须要载入 eBPF 的 C代码,再经过 bcc 提供的 BPF 类就能够将代码载入到 eBPF 虚拟机中,执行 python 程序,代码就能够运行起来了。
图中是 bcc 工具的使用例子,代码很是简单,导入一下 BPF,进行 BPF 初始化。
下面是经过 kprobe 监控机器 tcp(ipv4)的链接状态变化。首先须要知道 tcp 状态变化时内核会调用哪些函数。除了 time-wait 状态以外,其余状态基本上是经过 tcp_set_state 设置的。在 time-wait 阶段的时候,内核会建立一个新的结构体去存 time-wait 的 socket,内核考虑到内存的开销问题,以前的 socket 会释放掉。先不考虑 time-wait。
接下来看看具体的代码,上图中是载入到 eBPF 的 C 代码。
经过内核的几个参数,内核的结构体 socket,以及这个函数传进来的一些 state,能够获取当时 tcp 链接的状态转化状况,上图函数的第一个参数 ctx 其实是寄存器,后面是要介入函数的两个参数。这里会把一些 tcp 的状态存起来,使用 perf_submit 将这些状态更新到 perf ring buffer 中,就能够在用户态把 perf ring buffer 东西给读出来,这就是 tcp 的一些状态变化。
上图是 python 代码。
利用 uprobe 查看应用服务信息
上图是经过 uprobe 查看 nginx 请求分布的状况。首先要看 nginx 建立请求的位置,是在 ngx_http_create_request,和以前同样写一个要嵌入 eBPF 虚拟机的 C 代码,仍是建立一个 HASH 表,名称是 req_distr,key 是 32 位大小,value 是 64 位,核心函数是 check_ngx_http_create_request,在 nginx 调用该函数时,会执行这个钩子函数,函数内部调用的是 count_req。把 PID 和 PID 上建立的请求次数对应起来,当 PID 调用过 ngx_http_create_request 时,请求计数就会 +1。如此也就能够看到整个请求在各个 work 上的分布状况。
图中是 python 代码,一样把 C 代码读进来,并调用 bbf 把代码编译成 llvm 字节码,载入到 eBPF 虚拟机中,再调用 attach_uprobe。name 是指 nginx 的一个二进制文件,sym 是指要在哪一个函数中打个断点,上图是 ngx_http_create_request 函数。fn_name 是在 ngx_http_create_request 函数执行的时候须要调用的函数。另外须要注意二进制文件必需要把编译符号开放出来,好比编译的时加个 -g,不然会找不到这个函数。最下面是简单地获取 HASH 表,去输出 HASH 表的 key 和 value,这样就能看到 pid 对应的 request 数量,pid 也就会对应着 worker,如此就可以查看到运行 nginx 的请求分布状况。
查看运行中的 eBPF 程序与 map
能够经过内核包中 bpftool 提供的 bpftool 工具查看,它的目录是在 /lib/modules/uname-r
/tools/bpf/bpftool 中,须要本身编译一下,在 /lib/modules/uname-r
/tools 下执行 make-C/bpf/bpftool 就能够了。
上图是 bpftool 工具查看 map(前面 BPF_HASH 建立的)状况的效果,-p 参数,可以展现得好看一些。prog 参数能够把在虚拟机中跑的程序给展现出来。这样就能看到到底运行了那些 eBPF 程序以及申请的 map。