eBPF为什么很重要以及它的发展史

Linux 内核与用户空间

作为应用程序开发人员,我们通常不直接使用系统调用接口,因为编程语言为我们提供了高级抽象和标准库,这些都是更容易编程的接口。因此,很多人幸福地不知道内核在我们的程序运行时做了多少工作。如果想了解内核被调用的频率,可以使用strace实用程序来显示应用程序进行的所有系统调用。

Linux内核是应用程序和它们运行的硬件之间的软件层。应用程序运行在一个称为用户空间的非特权层中,该层不能直接访问硬件。相反,应用程序使用系统调用(syscall)接口发出请求,以请求内核代表其采取行动。这种硬件访问可能包括读取和写入文件、发送或接收网络流量,甚至只是访问内存。内核还负责协调当前进程,使许多应用程序能够同时运行。如下图所示。

![image-20230411082743293](/Users/mac/Library/Application Support/typora-user-images/image-20230411082743293.png)

用户空间中的应用程序使用系统调用接口向内核发出请求

添加新的功能到Linux内核

如果您能够拦截打开文件的系统调用,您就可以确切地看到任何应用程序访问哪些文件。但是你怎么拦截呢?让我们考虑一下,如果希望修改内核,添加新代码以在调用系统调用时创建某种输出,那么将涉及哪些内容。

Linux内核很复杂,目前大约有3000万行代码对任何代码库进行更改都需要熟悉现有代码,所以除非您已经是内核开发人员,否则这很可能是一个挑战。

此外,Linux是一种通用操作系统,可用于各种环境和情况。这意味着如果您希望您的更改成为正式Linux发行版的一部分,那么不仅仅是编写工作代码的问题。这些代码必须被社区(更具体地说,是Linux的创造者和主要开发者Linus Torvalds)接受,作为一个对所有人都有更大好处的改变。这不是给定的——只有三分之一提交的内核补丁

Linux内核发行版每隔两到三个月就会有一个新的,但即使在这些发行版中做出了更改,它距离在大多数人的生产环境中可用仍然需要一段时间。这是因为我们大多数人不只是直接使用Linux内核——我们使用像Debian、Red Hat、Alpine和Ubuntu这样的Linux发行版,它们将Linux内核的一个版本与各种其他组件打包在一起。您可能会发现,您最喜欢的发行版使用的是一个已有几年历史的内核发行版。例如:许多企业用户使用红帽企业Linux (RHEL)。版本是RHEL 8.5,日期为2021年11月,它使用Linux内核的4.18版本。该内核于2018年8月发布。

从概念阶段到Linux内核的生产环境,新功能需要花费数年的时间,可以看到如下图所示:

![image-20230411083145619](/Users/mac/Library/Application Support/typora-user-images/image-20230411083145619.png)

向内核添加特性(by Vadim Shchekoldin, Isovalent)

eBPF程序的动态加载

eBPF程序可以动态地加载到内核中或从内核中删除。一旦它们被附加到一个事件,它们将被该事件触发,而不管事件发生的原因是什么。例如,如果您将一个程序附加到打开文件的系统调用,那么当任何进程试图打开一个文件时,它将被触发。加载程序时该进程是否已经在运行并不重要。与升级内核并重新启动计算机以使用其新功能相比,这是一个巨大的优势。

可以通过eBPF非常快速地创建新的内核功能,而不需要每个Linux用户都接受相同的更改,如下图所示。

![image-20230411083242469](/Users/mac/Library/Application Support/typora-user-images/image-20230411083242469.png)

高性能的eBPF程序

eBPF程序是添加检测的一种非常有效的方法。一旦加载和JIT编译,程序就会作为CPU上的本机指令运行。此外,不需要在内核和用户空间之间进行转换(这是一项昂贵的操作)来处理每个事件。

对于性能跟踪和安全可观察性,eBPF的另一个优点是,在产生发送成本之前,可以在内核中过滤相关事件到用户空间。毕竟,仅过滤某些网络数据包是最初的BPF实现的目的。今天,eBPF程序可以收集关于系统中各种事件的信息,并且它们可以使用复杂的、自定义的编程过滤器,只将相关的信息子集发送到用户空间。

云原生环境中的eBPF

现在很多组织选择不直接在服务器上执行程序来运行应用程序。相反,许多人使用云原生方法:容器、编排器(如Kubernetes或ECS),或无服务器方法(如Lambda)、云函数、Fargate等等。这些方法都使用自动化来选择运行每个工作负载的服务器;在无服务器环境中,我们甚至不知道哪个服务器在运行每个工作负载。

然而,涉及到服务器,每一个服务器(无论是虚拟机还是裸金属机)都运行一个内核。当应用程序在一个容器中运行时,如果它们运行在同一台(虚拟)机器上,它们就共享同一个内核。在Kubernetes环境中,这意味着给定节点上所有pod中的所有容器都使用相同的内核。当我们用eBPF程序表来检测内核时,该节点上所有的容器化工作负载对于那些eBPF程序表都是可见的,如下图所示:

![image-20230411083519515](/Users/mac/Library/Application Support/typora-user-images/image-20230411083519515.png)

内核中的eBPF程序可以看到运行在Kubernetes节点上的所有应用程序

节点上所有进程的可见性,加上动态加载eBPF程序的能力,为我们提供了云原生计算中基于eBPF的工具的真正超级功能:

  • 我们不需要更改应用程序,甚至不需要更改它们的配置方式,就可以使用eBPF工具来检测它们。

  • 一旦它被加载到内核并附加到一个事件,eBPF程序就可以开始观察已经存在的应用程序进程。

与此形成对比的是sidecar模型,后者被用于在Kubernetes应用程序中添加日志记录、跟踪、安全和服务网格等功能。在sidecar方法中,工具作为“注入”到每个应用程序POD的容器运行。这个过程包括修改定义应用程序POD的YAML,添加sidecar容器的定义。这种方法当然比在应用程序的源代码中添加插装更方便(这是我们在sidecar方法之前必须做的事情;例如

  • 当sidecar被添加后,应用程序的POD必须重新启动。
  • 必须修改应用程序YAML。这通常是一个自动化的过程,但如果出现问题,sidecar将不会被添加,这意味着POD不会检测到新的sidecar被添加进来。例如,可能会对部署进行注释,以指示准入控制器应该将sidecar YAML添加到该部署的pod规范中。但是,如果没有正确地标记部署,则不会添加 sidecar,因此对仪器来说是不可见的。
  • 当一个POD中有多个容器时,它们可能在不同的时间达到就绪状态,其顺序可能无法预测。sidecar的注入会大大减慢POD的启动时间,更糟糕的是,它会导致状况或其他不稳定。例如,Open Service Mesh文档描述了在Envoy代理容器准备好之前,应用程序容器必须对所有被丢弃的流量具有弹性。

当网络功能(如服务网格)被实现为sidecar时,这必然意味着所有进出应用程序容器的流量都必须通过内核中的网络堆栈才能到达网络代理容器,这增加了流量的延迟;如下图所示。

![image-20230411083750654](/Users/mac/Library/Application Support/typora-user-images/image-20230411083750654.png)

可以看到eBPF为普通开发人员增加了调用内核的便利性,更为当下流行的云原生计算技术提供了超级功能。接下来就通过它的发展历史来了解下eBPF是如何产生的吧。

eBPF发展历史

eBPF是一种革命性的内核技术,允许开发人员编写可以动态加载到内核中的自定义代码,从而改变内核的行为方式。

这使新一代高性能的网络、可观察性和安全工具成为可能。 如果想使用这些基于eBPF的工具来设置应用程序,则无需以任何方式修改或重新配置应用程序,这要归功于eBPF在内核中的有利位置。

你可以用eBPF做的几件事包括:

  • 系统几乎任何方面的性能跟踪

  • 高性能网络,内置可见性

  • 检测和(可选)防止恶意活动

让我们简单探索一下eBPF的历史,从伯克利数据包过滤器开始。

1993

eBPF的根源:伯克利数据包过滤器

我们今天所说的“eBPF”起源于BSD数据包过滤器,该过滤器于1993年在劳伦斯伯克利国家实验室的Steven McCanne和Van Jacobson撰写的论文中首次描述。本文讨论了一种可以运行过滤器的伪机器,过滤器是为确定是否接受或拒绝网络数据包而编写的程序。这些程序是用BPF指令集编写的,

1997

BPF代表“伯克利数据包过滤器”,它于1997年首次引入Linux,内核版本为2.1.7.5,2,在tcpdump实用程序中使用它,作为捕获要追踪的数据包的有效方法。

2005

eBPF向生产系统的演变

自2005年以来,Linux内核中就存在一个名为kprobes(内核探针)的功能,允许在内核代码中的几乎任何指令上设置陷阱。开发人员可以编写内核模块,将函数附加到kprobes,用于调试或按形式测量。

2012

快进到2012年,当时seccomp-bpf在内核的3.5版本中引入。这使得使用BPF程序可以决定是否允许或拒绝用户空间应用程序进行系统调用。

2014

从BPF到eBPF

从2014年的内核版本3.18开始,BPF演变成我们所谓的“扩展BPF”或“eBPF”。这涉及几个重大变化:

  • BPF指令集进行了全面检修,以提高在64位机器上的效率,并且完全重写了解释器。引入了eBPF maps,这是BPF程序和用户空间应用程序可以访问的数据结构,允许它们之间共享信息。
  • 添加了bpf()系统调用,以便用户空间程序可以与内核中的eBPF程序交互。。
  • 添加了几个BPF助手函数。
  • 添加了eBPF验证器,以确保eBPF程序可以安全运行。

这为eBPF奠定了基础,但发展并没有放缓!从那时起,eBPF发生了重大变化。

2015

2015年增加了将eBPF程序附加到kprobes的能力,这是跨Linux系统进行跟踪方式革命的起点。与此同时,开始在内核的网络堆栈中添加钩子,允许eBPF程序处理网络功能的更多方面。

2016

到2016年,基于eBPF的工具被用于生产系统。Brendan Gregg在Netflix的追踪工作在基础设施和运营方面广为人知,他关于eBPF“为Linux带来超级大家族”的声明也是如此。同年,Cilium项目被宣布,这是第一个使用eBPF替换容器环境中整个数据路径的网络项目。

2017

第二年,Facebook(现在的Meta)将Katran变成了一个开源项目。第4层负载平衡器Katran满足了Facebook对高度可扩展和快速解决方案的需求。自2017年以来,Facebook.com的每个数据包都通过了eBPF/XDP.4。

2018

2018年,eBPF成为Linux内核中一个单独的子系统,来自Isovalent的Daniel Borkmann和来自Meta的Alexei Starovoitov作为其维护者(他们后来也来自Meta的Andrii Nakryiko加入了他们)。同年,BPF类型格式(BTF)推出,这使得eBPF程序更加便携。

2020

2020年推出了LSM BPF,允许将eBPF程序附加到Linux安全模块(LSM)内核接口。这表明已经确定了eBPF的第三个主要用例:很明显,除了网络和可观察性外,eBPF还是安全工具的绝佳平台。

多年来,由于300多名内核开发人员和相关用户空间工具(如bpftool)、编译器和编程语言库的许多贡献者的工作,eBPF的能力大幅增长。程序曾经被限制在4096条指令,但该限制已增长到100万条经过验证的指令,并通过支持尾部调用和函数调用有效地变得非常出色

后续更多eBPF的精彩内容可以通过订阅微信公众号 《中间件源码》 查看。

“eBPF and Kubernetes: Little Helper Minions for Scaling Microservices”,

https://youtu.be/99jUcLt3rSk