共计 5055 个字符,预计需要花费 13 分钟才能阅读完成。
eBPF 是一个用于拜访 Linux 内核服务和硬件的新技术,因为其灵活性和高性能等特点,被迅速用于网络、出错、跟踪以及防火墙等多场景。目前国内已有多数企业开始尝试将 eBPF 引入生产实践,又拍云也是其中一个。专为技术开发者提供常识分享的 Open Talk 公开课邀请了又拍云开发工程师周晨约直播分享 eBPF 的学习教训与开发心得,并对其分享内容进行整顿,下拉至文末点击浏览原文可回看原视频。
大家好,明天分享的主题是《eBPF 探索之旅》,围绕三局部开展:
- eBPF 是什么
- eBPF 能做什么
- 如何编写 eBPF 程序
意识 eBPF
eBPF 是什么,从字面上来看是扩大伯克利包处理器,那伯克利包处理器是什么呢?
在此之前先来理解一个性能优良的罕用抓包工具:tcpdump
tcpdump
图中展现了两个罕用指令
指令一:指定 IP 和端口,能够抓到 IP 为 220.173.103.227,端口为 80 的包
指令二:加上 grep,能够过滤出带有 route 字段的数据
那么 tcpdump 又是如何做到通过用户提供的规定解决网络上收到的包,再 copy 给用户的呢?如果放在用户层,就须要在零碎里所有 socket 读写的时候做一层解决,把规定放上去,这样做难度太大。而 tcpdump 是基于 libpcap 库实现的,libpcap 能做到在驱动将包交给内核网络时,把包取过去,通过用户传给 libpcap 的规定将须要的网络包 copy 一份给用户,再把包传给内核网络栈,而之所以 libpcap 能做到这点全靠 BPF。
BPF
BPF 是基于寄存器虚拟机实现的,反对 jit,比基于栈实现的性能高很多。它能载入用户态代码并且在内核环境下运行,内核提供 BPF 相干的接口,用户能够将代码编译成字节码,通过 BPF 接口加载到 BPF 虚拟机中,当然用户代码跑在内核环境中是有危险的,如有处理不当,可能会导致内核解体。因而在用户代码跑在内核环境之前,内核会先做一层严格的测验,确保没问题才会被胜利加载到内核环境中。
eBPF:BPF 的扩大
回到 eBPF,它作为一个 BPF 的扩大,都扩大了些什么呢?
- 首先在性能上,不仅仅局限于网络,它可能借助 kprobe 获取内核函数运行信息,这样调试内核就不须要 gdb 或者退出内核探点从新编译内核。
- 能够借助 uprobe 获取用户函数的运行信息,kprobe 和 uprobe 不仅能获取函数经营信息,还能够获取代码执行到了哪一行时的寄存器以及栈信息,其原理能够了解为在某个指令打断点,当 cpu 执行到这个断点的时候,cpu 会保留以后的寄存器信息,而后单步执行断点持载的 handler,也是想要在内核中执行的逻辑,执行实现后 cpu 会回到这个断点的地位,复原寄存器的状态,而后持续运行上来。
- 反对 tracepoint,即在写代码中退出 trace 点,获取执行到这点时的信息。
- 能够嵌入到 perf_event 中。咱们熟知的 XDP 以及 tc 都是基于 eBPF 实现的,并且在性能上有着不俗的体现。
eBPF 的性能
- 零碎性能监控 / 剖析工具:可能实现性能监控工具、剖析工具等罕用的系统分析工具,比方 sysstate 工具集,外面提供了 vmstate,pidstat 等多种工具,一些罕用的 top、netstat(netstat 可被 SS 替换掉),uptime、iostat 等这些工具少数都是从 /proc、/sys、/dev 中获取的会对系统产生肯定的开销,不适宜频繁的调用。比方在应用 top 的时候通过 cpu 排序能够看到 top cpu 占用也是挺高的,应用 eBPF 能够在开销绝对小的状况下获取零碎信息,定时将 eBPF 采集的数据 copy 到用户态,而后将其发送到剖析监控平台。
- 用户程序活体剖析:做用户程序活体剖析,比方 openresty 中 lua 火焰图绘制,程序内存应用监控,cdn 服务异样申请剖析,程序运行状态的查看,这些操作都能够在程序无感的状况下做到,能够无效提供服务质量。
- 进攻攻打:比方 DDoS 攻打,DDoS 攻打次要是在第七层、第三层以及第四层。第七层的攻打如 http 攻打,须要应用服务这边解决。第四层攻打,如 tcp syn 能够通过 iptable 回绝异样的 ip,当然前提是能发现以及难点是如何辨别失常流量和攻打流量,简略的防攻打会导致一些误伤,另外 tcp syn 也能够通过内核参数爱护应用服务。第 3 层攻打,如 icmp。对于攻打个别会通过一些非凡的路径去发现攻打,而攻打的进攻则能够通过 XDP 间接在网络包未到网络栈之前就解决掉,性能十分的优良。
- 流控:能够管制网络传输速率,比方 tc。
- 替换 iptable:在 k8s 中 iptable 的规定往往会相当宏大,而 iptable 规定越多,性能也越差,应用 eBP 就能够解决,对于这方面有很多开源的实际能够参考。
- 服务调优:如下图所示,在 cdn 服务中难免会呈现一些指标突刺的状况,这种突刺拉高整体的指标,对于这种突刺时常会因为找不到切入点而无从下手,eBPF 存在这种后劲能帮忙剖析解决该问题,当 eBPF 发现网络抖动,会被动采集过后利用的运行状态。
eBPF 程序实际
编写 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 初始化。
- text 是要执行的代码,外面是一个函数
- kprobe__schedule 内容是调用 bpf_trace_printk(“hello world\n”);return 0
- kprobe__schedule 的含意是用 kprobe 的 个性在内核调用 schedule 函数的时候调用 bpf_trace_printk,打出 hello world
- bpf_trace_printk 会把这些输入到 /sys/kernel/debug/tracing/trace_pipe 里,前面的 trace_print 就能够把数据打印进去
上面是通过 kprobe 监控机器 tcp(ipv4)的连贯状态变动。首先须要晓得 tcp 状态变动时内核会调用哪些函数。除了 time-wait 状态之外,其余状态基本上是通过 tcp_set_state 设置的。在 time-wait 阶段的时候,内核会创立一个新的构造体去存 time-wait 的 socket,内核思考到内存的开销问题,之前的 socket 会开释掉。先不思考 time-wait。
接下来看看具体的代码,上图中是载入到 eBPF 的 C 代码。
- 最下面的 BPF_HASH 示意创立一个 BPF 提供的 HASH 表;last 是 HASH 表的名称;struct sock* 是指 key 的大小,这里示意指针大小;uint64_t 是 value 的大小,为 64 位;最初的 10240 示意 map 最多可能放多少个元素。
- 往下是一个构造体 bcc_tcp_state,能够看到前面有一个 BPF_PERF_OUTPUT,它是利用到了 perf ring buffer 的一个个性。
- 再上面是函数 get_tcp_state_change,该函数会在内核调用 tcp_set_state 的时候调用。
通过内核的几个参数,内核的构造体 socket,以及这个函数传进来的一些 state,能够获取过后 tcp 连贯的状态转化状况,上图函数的第一个参数 ctx 实际上是寄存器,前面是要染指函数的两个参数。这里会把一些 tcp 的状态存起来,应用 perf_submit 将这些状态更新到 perf ring buffer 中,就能够在用户态把 perf ring buffer 货色给读出来,这就是 tcp 的一些状态变动。
上图是 python 代码。
- 首先把 C 代码读进来,通过调用 bpf 初始化,将代码编译成 eBPF 字节码,载入到 eBPF 虚拟机中运行。
- 上面是 attach_kprobe,就是在内核调用 tcp,event 是指内核在调用 tcp_set_state 的时候,fn_name 是指内核在调用 tcp_set_state 时会执行 get_tcp_state_change 函数,就是后面 C 代码中的函数。
- 关上 perf ring buffer,即前面调用的 bpf[“state_events”].open_perf_buffer,外面的参数是一个 Callback 函数,在 ring buffer 有数据的时候就会调用一次 print_state,也就是说在 C 代码中调用 perf_sumbit 时候就能够调用一次 print_tcpstats 函数,并会输入存入的数据。
- 最上面调用了 perf_buffer_poll 的性能,只会在 ring buffer 有音讯时被唤醒,再调用 Callback 函数,这样就不会无谓地节约 CPU。
利用 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。
eBPF 在又拍云的倒退
- 欠缺 cdn 系统监控体系
- 强化 cdn 业务链路 traceing,进步服务水平,提供更多的性能剖析的路径
- 解决 cdn 服务中遇到的某些难以解决的问题 注:目前通过 systemtap 能够解决
- 将 XDP 引入又拍云边缘机器,给予防备 DDoS 攻打提供帮忙
- 替换 tcpdump 工具,放慢抓包效率,缩小抓包时对系统性能的影响
举荐浏览
聊聊 HTTP 常见的申请形式
有赞对立接入层架构演进