本文分享了学习 eBPF 的经验,eBPF 是一种新的云原生技术,其目标是改善可观测性和安全性工作流。我们可能感觉它的入门门槛很高,通过 eBPF 工具来辅助生产环境调试的步骤会很多。本文将会介绍如何使用相关的工具并将其应用到自己的开发中,请逐步迭代自己的知识,并将其用到更高级的使用场景中。最后,我们会讨论如何在 CI/CD 中实现自动化开发及其面临的挑战。
如何开始入门 eBPF?
我第一次听说 eBPF 是在 2021 年,当时它是与可观测性相关的主题一起出现的,起初我并不能真正理解它的含义。描述中声称这是一种收集事件数据的新方法,有助于提升可观测性,也有助于实现安全的可观测性和实际执行。
实际上,我后来才知道,Falco 使用 eBPF 来探查 Kubernetes 中容器的活动。我的学习历程是将 Falco 视为云原生的安全工具,而没有去质疑其底层的技术。“Hacking Kubernetes”一书帮助我完善了对容器运行时、eBPF 和安全执行的学习。
KubeCon EU 2022上的eBPF日,以及后续的eBPF峰会活动,都有助于说明这一点。eBPF 的学习策略与技术领域的其他知识类似,也就是倾听、做笔记,但你依然无法理解它的所有内容。
参加讲座和阅读文章时,我们经常会遇到一些需要认识的术语模式,比如,我立即记住的术语包括 eBPF、BPF、bcc、bpftrace 和 iovisor。Brendan Gregg的博客也经常被提及。
在一个社区聚会上,通过自由讨论营(barcamp)式的演讲,我问到,“如何开始使用 eBPF?”。随后,我们使用三张幻灯片拉开了关于它如何运行的讨论,一起验证了相关的知识,并思考了其使用场景。在 eBPF 峰会上,有一个名为 Capture-the-Flag的环境可以进行学习,这吸引我停下脚步并亲自进行探索挑战。随后,我决定在自己的公共学习平台 o11y.love 上收集所有的eBPF资源,并决定以公开的方式进行学习,记录在这个过程中遇到的所有错误、误解和问题。
内核开发听起来很难,而且理解和入门 eBPF 可能存在一定的障碍。对于利用 eBPF 的工具和库,改变使用它们的方法,并配合生产环境的用例(例如在生产中进行调试),这极大地帮助了我的学习和迭代。对 Linux 操作系统、资源处理和故障排除的一般理解也很有助益。
更高层次的阐述和 ebpf.io 上的描述图片有助于对 eBPF 架构的一般理解。我非常喜欢来自 Brendan Gregg 的解释:
“eBPF 对 Linux 的作用就像 JavaScript 对 HTML 的作用。(某种程度上,可以这么说。)因此,JavaScript 可以让我们定义在点击鼠标等事件中运行的小型程序,而不再是静态的 HTML 站点,这些程序会在浏览器的安全虚拟机中运行。有了 eBPF 之后,我们不再是一个固定的内核,而是可以编写在磁盘 I/O 等事件上运行的小型程序,这些程序会在内核的安全虚拟机中运行。实际上,eBPF 更像是运行 JavaScript 的 v8 虚拟机,而不是 JavaScript 本身。eBPF 是 Linux 内核的一部分。”
eBPF 被添加到 Linux 内核中,以实现小型的沙箱程序。这兼顾了稳定的内核需求和少量的创新可能性,而 eBPF 程序能够有助于扩展和驱动创新,而不会阻碍内核的发展。
eBPF 的用例包括高性能网络和负载均衡、应用程序的追踪和性能问题的排查。此外,细粒度的安全可观测性和应用/容器的运行时安全执行也是我能想到的场景。
编写 eBPF 程序是很难的,内核期望的是字节码,但是它手动编写的效率并不高。因此,需要有一个抽象层,包括从更高级的编程语言生成字节码的编译器。在这种情况下,经常涉及到的工具是 Cilium、bcc 和 bpftrace。eBPF 程序的校验发生在从字节码向机器特定指令集的即时编译过程中。这使得在 CI/CD 工作流中进行静态校验更加困难。稍后,我们会看到更多这方面的内容。
在了解了需求之后,真正的问题在于,我们有什么实际的例子可以尝试和学习,然后深入研究实际的源码?
正式开始:游乐场
Brendan Gregg 的学习eBPF跟踪:教程和样例博文是一个很好的起点。不同的尝试和路线最终都会回到这里进行自学。在深入研究库和 eBPF 程序如何构建之前,在命令行上尝试不同的工具并测试它们的效果,这是一个很好的策略。
注意:Liz Rice 的“Learning eBPF”一书能够有助于进一步降低入门门槛,该书于 2023 年 3 月出版。
推荐的入门方式是选择具有最新内核(大约 4.17 版本)的 Linux 发行版,如 Ubuntu 22.04 LTS。请使用本地虚拟化方法,或在你喜欢的云厂商上生成一个虚拟机。下面的样例使用 Hetzner Cloud CLI 来生成一个新的 Ubuntu 虚拟机:
请根据你的需要重新创建设置过程,可以考虑编写 Ansible playbooks 或脚本来重复安装步骤。这对跟团队成员分享具体学习环境中使用的工具和库会很有帮助。本文讨论的工具和想法在GitLab上有基于Ansible的样例。有些默认的工具需要安装(git、wget、curl、htop 和 docker),还有 eBPF、混沌实验和可观测性等更具体的用例。
接下来的章节将讨论 eBPF 工具的样例。要构建和安装它们,需要 Linux 内核头文件和额外的依赖。在 Ubuntu 22 LTS 上还有一个额外的步骤就是启用DDebs仓库,以访问调试符号(debug symbol),接下来是一个完整的编译器工具链。该针对eBPF的Ansible配置详细描述了安装步骤。你可以查看 Git 的历史记录,了解学习的步骤以及这个过程中的错误。下面的几节主要是运行这些工具,并阐述它们的使用场景。
跟踪系统调用
你可能已经使用过strace
命令来跟踪运行中的二进制文件的系统调用,查看是否有文件被打开和权限错误等。Brendan Gregg 的教程博客建议从提供execsnoop
命令的bcc toolchain开始。它可以跟踪exec()
系统调用。一个很容易的测试方法是打开 SSH 连接,或者在另外一个终端上执行curl opsindev.news
命令。
我们已经学习了一种跟踪系统调用的新方法。bcc 工具链提供了更多实用的工具和用例。从学习的角度来讲,还有哪些工具可以用来深入研究 eBPF 呢?
bpftrace:高级的跟踪语言
Bpftrace提供了自己的高层级跟踪语言,类似于 DTrace 这样的调试框架。乍看上去,在线样例可能会让人无所适从,但由于我们使用的是测试虚拟机,所以可以运行这些样例,以后再分析语言。Bpftrace 允许我们跟踪更多的系统调用,例如open()
。这个方法可以用来打开文件、套接字等,更通用地来讲,是进程可以打开的所有内容,不管是善意还是恶意的。它可以视为strace
命令的一种更为现代的方式。
为了使用可预测的样例来测试 bpftrace,我们可以使用这个最小化的 C 程序,它打开一个文件句柄来创建新文件(源码):
使用 gcc 编译器编译 C 程序,并在启动 bpftrace 命令后运行它。如果opensnoop.bt
命令在 Ubuntu 22 LTS 上运行失败的话,请从 DDeb 仓库安装调试符号。
跟踪语言允许挂钩进入特定的系统调用。要找到正确的系统调用名称,需要慢慢试验,可能还会遇到错误。我不得不将sys_enter_open
改为sys_enter_openat
来触发 C 程序中的打开文件的调用。bpftrace -l
可以列出所有可跟踪的系统调用。
上述代码会将命令和文件名的路径打印到终端上。访问要打印的文件名需要阅读 C 结构的代码,以了解在这种情况下,哪些属性是可用的。
学习曲线的“顿悟时刻(aha moment)”不仅仅会看到文件打开和写入调用,而且还会加载库的依赖关系(stdlib
需要libc
)。bfptrace 工具对于验证二进制文件是否真的加载了某些库是非常有用的,其次是使用ldd
和nm
来窥探依赖关系和调试符号。
深入研究源码和 eBPF 程序
BPF编译器集合(BPF Compiler Collection,BCC)提供了一些样例来学习内核和用户空间之间的数据传输和交互。以前的样例只是挂钩系统调用并立即返回。BCC 在 C 代码中提供了内核插装,并允许使用 Python 或 Lua 编写前端用户空间的应用。按照描述,使用场景包括性能分析和网络流量控制,这都是很好的洞察点,并为以后的知识验证增加了学习难度。Python 和 C 语言知识有助于更容易地深入研究这些样例。
另外,基于我的研究过程,推荐libbpf库,因为它的bootstrap项目提供了更多的演示应用。它们提供了真实的程序,可以用来实现自己的第一个 eBPF 程序。其中有一个样例是使用 Rust 编写的,允许我们按照XDP规范检查网络流量以及数据包的大小。eXpress Data Path(XDP)允许在大规模网络调用时挂钩发送/接收的网络数据包,这会发生在中断之后和内存分配之前。例如,这可以用来悄悄地丢弃数据包(请注意后面高级的 eBPF 程序开发用例)。
用户需要指定端口号,这会导致再一轮的试验和错误排查。使用eth0
作为接口名称无法成功运行。这个样例的输出源自同一台主机上运行的 Prometheus 服务器实例,产生的网络流量来自以 HTTP 端点探查监控目标的输出。
在构建和运行更多的样例后,我们并不完全清楚复制或修改源码是否为一个好的策略。如何将 XDP 样例缩减至最小的尺寸?也许有更好的方法来逐步入手编写 eBPF 程序代码,并增加学习过程中获得的经验。
eBPF 程序开发,学习更多用例
在深入研究如何开发自己的程序之前,了解 BPF 和 eBPF 的基础知识是很重要的。eBPF 是 Berkley Packet Filter(BPF)的一个扩展版本,它提供了一个运行在 Linux 内核中的抽象虚拟机,在受控的环境中运行 eBPF 程序。从根本上说,Linux 内核中的“老”BPF 标准可以被称为“经典BPF”,以便于和eBPF进行区分。
我们可以从尝试 bcc 工具开始,运行 bpftrace 并识别在日常业务和事件中有助于 SRE 和 DevOps 工程师的用例。这可能包括跟踪程序的启动/退出、查看控制组(cgroups)、观察 TCP 连接、检查网络接口等等。建议尽可能保持用例的简单性,以确保稳定的学习曲线。
在验证了关于 eBPF 的基础知识并定义了用例之后,请以库和工具链的形式探寻抽象的概念。现代编译器和库可用于 Go、Rust 和 C/C++。在决定编写 eBPF 程序之前,建议先学习基本的编程语言。根据我自己的经验,在具有 C++或 Python 知识之后,学习 Rust 是一条可行的发展道路。这有助于避免内存处理相关的运行时错误,与 C/C++ eBPF 程序相比,可以说这是一种更安全的方法。
Cillium 在一个Golang的开源库中实现了它的 eBPF 功能。除了学习编写自己的 eBPF 程序外,该库还提供了如下用例:将程序附加到入口/出口、计算 egress 流量包,以及探查网络接口(请注意 XDP 术语,以供后续学习)。XDP 程序可以用 Go 编译器工具链进行构建,并接受接口名称作为命令行参数。它使用 map 来持久化特定 IP 地址的网络包的数量;对于 Kubernetes 节点上的任意类型的网络接口,探查容器流量或跟踪嵌入式硬件的流量都是很好的使用场景。
如果你觉得编写 Rust 代码更舒服的话,aya-rs 的维护者提供了一个Rust开发人员工具链,包含一本带有教程的图书。书中的样例实现了一个类似的 XDP 网络流量场景,可以直接从 Cargo 构建链中运行,使开发过程更加高效。
样例程序没有跟踪 IP 地址及其数据包的数量,但是这可以作为一个很好的练习,模仿 Go 库样例中的行为。
aya-rs 的其他实际用例是持续剖析(profiling),Polar Signals 的开发人员将 Rust 库用到了 Parca 代理中,用于自动的函数调用栈分析和更好的内存安全性(来自KubeCon EU 2022 eBPF日上的幻灯片和Pull Request)。
有不同的方式来着手开发 eBPF 程序。请记住,该架构遵循将字节码编译的 eBPF 程序加载到内核,并需要一个用户空间的“收集器(collector)”或“打印器(printer)”。通信是通过套接字或文件句柄进行的。
测试和校验 eBPF 程序
在 CI/CD 流水线中自动化测试 eBPF 程序是很棘手的事情,因为内核会在加载时验证 eBPF 程序并拒绝潜在的不安全程序。测试将会需要一个新的虚拟机沙箱,加载 eBPF 程序,并模拟内核和 eBPF 程序相关的行为。需求包括触发事件,再次触发 eBPF 程序代码所订阅的钩子。根据不同的目的,这会涉及到不同的内核接口和系统调用(网络、文件访问等)。创建一个独立的单元测试 mock 是很难的,需要开发人员模拟一个运行中的内核。
有人试图将eBPF验证器转移到内核之外,并允许在 CI/CD 中测试 eBPF 程序。同时,在 CI/CD 中加载 eBPF 程序需要一个运行中的 Linux 虚拟机,其 CI/CD 的 runner/executor 要具有较高的权限。在 Ubuntu 22 LTS 中,加载非特权程序默认已被禁用,可能需要通过运行sudo sysctl kernel.unprivileged_bpf_disabled=0
来启用。
CI/CD 中的持续测试
为了提供持续测试的 CI/CD runner 环境,建议使用 Ansible/Terraform 生成一个 Linux 虚拟机,安装 CI/CD runner,将其注册到 CI/CD 服务器上,并准备好加载和运行 eBPF 程序的需求。对于不同的供应商来说,这是一个通用的模式。下面的样例使用 Ansible 安装并注册 GitLab Runner 到 GitLab.com 项目中,然后使用它来构建和运行 eBPF 程序。GitLab Runner 注册了标签ebpf
,它将只会执行使用了该标签的 CI/CD job。
注册需要gl_runner_registration_token
变量,该变量来自 GitLab 项目中针对 CI/CD Runners 的配置。
GitLab runner 可以在项目设置的CI/CD > Runners
中看到。
在 CI/CD 中测试基于 Rust 的 eBPF 程序
我们使用一个实际的 eBPF 程序来尝试一下 CI/CD 工作流,这里使用 aya-rs Rust 库模板作为演示样例。首先,在 Linux 虚拟机上本地安装 Rust 和所需的 eBPF,以验证一切均能正常运行。
接下来,生成一个模板骨架树,用于使用 XDP(eXpress Data Path)类型创建一个演示程序。探查ebpf-chaos-demo-xdp/src/main.rs
中的代码,并更新网络接口名。然后,构建并运行程序,将日志级别设置为 info(或 debug)。
示例代码由两部分组成:ebpf-chaos-demo-xdp-ebpf/src/main.rs
中的内核空间 eBPF 程序和ebpf-chaos-demo-xdp/src/main.rs
中的用户空间程序,后者会加载 eBPF 程序并将其附加至内核跟踪点。为了只构建 eBPF 程序,我们可以调用build-ebpf
xtask 并使用llvm-objdump
命令检查字节码:
完整的源代码位于该GitLab项目中,可以使用 GitLab CI/CD 流水线进行测试。注意,它需要在 runner 环境中安装 Rust 工具链。随后的流水线运行将会使用配置好的缓存。该流水线有三个 job:
install-deps
准备 Rust 环境,这需要将CARGO_HOME
变量指定为 runner 的项目目录。aya-rs-xdp-build-ebpf
构建核心 eBPF 程序,并运行llvm-objdump
命令。aya-rs-xdp-run
运行用户空间程序,这需要 sudo 权限。它会将命令放到后台,捕获 stdout,睡眠 60 秒,然后使用pkill
来杀死 xtask 命令,最后打印捕获到的输出。
对输出分析进行增强以及思考运行 eBPF 程序的更多测试报告是留给读者的练习。
该截屏显示了运行 eBPF 程序的 job,以及捕获网络数据包的日志输出。根据对源代码的修改,输出会发生变化并且可以进行测试。一个思路是以机器可读的格式总结捕获到的数据包,并在终止时创建一个汇总表。在 CI/CD 以及命令行中,这种方式更易于消费和理解。
将进程放入后台的方法可能无法正确地唤醒它,这可能需要更好的信号处理实现。它远远谈不上完美,你可以在这个合并请求中看到我的学习历史。可能有更好的方式来构建要发布的二进制文件,并通过supervisorctl
或systemd
命令来启动它,这是下一个学习步骤。终止和卸载过程的实现比较棘手。下面的代码片段实现了正确的信号处理,但是无法始终从运行中的内核卸载已注册的 XDP 链接。另一种方法是为每次的 CI/CD 运行生成一个新的 Linux 虚拟机,以避免这些可重复性相关的失败。但是,其缺点是我们需要一个 Rust 构建的远程缓存,以避免较长的 CI/CD 构建运行时间。
CI/CD 和 DevSecOps 工作流的额外待办事项
剩下的挑战就是扩展 eBPF 程序以生成测试报告,并创建运行时测试环境,即通过用 curl 命令运行网络流量测试周期,并验证输出包的确切大小。另外,架构也很重要,要么 eBPF 程序被加载到内核中,并且有一个用户空间应用来读取其结果,要么 eBPF 程序是一个单一的二进制文件,直接附加其探针。后者需要在 CI/CD job 中将程序发送至后台,捕获它的输出,执行测试,然后合并测试报告。对于 DevSecOps 工作流来说,这个过程还有许多需要改进的地方,但我相信在不久的将来我们会达到最终的目的。
代码覆盖是测试 eBPF 程序的另一个新领域。目前并没有太多的工具帮助开发人员理解代码在 Linux 内核中运行时的路径,哪些代码区域会受到影响,哪些代码没有被覆盖到。bpfcov
是由Elastic的工程师创建的,以帮助解决这个问题,让开发人员了解 eBPF 程序的代码执行路径。在 CI/CD 中运行自动化的代码质量和安全扫描也是一项挑战:如何确定一个有可能拖慢内核操作的编程错误呢?比较有意思的是,我们可以看一下 eBPF 程序的持续剖析(continuous profiling)是否可以实现(本身就是使用 eBPF 的,如Parca项目)。还有一些编程模式会规避内核验证器,并造成对软件供应链的安全攻击,通过贡献的拉取和合并请求,将恶意代码注入到已发布的 eBPF 程序中。这需要DevSecOps工作流来确保安全措施行之有效。AI 可能也会提供一些帮助。
结论
eBPF 是一种收集可观测性数据的新方法,它有助于实现网络洞察力,以及安全的可观测性和执行。为了获得最好的库、工具和框架,我们需要一起公开学习,以降低知识的壁垒,并使每个人都能做出贡献。从测试现有的工具到编写 eBPF 程序的详细教程,我们还有很长的路要走。在 CI/CD 中进行 eBPF 程序测试和验证是一项重要的工作,接下来就是将所有的想法带到上游,降低使用和贡献 eBPF 开源项目的入门门槛。
要想开始相关的工作,需要启动一个 Linux 虚拟机,使用脚本/Ansible 进行可重复的设置,并进行测试和开发。当接口名称和内核技术阻碍学习的进度时,那就回退一步,你并没有必要完全理解 eBPF 的全部内容。当遇到生产环境中断时,对数据收集有一般化的了解能够提供一定的帮助。最后,但同样重要的是,这里有个提示,那就是当调试 eBPF 程序时,考虑在多个发行版上进行测试,避免遇到内核相关的缺陷。
作者简介:
Michael Friedrich 是 GitLab 的高级开发人员布道者,专注于可观测性、DevSecOps 和 AI。Michael 创建了 o11y.love 作为可观测性学习平台,并在他的 opsindev.news 通讯中分享技术趋势以及对 day-2ops、eBPF、AI/MLOps 的见解。在没有旅行和远程工作的时候,他喜欢搭建乐高模型。
原文链接:
Learning eBPF for Better Observability
相关阅读:
评论