eBPF Internal: Instructions and Runtime | 凌云时刻

eBPF Internal: Instructions and Runtime | 凌云时刻


凌云时刻 · 技术

eBPF Internal: Instructions and Runtime | 凌云时刻

eBPF Internal: Instructions and Runtime | 凌云时刻

导读:eBPF 是最近几年异常火爆的一门内核技术,从2011年开发至今,eBPF 社区依然非常活跃。eBPF 可以通过热加载的方式动态的获取、修改内核中的关键数据和执行逻辑,避免内核模块的方式可能会引入宕机风险,并具备堪比原生代码的执行效率。eBPF 是如何具备堪比原生的执行效率和动态扩展当前 Linux 内核的能力,接下来将为大家揭开这一层薄纱。

作者 | 荣旸

来源 | 凌云时刻(微信号:linuxpk)

Intro

eBPF Internal: Instructions and Runtime | 凌云时刻

首先我们介绍一下 eBPF 的前世今生,以便我们更好的了解接下来的内容。如果已有了解和实践,可快速跳到下一章节。

大家或多或少都接触使用过 tcpdump 工具,tcpdump 可以根据用户指定的自定义过滤规则,在报文出入协议栈时获取报文的元信息。tcpdump 之所以可以灵活的过滤用户报文,本质是将过滤规则转化为一种特殊的指令,例如下图:

eBPF Internal: Instructions and Runtime | 凌云时刻

这种特殊的指令被称为 BPF,在 eBPF 诞生后被称为 cBPF。这种特殊指令通过 libpcap 接口传递进入内核,当网卡收到了数据包后会执行注册的 AF_PACK 协议中的 packet_rcv 函数,执行用户态传入的 BPF 指令,如果满足过滤规则就 clone 到用户态。大体的流程如下图:

eBPF Internal: Instructions and Runtime | 凌云时刻

通过这种机制可以极大提高了规则的灵活度,可以根据用户的需求过滤复杂的报文。同时可以不断优化内核中的 BPF 指令执行器提高执行效率,例如 JIT、SIMD 等等。

cBPF (classic Berkeley Packet Filter) 的诞生可以追溯到1992年。tcpdump 作为 cBPF 的典型应用,seccomp 也基于 cBPF 进行安全过滤。cBPF 主要特点如下:

  1. 内核内置 BPF 指令解释器,允许从用户态传入内核中;

  2. 图灵不完备,BPF 指令不具备循环等语义,确保内核执行指令的安全;

  3. 解释运行,支持 JIT。如上面提到的 tcpdump 场景,每一个报文皆需要经过过滤器,指令的执行速度严重影响性能,故引入了常见的 JIT 指令优化方式,可以将指令转换为本地指令,加速指令执行,通常会有数倍的性能提升;

时间逐渐来到了21世纪,eBPF 从2011年开始开发。eBPF 与 cBPF 的主要区别如下:

  1. 定义了新的 ISA,扩展了 cBPF 指令,eBPF 的指令主要受 amd64 和 arm64 的影响,并扩展了 64bit 的寄存器;

  2. 使用 LLVM 作为 BPF 的编译器,由于 eBPF 指令极大的扩展,并支持将 C 编译为 BPF 指令集,再将编译器内置在内核中会引入庞大的代码,同时社区已有 LLVM 和 GCC 等成熟的工具,故首先基于 LLVM 扩展了 BPF 后端,GCC 距离使用还要等等;

  3. 引入了用户可使用的 bpf.h 头文件,便于用户态程序使用内核封装的 eBPF 程序;

  4. 依然是图灵不完备,安全和效率依然是第一位考虑,不过在最近的内核中引入了 bonded loop,可以在安全的情况下执行循环;

  5. 解释运行,支持 JIT。同 cBPF,但是扩展了更多的架构,支持在 amd64 和 aarch64 等更多的架构;

经过了 cBPF 和 eBPF 的不断迭代和发展,基于 BPF 已经诞生了很多生产级别的项目:

  1. Katran,Facebook 开源的4层负载均衡,基于 XDP;

  2. BCC 工具集,bpftrace 和 systemtap-bpf,丰富并增强了内核调试和跟踪的能力;

  3. Cilium,微服务和 k8s 场景下的网络治理工具;

  4. IO Visor Project,提到了 BCC 就不能不提到 iovisor 项目,其开源了 BCC, bpftrace, gobpf, ubpf 等一众工具;

当前的 BPF 常见模型:无循环、无锁的简短的 BPF 程序,将很多内核的 helper 和 hook 点粘合在一起。在下面这几种场景下都有运用:

  1. Tracing

    1. kprobe

    2. tracepoint

  2. Networking

    1. sched

    2. XDP

  3. Security

    1. secomp

最后,大家为什么会去了解并使用 BPF。很重要的原因是为了更多的控制权,包括实现一些在用户态还不能够满足需求,或者内核的某些行为需要修改的场景。BPF 的最佳场景也是在用户态和内核态互相配合,共享数据。当然,BPF 也是 CO-RE,一次编译各处运行,具有比较好的可移植性。



Why BPF is FAST?

eBPF Internal: Instructions and Runtime | 凌云时刻


BPF 在内核中的运行,可以概括为下面的流程:

eBPF Internal: Instructions and Runtime | 凌云时刻

我们假设一种场景,我们将 BPF attach 到了某个热点的 tracepoint 之上,例如收发包,每次收发包时,tracepoint attached 的 BPF 程序都会被执行一遍。在比较繁忙的机器上,收发包可能每秒钟百万次,执行效率至关重要,如果 BPF 程序被 attach 在热点中,性能问题很可能会成千上万倍的放大。在我们探讨 BPF 程序为什么会执行的如此之快之前,我们有必要先了解下 BPF 指令和解释器。

指令

eBPF Internal: Instructions and Runtime | 凌云时刻

BPF 当前拥有102个指令,主要包括三大类:ALU (64bit and 32bit)、内存操作和分支操作。其中指令的格式主要由下面这几部分组成:

  1. 8bit opcode

  2. 4bit destination register (dst)

  3. 4bit source register (src)

  4. 16bit 偏移

  5. 32bit 立即数

eBPF Internal: Instructions and Runtime | 凌云时刻

与我们常见的 x86 或 ARM 的指令非常接近。在定义了指令后,每一条的指令执行,是通过内核中的解释器运行,流程可以抽象为一个 loop 循环,也被称为指令分发,循环内会不断的载入指令、执行指令,直至退出。

eBPF Internal: Instructions and Runtime | 凌云时刻

虚拟机

eBPF Internal: Instructions and Runtime | 凌云时刻

我们可以认为是 BPF 字节码是运行在内核中的 BPF 虚拟机中,BPF 字节码也是我们通常提到的 p-code (portable code),主要目的是为了软件解释器的高效运行。提到了虚拟机,不得不提到我们常见的几种解释运行的语言,例如 Python 和 Lua。根据虚拟机的实现,可以分为两类,基于栈的虚拟机和基于寄存器的虚拟机,其中基于栈的虚拟机的思想,最早是来自于 Pascal,CPython 和 Lua 4 同样是基于栈的虚拟机。Lua 5 和 Dalvik JVM 则是基于寄存器的虚拟机,BPF 同样是基于寄存器的虚拟机,那么栈和寄存器的实现有何不同,性能是否有所差异,接下来我们继续分析。

基于栈的虚拟机,顾名思义指令是以栈的数据结构组织的。下面的图可以比较清晰的展示这一流程:

eBPF Internal: Instructions and Runtime | 凌云时刻

当我们需要获得 20+7 结果时,需要生成4条指令,LIFO 执行。这样会生成更多的指令,同时需要移动多次内存,但是由于没有众多的寄存器,虚拟机的实现会相对简单。

我们再来看下基于寄存器的虚拟机,不同于频繁操作栈,它可以直接操作寄存器,如下图流程演示:

eBPF Internal: Instructions and Runtime | 凌云时刻

同样的需要获得 20+7 的结果,在寄存器足够的情况下,我们只需要生成并执行一条指令即可。指令行数相对于栈的实现有显著减少,效率也会提高。但是基于寄存器的虚拟机实现会更加复杂,同时每次指令需要访问更多的内存,并且指令也会更复杂,因为需要提供 2,3,4 地址指令的支持。

通过 Data from A Performance on Stack-based and Register-based Virtual Machine 论文,我们可以对通用场景下,基于栈和基于寄存器的进行一个简单的对比:

  • 基于寄存器的虚拟机性能在总的时间上比基于栈的虚拟机快 20.39%;

    • 指令分发执行,基于寄存器的虚拟机快 66.42%

    • 数据获取,基于栈的虚拟机快 23.5%

eBPF Internal: Instructions and Runtime | 凌云时刻

eBPF Internal: Instructions and Runtime | 凌云时刻

通过这个对比,我们可以得出一个初步结论,在通用场景下,基于寄存器比基于栈的虚拟机实现,性能更好。当然仅仅这种精心设计的测试可能实际意义不是很大,我们还需要一个实际生产级别的示例和数据。巧合的是,Lua 4 的虚拟机实现是基于栈,而 Lua 5 换成了性能更好的基于寄存器的实现。我们对比了二者的性能:

eBPF Internal: Instructions and Runtime | 凌云时刻

通过这一个官方的数据对比,可以看出来 Lua 5 比 Lua 4 快了 34% 左右。由此可见在实际的应用中,基于寄存器的虚拟机确实可以带来更高的性能,但是从上面的数据看到,仅仅百分之几十的性能提升,相对于原生指令还有更大的提升余地。

JIT

eBPF Internal: Instructions and Runtime | 凌云时刻

在语言层面的性能对比中,有一个代表性的性能测试场景 Techempower。

一门语言,和这门语言下的不同 web 框架,分别测试 HTTP 处理性能。通过下面这种图,我们可以看到,编译为本地代码的语言性能遥遥领先,而 Python 这种解释运行的语言却名落孙山,但是其中有一个例外,Java 的性能可以和 Rust、Go 这些语言互有胜负,我们已经知道 Java 某种意义上也是解释运行,抛开 Java VM 多年持续优化,与 CPython 最大的不同则是 JIT 的支持。

eBPF Internal: Instructions and Runtime | 凌云时刻

何为 JIT?JIT (Just-in-time) 在2011年引入到 cBPF。与 JIT 相对应的为 AOT (ahead-of-time)。JIT 不需要解释器,或者说扩展了解释器,JIT 在运行时会将指令编译为原生指令在本机执行。BPF 虚拟机会将所有的字节码翻译到本地原生代码再执行,具体的是翻译 BPF 字节码到本地原生代码,保存到内存中的特定区域并执行。BPF 程序通常比较简洁和轻量,引入 JIT 不会显著影响冷启动性能。

启用 JIT 究竟会带来多大的性能提升?之前提到的 Lua 在之后的版本提供了 LuaJIT 的实现,最大的变化是使用 JIT 重写。下面是一组 LuaJIT vs Lua 的性能数据,我们可以看到 LuaJIT 比 Lua 快2-10倍。

eBPF Internal: Instructions and Runtime | 凌云时刻

同样的,PyPy 是 CPython 基于 JIT 的实现,我们看到 PyPy 比 CPython 快2-10倍。

eBPF Internal: Instructions and Runtime | 凌云时刻

对于 BPF 而言,JIT 究竟会带来多大的性能?uBPF 是一个很好的测试程序,uBPF 是 BPF 虚拟机在用户态的实现,它提供了可选的 JIT,我们可以使用 clang 将测试程序编译为 elf 文件,分别测试开启和关闭 JIT 情况下,执行同一个 BPF 程序的性能。从下面的测试数据可以看到,开启 JIT 后性能同样也有数倍的提升。

eBPF Internal: Instructions and Runtime | 凌云时刻


How BPF extends Kernel

eBPF Internal: Instructions and Runtime | 凌云时刻

我们在前面的内容中,提到了编译、指令集和虚拟机。那么 BPF 是如何编译成一个可执行文件,在内核中运行的?

eBPF Internal: Instructions and Runtime | 凌云时刻 LLVM 

当前 BPF 的编译离不开 LLVM,LLVM 分为前端和后端,我们可以将任何语言编译为 LLVM IR,这是一种中间文件。LLVM 可以将 LLVM IR 编译为目标文件,也就是我们提到的二进制文件。

eBPF Internal: Instructions and Runtime | 凌云时刻

对于 BPF 而言,我们可以使用 clang 将 BPF 编译为 LLVM IR 文件,LLVM 当前已经支持 BPF 作为目标文件,因此我们可以将任何的 LLVM IR 编译为 BPF 目标文件。大体的流程可以参考下图:

eBPF Internal: Instructions and Runtime | 凌云时刻

我们当前在使用 C 编写,并编译成 BPF 程序。从上面的流程中,我们可以了解到,我们可以将任何语言翻译为 LLVM IR,只需要这门语言提供 LLVM 的前端,我们就可以将这门语言编译为 BPF 目标文件。幸运的是,当前很多主流语言都提供了 LLVM 的前端,例如 C, C++, Go Haskell 等等。

我们将各种语言编译为 BPF 目标文件后,我们不仅可以使用这些语言来开发 BPF 程序,我们还可以将 BPF 作为一种通用的指令集,使用用户态的虚拟机来运行 BPF 执行,作为一种平台无关、CO-RE 的指令架构。

eBPF Internal: Instructions and Runtime | 凌云时刻 WASM

如同现在如日中天的 WASM,作为一种开源的可移植的字节码格式,在边缘计算和浏览器中被广泛使用。其中 WASM 已有具备了在内核中执行的能力,BPF 作为内核的亲儿子,相比于 WASM 更适合在内核中运行,并且可以与内核更紧密的结合。

eBPF Internal: Instructions and Runtime | 凌云时刻


BPF in the future

eBPF Internal: Instructions and Runtime | 凌云时刻

在谈未来之前,我们不能忘记 BPF 的初衷:


eBPF Internal: Instructions and Runtime | 凌云时刻 BPF goal

  • Let non-kernel developers safely and easily modify kernel behavior.


eBPF Internal: Instructions and Runtime | 凌云时刻 BPF non goal

  • Implement dynamic tracing and kernel introspection

  • Implement software defined networking, firewalls, load balancers, service mesh

在秉持着 BPF 的 goals 前提下,我们在未来做的更多,场景也更大:

eBPF Internal: Instructions and Runtime | 凌云时刻 BPF in kernel


  • 安全的锁和内存操作

  • 允许用户在内核中执行更多的指令

  • 更快的速度

eBPF Internal: Instructions and Runtime | 凌云时刻 BPF in user-space

  • 作为一种通用的字节码

  • CO-RE

  • 原生支持 Rust、Go 和其他语言


尾声

eBPF Internal: Instructions and Runtime | 凌云时刻

我们团队在使用 eBPF 做一些很 cool 的事情,包括将社区的 bcc 工具包引入集团和 Aliyun Linux 2 中,基于 eBPF + tracepoint 自研了网络时延跟踪工具 NX tracepoint 等等。如果有对 BPF 技术生态感兴趣的小伙伴可以随时联系我们(钉钉群号:23149462 )。

END

eBPF Internal: Instructions and Runtime | 凌云时刻

往期精彩文章回顾

eBPF Internal: Instructions and Runtime | 凌云时刻

啥是数据湖?老子(zǐ)告诉你

开源流媒体服务器:为何一定得再撸个新的

智能制造的灾备问题如何解决?

全球CT影像20秒诊断,阿里云为新冠AI辅助诊断系统加速

Code Review 是一场苦涩但有意思的修行

开源界也要封闭,OpenSource能否继续无国界

FPGA设计之“甩锅大法”

Kafka从上手到实践 - 初步认知:MQ系统

进阶之路:深入解读 Java 堆外内存

干货:一文看懂Apache Ranger

eBPF Internal: Instructions and Runtime | 凌云时刻

长按扫描二维码关注凌云时刻

每日收获前沿技术与科技洞见