本文主要目的是让你了解eBPF的来龙去脉,以及为什么它在观察容器和Kubernetes集群时特别有用。
eBPF有点像牛油果吐司:它的原料已经存在很长一段时间了。然而,仅仅在过去的几年里,eBPF突然成为IT界最新、最伟大的流行语之一。
我们无法解释牛油果吐司是如何在全球时尚圈风靡一时的。但我们可以告诉你,为什么eBPF在Kubernetes可观测性的革命中变得如此重要。让我们来看看eBPF的历史,它是如何工作的,它解决了哪些问题,以及为什么你应该开始使用它。
什么是eBPF?eBPF简史...
eBPF是“extended Berkeley Packet Filter”的缩写,你不可能了解eBPF的历史,除非你了解老式伯克利包过滤器BPF。
BPF于1993年引入,作为一种为Linux内核配备可编程的、高效的虚拟机的方式,可以控制和过滤流量。这在当时是很有意义的,因为Linux那个时候刚能够支持软件定义网络,而BPF提供了一种强大的操作方法。
也就是说,尽管在20世纪90年代,就有关于BPF的使用,但不久之后它或多或少就销声匿迹了。原因在于,对于大多数人来说,获得对网络流量的内核级可编程控制实际上并不是那么重要,因为工作负载都运行在裸机或vm上,并且可以通过防火墙和管理程序很好地管理流量。
这一切都在2013年开始改变,当Docker出现时,突然间容器就大放异彩了。(顺便说一句,Docker的发展类似于eBPF,容器实际上已经存在了几十年,所以Docker所做的并不是真正的新事物;相反,Docker真正的成就是它第一次成功地让容器流行起来,这主要归功于引入了更好的工具)。与一年后出现的Kubernetes相结合,Docker引领了容器世界,在这个世界里,以非常细粒度的、逐个容器的方式过滤和控制流量的能力变得非常有价值。
走进eBPF
因此在2014年引入了eBPF,它通过提供允许程序直接在Linux内核空间中运行的工具,扩展了BPF的原始架构。我们将在稍后讨论为什么这在容器和Kubernetes的环境中很关键。但首先,让我们退一步,先解释在内核空间中运行程序意味着什么。
基本上,eBPF代码是由内核执行的,而不像标准应用程序那样运行在“用户空间”。这很重要,主要有三个原因:
1、它允许代码高效地运行。
2、它允许代码访问底层内核资源,否则从用户空间访问这些资源很复杂和昂贵的(就资源开销而言)。
3、它允许你观察用户空间中运行的任何程序——这对于在用户空间中运行的可观察性工具是很难做到的。
这就是为什么Brendan Gregg等人称eBPF为“无价的技术”,并将其与JavaScript进行比较:
JavaScript不是静态的HTML网站,而是允许你定义在鼠标点击等事件上运行的小程序,这些事件在浏览器中的安全虚拟机中运行。使用eBPF,而不是固定的内核,你现在可以编写运行在磁盘I/O等事件上的小程序,这些事件在内核的安全虚拟机中运行。
如果你是一个铁杆的Linux极客,你可能会想:“在使用内核模块之前,eBPF做了哪些我不能做的事情?”
这是一个合理的问题。的确,在内核空间中使用内核模块执行代码早就可以做到了。但问题是这些模块必须被插入到内核中,这使得它们的部署更加复杂。它们还往往具有复杂的依赖关系,这增加了部署方面的麻烦。而且它们不是特别安全,这要求你信任用户只插入既稳定又安全的模块。
当然,从理论上讲,你还可以修改Linux内核本身,以便在内核空间中运行你想要的任何代码。但是一个普通的开发人员不能简单地修改Linux内核——除非他是一个内核程序员(相对而言,这样的程序员并不多),或者他想要维护某种自定义的内核分支,这将是一个管理的噩梦。
从你提出需求到Linux社区接受你的需求大概需要一年时间,再从Linux内核到你使用的发型版本需要5年,这就是一个噩梦。
eBPF允许自定义程序在独立的内核级虚拟机中运行,从而解决了所有这些问题。我们将在下面看到,你可以在各种工具的帮助下,轻松地部署你所选择的代码,而不必处理内核模块依赖关系,接触内核源代码,甚至不必拥有root权限。再见,modprobe;你好,eBPF字节码!
eBPF架构
现在你已经知道了eBPF是怎么发展起来的,以及它为什么如此强大,让我们来谈谈它的实际工作原理。
一般来说,要部署eBPF程序,你需要做这些事情:
编写和编译代码
eBPF代码通常是用“限制性C”编写的,然后编译成eBPF字节码。Clang是事实上的编译标准。
在编写代码时,可以引用bpf_helper函数来执行各种常见操作,如内存复制、检索PID和时间戳属性以及与其他应用程序通信(需定义eBPF数据结构,eBPF maps)。因此,你通常不必从头开始编写大量自定义代码。定制代码仅限于你想要实现的特定功能。
检验与加载
要部署已编译的eBPF程序,首先调用bpf()系统调用,它将字节码传递给内核检验器。内核检验器的工作是确保程序不会对内核造成问题。如果验证成功,内核JIT编译器将把它转换为可执行的机器代码。
运行时(runtime)
加载并验证后,程序就可以执行了。它将监视你附加到的任何代码流—无论是在内核空间、用户空间还是两者都有。一旦它运行,你就可以使用eBPF映射或预定义的文件描述符访问程序输入或输出。
下面摘自Cilium的Golang eBPF框架的部分代码片段应该有助于说明这个过程(上面是eBPF代码,下面是加载eBPF程序并与之通信的用户空间应用程序)。在编译和运行应用程序时,它将计算系统上正在执行的新程序的数量。很整洁!
SEC("kprobe/sys_execve")
int kprobe_execve() {
u32 key = 0;
u64 initval = 1, *valp;
valp = bpf_map_lookup_elem(&kprobe_map, &key);
if (!valp) {
bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY);
return 0;
}
__sync_fetch_and_add(valp, 1);
return 0;
}
// Open a Kprobe at the entry point of the kernel function and attach the
// pre-compiled program. Each time the kernel function enters, the program
// will increment the execution counter by 1. The read loop below polls this
// map value once per second.
kp, err := link.Kprobe(fn, objs.KprobeExecve, nil)
if err != nil {
log.Fatalf("opening kprobe: %s", err)
}
defer kp.Close()
// Read loop reporting the total amount of times the kernel
// function was entered, once per second.
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
log.Println("Waiting for events..")
for range ticker.C {
var value uint64
if err := objs.KprobeMap.Lookup(mapKey, &value); err != nil {
log.Fatalf("reading map: %v", err)
}
log.Printf("%s called %d times\n", fn, value)
}
k8s可观测和eBPF使用场景
有各种各样的eBPF用例。容器和Kubernetes可观测性只是其中之一。
但当涉及到观察容器和Kubernetes集群时,eBPF尤其令人兴奋。为什么?因为eBPF允许你获得“干净的”数据来跟踪任何类型的事件——例如网络操作或用户空间中的事件。数据直接来自Linux内核,具有最小的性能开销。这意味着你可以部署一个eBPF程序,在每个数据包进入或发送出主机时监视它,然后将其映射到该主机上运行的进程或容器。其结果是对网络流量发生的情况的细粒度可见性。
再加上eBPF程序非常高效和安全,很容易理解为什么人们现在对eBPF如此兴奋,以及它在云原生世界中为可观测性解锁的可能性。
eBPF SDKs的使用
曾经有一段时间——也就是说,在2010年代中期——编写和加载eBPF程序是一项大量的工作,因为围绕eBPF的工具还不成熟。
在过去几年中,由于引入了更多简化eBPF使用的工具,以及bpf_helper函数和eBPF映射的不断改进,这种情况已经发生了变化。
同时,外部工具链有助于简化eBPF的引导和开发。关键的例子包括BCC和libpf(它现在作为Linux内核的一部分进行维护,因此开始成为事实上的选项)。如上所述,还有一些eBPF友好的编译器,如Clang。
而且,对于那些希望在现代语言开发方面提高eBPF使用水平的人来说,有一些解决方案要感谢那些能够编写用户空间代码来与eBPF程序交互的项目,比如Python,Golang和Rust。
总的来说,eBPF工具链生态系统正在快速发展,现在确切地说哪些工具最终会得到广泛采用还为时过早。但是我们可以肯定地说,围绕eBPF的工具越来越成熟,它给了开发人员越来越少的理由回避使用eBPF。多亏了这些伟大的工具,即使是我们中间最缺乏动力的开发者也可以编写和加载eBPF程序!
eBPF的缺点
总的来说,值得注意的是,eBPF并不能解决实现中的所有问题,而且它受到某些可能永远不会消失的限制。
一是编写符合内核检验器的eBPF程序可能很棘手,尤其是对新手来说。如果你的程序被拒绝,检验器并不会详细地解释原因。独立于检验器的工具有助于解决这一挑战,但它们并不能消除程序被拒绝的风险,并且在试图找出原因时,你将变得非常沮丧。而且,由于验证只在运行时才发生,你面临的风险是,一个内核接受了你的程序,而另一个内核版本有可能拒绝。
eBPF的另一个限制是eBPF程序在堆栈空间上受到限制,这使得开发更加困难和不太直观。(它们以前在指令大小上也有限制,但在内核5.3中有效地消除了这一限制)。你需要学习如何高效地编写eBPF代码,以使其能够大规模地工作。
第三个问题是,尽管eBPF是在Linux内核中实现的,但不同Linux发行版之间的内核版本和自定义之间的差异意味着eBPF程序并不总是像你期望的那样可移植。如果你有一些节点使用Alpine Linux,其他节点运行Ubuntu,那么你可能会发现你的eBPF程序不能跨所有节点工作。改进eBPF可移植性的工作正在进行中,但它仍然不像你想的那样天衣无缝。
eBPF的美好未来
我们不知道十年或二十年后人们是否还会吃牛油果吐司。但是我们非常有信心,Kubernetes的开发人员和管理员将利用eBPF来帮助理解节点和pod内部发生的事情。
考虑到eBPF生态系统正变得越来越有组织,这一点尤其如此,这要归功于eBPF基金会等组织。正如Thomas Graf提到的eBPF发展,我们开始看到“像谷歌和Facebook这样的大公司在维护和推动eBPF的发展。”
所以,如果你一直在抵抗eBPF革命,现在是投降的时候了。Kubernetes可观测性的未来——除其他外——取决于eBPF,你也可以开始学习使用它。
网友评论