eBPF 的发展历程及工作原理

目录

eBPF 是什么

掌握 eBPF 是不是得先成为内核开发者?

eBPF 的发展历程是什么样的?

eBPF 是怎么工作的?

eBPF 是万能的吗?

小结


eBPF 是什么

eBPF 是什么呢? 从它的全称“扩展的伯克利数据包过滤器 (Extended Berkeley Packet Filter)” 来看,它是一种数据包过滤技术,是从 BPF (Berkeley Packet Filter) 技术扩展而来的。

BPF 提供了一种在内核事件和用户程序事件发生时安全注入代码的机制,这就让非内核开发人员也可以对内核进行控制。随着内核的发展,BPF 逐步从最初的数据包过滤扩展到了网络、内核、安全、跟踪等,而且它的功能特性还在快速发展中,这种扩展后的 BPF 被简称为 eBPF(相应的,早期的 BPF 被称为经典 BPF,简称 cBPF)。实际上,现代内核所运行的都是 eBPF,如果没有特殊说明,内核和开源社区中提到的 BPF 等同于 eBPF。

我想你已经知道,在 eBPF 之前,内核模块是注入内核的最主要机制。由于缺乏对内核模块的安全控制,内核的基本功能很容易被一个有缺陷的内核模块破坏。而 eBPF 则借助即时编译(JIT),在内核中运行了一个虚拟机,保证只有被验证安全的 eBPF 指令才会被内核执行。同时,因为 eBPF 指令依然运行在内核中,无需向用户态复制数据,这就大大提高了事件处理的效率。

正是由于这些突出的特性,eBPF 现如今已经在故障诊断、网络优化、安全控制、性能监控等领域获得大量应用。比如,Facebook 开源的高性能网络负载均衡器  Katran、Isovalent 开源的容器网络方案  Cilium ,以及著名的内核跟踪排错工具 BCC 和 bpftrace 等,都是基于 eBPF 技术实现的。

下图(来自 ebpf.io)是对 eBPF 技术及其应用的一个概览:

eBPF 的发展历程及工作原理_第1张图片

可以说,如果你想洞悉内核的运行状态,优化内核网络性能,控制诸如容器等应用程序的安全,那么 eBPF 就是一个你必须要掌握的技能。

掌握 eBPF 是不是得先成为内核开发者?

实际上,前面我提到的 BCC、bpftrace 等一系列的开源项目已经提供了大量工具,可以帮你解决像故障诊断、性能监控、安全控制等绝大部分场景中的问题。而在你需要开发新的eBPF 程序时,内核社区提供的  libbpf 库不仅帮你避免了直接调用内核函数,而且还提供了跨内核版本的兼容性(即一次编译到处执行,简称 CO-RE)。

所以,掌握 eBPF 并不需要掌握内核开发。我认为,学习最快的方法就是理解原理的同时配合大量的实践,eBPF 也不例外。下面这三点是学习 eBPF 的重中之重:

eBPF 的发展历程及工作原理_第2张图片

eBPF 的发展历程是什么样的?

eBPF 是从 BPF (Berkeley Packet Filter) 技术扩展而来的。而说起 BPF,它的历史就更悠长了。

早在 1992 年的 USENIX 会议上,Steven McCanne 和 Van Jacobson 发布的论文“  The BSD Packet Filter: A New Architecture for User-level Packet Capture” 就为 BSD 操作系统带来了革命性的包过滤机制 BSD Packet Filter(简称为 BPF),这比当时最先进的数据包过滤技术还快 20 倍。为什么性能这么好呢?这主要得益于 BPF 的两大设计:

  第一,内核态引入一个新的虚拟机,所有指令都在内核虚拟机中运行。
  第二,用户态使用 BPF 字节码来定义过滤表达式,然后传递给内核,由内核虚拟机解释执行。

这就使得包过滤可以直接在内核中执行,避免了向用户态复制每个数据包,从而极大提升了包过滤的性能,进而被各大操作系统广泛接受。BPF 最初的名字 BSD Packet Filter ,也被作者的工作单位名所替代,变成了 Berkeley Packet Filter(很巧的是,还是简称BPF)。

在 BPF 诞生五年后,Linux 2.1.75 首次引入了 BPF 技术,随后 BPF 开始了不温不火的发展历程。其中,Linux 3.0 中增加的 BPF 即时编译器可以算是一个最重大的更新了。它替换掉了原本性能更差的解释器,进一步优化了 BPF 指令运行的效率。但直到此时,BPF 的应用还是仅限于网络包过滤这个传统的领域中。

时间到了 2014 年。为了研究新的软件定义网络方案,Alexei Starovoitov 为 BPF 带来了第一次革命性的更新,将 BPF 扩展为一个通用的虚拟机,也就是 eBPF。eBPF 不仅扩展了寄存器的数量,引入了全新的 BPF 映射存储,还在 4.x 内核中将原本单一的数据包过滤事件逐步扩展到了内核态函数、用户态函数、跟踪点、性能事件(perf_events)以及安全控制等。

eBPF 的诞生是 BPF 技术的一个转折点,使得 BPF 不再仅限于网络栈,而是成为内核的一个顶级子系统。

在内核发展的同时,eBPF 繁荣的生态也进一步促进了 eBPF 的蓬勃发展。这其中,最典型的就是 iovisor 带来的 BCC、bpftrace 等工具,成为 eBPF 在跟踪和排错领域的最佳实践。由于 eBPF 无需修改内核源码和重新编译内核就可以扩展内核的功能,Cilium、Katran、Falco 等一系列基于 eBPF 优化网络和安全的开源项目也逐步诞生。并且,越来越多的开源和商业解决方案开始借助 eBPF,优化其网络、安全以及观测的性能。比如,最流行的网络解决方案之一 Calico,就在最近的版本中引入了  eBPF 数据面网络,大大提升了网络的性能。

为了帮你更好地理解 eBPF 的发展历程,我把 eBPF 诞生以来的发展过程整理成了一张图片:

eBPF 的发展历程及工作原理_第3张图片

eBPF 的发展历程及工作原理_第4张图片

eBPF 的发展历程及工作原理_第5张图片

eBPF 是怎么工作的?

eBPF 程序并不像常规的线程那样,启动后就一直运行在那里,它需要事件触发后才会执行。这些事件包括系统调用、内核跟踪点、内核函数和用户态函数的调用退出、网络事件,等等。借助于强大的内核态插桩(kprobe)和用户态插桩(uprobe),eBPF 程序几乎可以在内核和应用的任意位置进行插桩。

看到这个令人惊叹的能力,你一定有疑问:这会不会像内核模块一样,一个异常的 eBPF程序就会损坏整个内核的稳定性呢?其实,确保安全和稳定一直都是 eBPF 的首要任务,不安全的 eBPF 程序根本就不会提交到内核虚拟机中执行。

Linux 内核是如何实现 eBPF 程序的安全和稳定的呢?其实很简单,我带你看个 eBPF 程序的执行过程,你就明白了。

如下图(图片来自  brendangregg.com)所示,通常我们借助  LLVM 把编写的 eBPF程序转换为 BPF 字节码,然后再通过 bpf 系统调用提交给内核执行。内核在接受 BPF 字节码之前,会首先通过验证器对字节码进行校验,只有校验通过的 BPF 字节码才会提交到即时编译器执行。

eBPF 的发展历程及工作原理_第6张图片

如果 BPF 字节码中包含了不安全的操作,验证器会直接拒绝 BPF 程序的执行。比如,下面就是一些典型的验证过程:

  1. 只有特权进程才可以执行 bpf 系统调用;
  2. BPF 程序不能包含无限循环;
  3. BPF 程序不能导致内核崩溃;
  4. BPF 程序必须在有限时间内完成。

BPF 程序可以利用 BPF 映射(map)进行存储,而用户程序通常也需要通过 BPF 映射同运行在内核中的 BPF 程序进行交互。如下图(图片来自  ebpf.io)所示,在性能观测中,BPF 程序收集内核运行状态存储在映射中,用户程序再从映射中读出这些状态。

eBPF 的发展历程及工作原理_第7张图片

可以看到,eBPF 程序的运行需要历经编译、加载、验证和内核态执行等过程,而用户态程序则需要借助 BPF 映射来获取内核态 eBPF 程序的运行状态。

eBPF 是万能的吗?

看到这里,你是不是因为 eBPF 在扩展内核功能上的强大能力而兴奋不已?我猜你已经迫不及待想要体验一下了。不过,在你体验之前,我还要提醒你一点:eBPF 并不是万能的,它也有很多的局限性。下面是一些最常见的 eBPF 限制:

  1. eBPF 程序必须被验证器校验通过后才能执行,且不能包含无法到达的指令;
  2. eBPF 程序不能随意调用内核函数,只能调用在 API 中定义的辅助函数;
  3. eBPF 程序栈空间最多只有 512 字节,想要更大的存储,就必须要借助映射存储;
  4. 在内核 5.2 之前,eBPF 字节码最多只支持 4096 条指令,而 5.2 内核把这个限制提高到了 100 万条;

由于内核的快速变化,在不同版本内核中运行时,需要访问内核数据结构的 eBPF 程序很可能需要调整源码,并重新编译。

小结

eBPF 是从 BPF 技术扩展而来的。BPF 出现后,一直都是网络数据包过滤的核心,但直到eBPF 诞生前,BPF 都仅用于包过滤这个场景中。eBPF 的诞生是 BPF 技术的一个转折点,使它的应用范围逐步从包过滤扩展到内核函数、用户函数、跟踪点、性能事件、安全控制等全新的领域中。而这也进一步催生了 Cilium、Katran、Falco 等一大批基于 eBPF 构建的网络和安全解决方案,形成了繁荣的 eBPF 生态。

eBPF 程序以内核事件触发的方式运行,并且其运行过程包括编译、加载、验证和内核态执行等。为了保护内核的安全和稳定,如果编译后 BPF 字节码中包含了不安全的操作,验证阶段会直接拒绝 BPF 程序的执行。

不过,需要提醒你的是:为了确保安全和稳定,eBPF 程序也有很多的限制,这是你在后续的学习过程中需要特别留心的。

你可能感兴趣的:(eBPF技术,eBPF)