BPF Ring Buffer:使用场景、核心设计及程序示例

网站建设3年前发布
18 0 0

很多场景下,BPF 程序都需要将数据发送到用户空间(userspace), BPF perf buffer(perfbuf)是目前这一过程的事实标准,但它存在一些问题,例如 浪费内存(因为其 per-CPU 设计)、事件顺序无法保证等。,作为改进,内核 5.8 引入另一个新的 BPF 数据结构:BPF ring buffer(环形缓冲区,ringbuf),,perfbuf 是 per-CPU 环形缓冲区(circular buffers),能实现高效的 “内核-用户空间”数据交互,在实际中也非常有用,但 per-CPU 的设计 导致两个严重缺陷:,因此内核 5.8 引入了 ringbuf 来解决这个问题。ringbuf 是一个“多生产者、单消费者”(multi-producer, single-consumer,MPSC) 队列,可安全地在多个 CPU 之间共享和操作。perfbuf 支持的一些功能它都支持,包括,,此外,它还解决了 perfbuf 的下列问题:,perfbuf 为每个 CPU 分配一个独立的缓冲区,这意味着开发者通常需要 在内存效率和数据丢失之间做出折中:,ringbuf 的解决方式是分配一个所有 CPU 共享的大缓冲区,,另外,ringbuf 内存效率的扩展性也更好,比如 CPU 数量从 16 增加到 32 时,,如果 BPF 应用要跟踪一系列关联事件(correlated events),例如进程的启动和终止、 网络连接的生命周期事件等,那保持事件的顺序就非常关键。perfbuf 在这种场景下有一些问题:如果这些事件发生的间隔非常短(几毫秒)并且分散 在不同 CPU 上,那事件的发送顺序可能就会乱掉 ——这同样是 perbuf 的 per-CPU 特性决定的。,举个真实例子,几年前我写的一个应用需要跟踪进程 fork/exec/exit 事件,收集进程级别(per-process)的资源使用量。BPF 程序将这些事件 写入 perfbuf,但它们到达的顺序经常乱掉。这是因为内核调度器在不同 CPU 上调度进程时, 对于那些存活时间很短的进程,fork(), exec(), and exit() 会在极短的时间内在不同 CPU 上执行。这里的问题很清楚,但要解决这个问题,就需要在应用逻辑中加入大量的判断和处理, 只有亲自做过才知道有多复杂。,但对于 ringbuf 来说,这根本不是问题,因为它是共享的同一个缓冲区。ringbuf 保证 如果事件 A 发生在事件 B 之前,那 A 一定会先于 B 被提交,也会在 B 之前被消费。这个特性显著简化了应用处理逻辑。,BPF 程序使用 perfbuf 时,必须先初始化一份事件数据,然后将它复制到 perfbuf, 然后才能发送到用户空间。这意味着数据会被复制两次:,BPF ringbuf 提供了一个可选的 reservation/submit API 来避免这种问题。,应用就可以直接将准备发送的数据放到 ringbuf 了,从而节省了 perfbuf 中的第一次复制,,将数据提交到用户空间将是一件极其高效、不会失败的操作,也不涉及任何额外的内存复制。如果因为 buffer 没有空间而预留失败了,那 BPF 程序马上就能知道,从而也不用再 执行 perfbuf 中的第一步复制。后面会有具体例子。,对于所有实际场景(尤其是那些基于bcc/libbpf 的默认配置在使用 perfbuf 的场景), ringbuf 的性能都优于 perfbuf 性能。各种不同场景的仿真压测(synthetic benchmarking) 结果见内核 patch。,Per-CPU buffer 特性的 perfbuf 在理论上能支持更高的数据吞吐, 但这只有在每秒百万级事件(millions of events per second)的场景下才会显现。,在编写了一个真实场景的高吞吐应用之后,我们证实了 ringbuf 在作为与 perfbuf 类似的 per-CPU buffer 使用时,仍然可以作为 perfbuf 的一个高性能替代品,尤其是用到手动管理事件通知(manual data availability notification)机制时。,唯一需要注意、最好先试验一下的场景:BPF 程序必须在 NMI (non-maskable interrupt) context 中执行时,例如处理 cpu-cycles 等 perf events 时。,ringbuf 内部使用了一个非常轻量级的 spin-lock,这意味着如果 NMI context 中有竞争,data reservation 可能会失败。因此,在 NMI context 中,如果 CPU 竞争非常严重,可能会 导致丢数据,虽然此时 ringbuf 仍然有可用空间。,除了 NMI context 之外,在其他所有场景中优先选择 ringbuf 而不是 perfbuf 都是非常明智的。,完整代码见 bpf-ringbuf-examples project。,BPF 程序的功能是 trace 所有进程的 exec() 操作,也就是创建新进程事件。,每次 exec() 事件:收集进程 ID (pid)、进程名字 (comm)、可执行文件路径 (filename),然后发送给用户空间程序;用户空间简单通过 printf() 打印输出。用三种不同方式实现,输出都类似:,事件的结构体定义:,这里有意让这个结构体的大小超过 512 字节,这样 event 变量就无法 放到 BPF 栈空间(max 512Byte)上,后面会看到 perfbuf 和 ringbuf 程序分别怎么处理。,内核 BPF 程序,用户空间程序,完整代码 the user-space side, 基于 BPF skeleton(更多信息见 这里)。,看一个关键点:使用 libbpf user-space perfbuffer_new() API 来创建一个 perf buffer consumer:,这里设置 per-CPU buffer 为 32KB, 注意其中的 8 表示的是 number of memory pages,每个 page 是 4KB,因此总大小:8 pages x 4096 byte/page = 32KB。,完整代码:,内核 BPF 程序,bpf_ringbuf_output() 在设计上遵循了bpf_perf_event_output() 的语义, 以使应用从 perfbuf 迁移到 ringbuf 时更容易。为了看出二者有多相似,这里展示下 两个示例代码的 diff。,只有两个小改动:,ringbuf map 的大小(max_entries)可以在 BPF 侧指定了,注意这是所有 CPU 共享的大小。,bpf_perf_event_output() 替换成了类似的 bpf_ringbuf_output(),后者更简单,不需要 BPF context 参数。,用户空间程序,事件 handler 签名有点变化:,如果 CPU index 对你很重要,那你需要自己在 BPF 代码中记录它。,另外,ringbuffer API 不提供丢失数据(lost samples)的回调函数,而 perfbuffer 是支持的。如果需要这个功能,必须自己在 BPF 代码中处理。这样的设计对于一个(所有 CPU)共享的 ring buffer 能最小化锁竞争, 同时也避免了为不需要的功能买单:在实际中,这功能除了能用户在 userspace 打印出有数据丢失之外,其他基本也做不了什么, 而类似的目的在 BPF 中可以更显式和高效地完成。,第二个不同是 ringbuffer_new() API 更加简洁:,接下来基本上就是文本替换一下的事情了:perf_buffer__poll()- ring_buffer__poll(),bpf_ringbuf_output() API 的目的是确保从 perfbuf 到 ringbuf 迁移时无需对 BPF 代 码做重大改动,但这也意味着它继承了 perfbuf API 的一些缺点:,这意味着需要额外的空间来构建 event 变量,然后将其复制到 buffer。不仅低效, 而且经常需要引入只有一个元素的 per-CPU array,增加了不必要的处理复杂性。,如果这一步失败了(例如由于用户空间消费不及时导致 buffer 满了,或者有大量 突发事件导致 buffer 溢出了),那上一步的工作将变得完全无效,浪费内存空间和计算资源。,原理,如果能提前知道事件将在第二步被丢弃,就无需做第一步了, 节省一些内存和计算资源,消费端反而因此而消费地更快一些。但 xxx_output()风格的API 是无法实现这个目的的。这就是为什么引入了新的bpfringbufreserve()/bpfringbufcommit() API。,提前预留空间,或者能立即发现没有可以空间了(返回 NULL);,预留成功后,一旦数据写好了,将它发送到 userspace 是一个不会失败的操作。也就是说只要 bpf_ringbuf_reserve() 返回非空,那随后的 bpf_ringbuf_commit() 就永远会成功,因此它没有返回值。另外,ring buffer 中预留的空间在被提交之前,用户空间是看不到的, 因此 BPF 程序可以从容地组织自己的 event 数据,不管它有多复杂、需要多少步骤。这种方式也避免了额外的内存复制和临时存储空间(extra memory copying and temporary storage spaces)。,限制,唯一的限制是:BPF 校验器在校验时(at verification time), 必须知道预留数据的大小 (size of the reservation),因此不支持动态大小的事件数据。,内核 BPF 程序。,用户空间程序,用户空间代码与之前的 ringbuf output API 完全一样,因为这个 API 涉及到的只是提交方(生产方), 消费方还是一样的方式来消费。,在高吞吐场景中,最大的性能损失经常来自提交数据时,内核的信号通知开销(in-kernel signalling of data availability) ,也就是内核的 poll/epoll 通知阻塞在读数据上的 userspace handler 接收数据。,这一点对 perfbuf 和 ringbuf 都是一样的。,perfbuf 处理这种场景的方式是提供了一个采样通知(sampled notification)机制:每 N 个事件才会发送一次通知。用户空间创建 perfbuf 时可以指定这个参数。,这种机制能否解决问题,因具体场景而异。,ringbuf 选了一条不同的路:bpfringbufoutput() 和 bpfringbufcommit() 都支持一个额外的 flags 参数,,基于这个 flags,用户能实现更加精确的通知控制。例子见 BPF ringbuf benchmark。,默认情况下,如果没指定任何 flag,ringbuf 会采用自适应通知 (adaptive notification)机制,根据 userspace 消费者是否有滞后(lagging)来动态 调整通知间隔,尽量确保 userspace 消费者既不用承担额外开销,又不丢失任何数据。这种默认配置在大部分场景下都是有效和安全的,但如果想获得极致性能,那 显式控制数据通知就是有必要的,需要结合具体应用场景和处理逻辑来设计。,本文介绍了 BPF ring buffer 解决的问题及其背后的设计。,文中给出的示例代码和内核代码链接,展示了 ringbuf API 的基础和高级用法。希望阅读本文之后,读者能对 ringbuf 有一个很好的理解和把握,能根据自己的具体应用 选择合适的 API 来使用。,赵亚楠,携程资深架构师,负责携程云平台网络虚拟化、云原生安全、内核等基础设施研发工作。,译者序本文翻译自 BPF 核心开发者 Andrii Nakryiko 2020 的一篇文章:BPF ring buffer。,文章介绍了 BPF ring buffer 解决的问题及背后的设计,并给出了一些代码示例和内核 patch 链接,深度和广度兼备,是学习 ring buffer 的极佳参考。,由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。

© 版权声明

相关文章