eBPF原理及实践:什么是eBPF

分享:  

eBPF是一项革命性的内核技术,它允许开发人员编写自定义的代码,然后被内核动态加载后执行,以此来改变内核的执行行为。它的这个特点能够帮助实现高性能网络、更好的可观测性、更细致的安全分析工具。

eBPF的前身:bpf

1、eBPF的前身是bpf(BSD Packet Filter),最早它在1993年论文中有Lawrence Berkeley National Laboratory的Steven McCanne和Van Jacobson提出,它是一种类似字节码虚拟机的东西,有自己的指令集,你可以通过它来编写程序然后交给这个小的虚拟机去执行,这个指令集非常像汇编。比如你可以用它来写包过滤的逻辑(接受或者拒绝一个网络分组)。在这篇论文中可以找到其他一些更加复杂的示例程序,论文名:The BSD Packet Filter: A New Architecture for User-level Packet Capture。

2、1997年,Linux内核版本2.1.75首次引入了BPF,BPF也就开始成为了Berkeley Packet Filter的简称,主要用在tcpdump这些工具中来实现高效的网络包的跟踪。

3、时间快进到2012年,Linux内核版本3.5中引入了seccomp-bpf,它能够控制是否允许用户态应用程序执行系统调用,举个例子,我们启动一个docker容器,如果不添加特殊的选项控制,在docker容器内部去调试程序的时候是执行不了的,因为Linux系统中程序调试需要利用系统调用ptrace,但是ptrace往往都是被默认不允许的,发挥作用的就是seccomp-bpf,这里有一篇文章介绍了seccomp+ptrace调试原理的文章:https://zhuanlan.zhihu.com/p/606317619。seccomp-bpf是首次开始将bpf从包过滤这个范畴开始向其他范畴扩展。到今天发展到eBPF这个阶段,其实与最早的“包过滤”已经没有多大关系了。

从BPF到eBPF

随着BPF在Linux内核中的演进,到了2014年,从版本3.18开始可以使用eBPF将来称呼这项技术,全程就是extended BPF,这包含了几个比较明显的改变:

  • BPF指令级对64位机器做了高度的优化,解释器也基本上重写了;
  • eBPF中增加了maps,BPF程序执行时可以访问它记录一些数据,这些数据可以在BPF程序间共享,也可以允许用户态程序访问它获取结果;
  • 增加了bpf()系统调用,用户态程序通过它可以和eBPF程序进行交互,比如加载到内核、从内核卸载、访问maps数据等;
  • 增加了bpf_这样的一些helper函数;
  • 增加了eBPF程序验证器,验证安全的程序才可以被执行;

这是eBPF首次正式放出,但是不是结束,此后就开始了它的快速发展之路。

eBPF到生产系统

这里介绍下eBPF技术演进过程中的一些关键事件:

  • 2005年Linux中就引入了特性kprobe,它允许在任意指令地址处设置trap,当执行到此处时允许回调用户自定义的函数。开发人员可以编写内核模块,将其中的函数设置为kprobe的回调以执行调试。 ps: 调试器一般也是使用这种指令patch的方式,区别在于kprobe回调函数是内核处理的,而调试器tracee执行时触发断点是内核通过信号通知tracer由tracer来执行的。
  • 2015年的时候允许将eBPF程序连接到kprobe,kprobe可以回调eBPF程序了,这使得在Linux中tracing变得简单,为了更好的追踪Linux内核网络栈的各类事件,Linux中开始增加各种hooks允许eBPF程序进行更细致的观测。
  • 2016年,Netflix的工程师Gregg大佬公开了他和团队在eBPF基础上的大量性能观测工具及实践,让基础设施、运维领域认识到了eBPF在这方面的巨大潜力。
  • 2017年,Facebook开源了Katran这个基于eBPF的高性能L4负载均衡器,也是这一年,Liz Rice这位女强人对此也产生了浓厚的兴趣,并开始研究。 ps: Liz Rice 经常做些技术方面的分享,目前是 the chief open source officer with eBPF specialists at Isovalent, 也是 the Cilium cloud native networking, security and observability project 的创建者.
  • 2018年,Netflix、Meta的几个工程师为Linux eBPF做了大量贡献,使得eBPF成为了Linux内核的一个独立子系统,同年BTF(bpf type format)成为了ebpf的格式类型,使得ebpf程序更加兼容。
  • 2020年,Linux内核支持了LSM BPF允许将eBPF程序和Linux安全模型LSM(Linux Security Model)连接起来,这意味着eBPF的用途又进一步清晰了、扩大了,就是安全工具、网络、可观测性。
  • 近些年,更是有越来越多的项目诞生,cilium、aya等等,很多开发者都对此做出了贡献,业界的实践也越来越多、越来越成熟。

起名有点难

到现在的话,ebpf中的字母e已经没有太大意义了,它已经不仅仅是对bpf的扩展了,它成为了一个独立的子系统。现在提起ebpf的时候,有些人也会用bpf来称呼。但是在Linux内核中,包括操作ebpf程序的系统调用bpf()以及相关的helper函数bpf_xxx,都是直接以bpf来称呼的,这说明Linux内核开发人员已经认可了bpf来代指ebpf,它的含义已经变了,直接代指这个子系统了。但是在Linux内核社区外,还有些人会使用ebpf来称呼,比如ebpf.io这类站点。

eBPF诞生崛起的原因

前面的介绍,让大家知道了ebpf演进过程中的一些关键事件,不禁要问为什么它会诞生?或者说它有哪些优点?

关心点:内核系统调用

大家对Linux内核可能比较陌生,但是对操作系统应该不陌生,毕竟大学都学过。用户程序在执行某些操作时,离不开操作系统的支持,操作系统充当的就是用户程序、硬件之间的一个服务人员,用户程序和服务人员之间传话的窗口就是syscall(系统调用)。

通常用户程序,并不会直接使用系统调用,或者说直接调用的场景比较少,大家一般是通过标准库或者其他库函数的方式来间接使用系统调用。以golang为例,所有网络层面的系统调用都被封装到了标准库net中。

系统调用,比大家的认识可能要复杂些,它包括阻塞性系统调用、非阻塞性系统调用,不同系统调用对程序执行的影响是不一样的,所以go为什么是一门工程化很好的语言,就是它在运行时层面屏蔽了这些,即使某个线程因为系统调用阻塞了,程序还可以继续跑。

因为系统调用如此重要,开发人员会想知道程序中到底在执行哪些系统调用,此时就会借助strace之类的一些工具来跟踪、统计系统调用的执行情况,这样我们能更好了解程序的执行情况。

困难点:内核增加功能

Linux内核的代码规模已经达到了3kw+了,相当大的规模了,如果自己不是Linux内核开发人员或者说对感兴趣的模块不是不熟悉,你很难去修改它的。即使你修改了还要考虑另一个问题,你可能只解决了在你这个情景下、平台下的问题,但是Linux内核是一个通用操作系统,意味着我们的修改可能不一定能解决其他情景、平台下的问题。往往你修改个东西,要经过社区、Torvalds的同意才行,这个周期会非常常。根据统计,Linux内核社区贡献的所有patches也就只有1/3能够进入主线。

即使进入了这个主线,可能已经过去一段时间了,你还要考虑发行版的问题,因为我们大部分开发人员、企业使用的都是某个发行版,发行版使用的内核版本又不一样了,什么时候主线代码被发行版使用了发布了,你才能考虑升级机器上的操作系统。这里就又过去一段时间了。

也就是说,即使你发现内核代码有缺陷,或者想做功能扩展,即使你很有能力开发内核代码(大部分开发估计并不擅长还是需要多年沉淀才行),即使被合入主线、被发行版使用、机器也顺利升级了,但是时间不等人,这种方式满足不了需求方快速变化的需要。

困难点:内核模块扩展

内核开发人员可以考虑通过内核模块的方式(但是开发内核模块也比较困难),来代替直接修改内核代码贡献到上游这个方式,这个路子更敏捷更快,时间成本大大缩短。

内核模块也可以动态加载、卸载,不需要升级系统时停止机器。

但是内核模块的安全性一直是大家比较担心的:

  • 它运行在特权用户级别,
  • 这个模块经过大家充分CR吗,有漏洞吗,会给攻击吗
  • 这个作者值得信任吗
  • 这个模块万一有bug会影响到整机稳定性吗

大家对于内核模块的使用慎之又慎,eBPF通过验证器来尽可能保证字节码程序的安全性,至少不会影响到内核本身的健壮性。又可以独立开发的方式来增强内核功能、快速响应需求变化,相比之下就有很大的吸引力。

优势:eBPF程序动态加载

ebpf程序支持动态加载、移除,不需要升级内核来获得要扩展的特性,也不需要重启机器来应用这些特性,这对于进行性能方面的观测、实现并应用安全工具就非常好。

优势:eBPF程序的高性能

ebpf程序(可能用c写、用rust写),写完的ebpf程序会被编译器编译为target为ebpf的字节码程序,被ebpf子系统加载后会被JIT(即时编译器)编译为机器指令,执行的是机器指令。

ebpf程序最终执行时,可以最小化用户态、内核态的频繁切换、减少上下文切换的开销,数据记录在ebpf maps,用户程序要获取数据就从ebpf maps中取。

所以ebpf程序的性能是比较高的。

优势:云原生领域

在云原生领域,ebpf这种对业务代码无侵入、无需编排配置的方式,使得它在可观测性等方面具有很大的优势。

之前大伙也是一般通过sidecar(边车)模式来增强pod的功能,比如logging、trcacing等,servicemesh也会通过sidecar实现network的能力,sidecar有它的灵活性和优势,但是也有它的局限性:

  • 添加sidecar时,要使其生效(如果一开始忘了加),pod必须整个重启;
  • 需要修改k8s的编排配置的yaml来增加这个sidecar,尽管这个过程功过鼠标勾勾点点就可以、配置是自动化的,但是如果不小心勾选错误还是不会被织入这个sidecar;
  • pod内如果有多个容器,有可能是需要指定启动顺序的,否则可能会发生竞态条件或者故障发生,这样的话也意味着pod启动更慢;
  • servicemesh中通过sidecar来实现network的功能,所有的网络流量都需要经过一个pod中网络代理容器的中转,这增加了传输延迟,影响网络性能;

这些问题也确实是sidecar模式的一些问题,幸运的是ebpf作为一种平台能力,就可以比较好的解决这些问题。

本文小结

这里介绍了什么是ebpf,包括它的前身、演进的一些关键过程,以及相对于传统的方式,当我们希望做些可观测性、内核功能增强、缺陷修复时相比修改主线内核、写内核模块所具有的一些优势。然后,如果能将ebpf作为一种平台能力进行建设,这将使得在可观测性、安全工具、网络性能优化方面做出一些比较大的效果,不管你的机器是裸金属机器、虚拟机,还是容器化应用,它都能统统搞定,而且不需要你侵入业务代码、部署配置也不需要重启机器、pod。