LWN:用BPF异步采集stack trace!

共 3019字,需浏览 7分钟

 ·

2024-07-01 12:45

关注了就能看到更多这么棒的文章哦~

Capturing stack traces asynchronously with BPF

By Daroc Alden
June 19, 2024
LSFMM+BPF
Gemini-1.5-flash translation
https://lwn.net/Articles/978736/

Andrii Nakryiko 在 2024 年的 Linux 存储、文件系统、内存管理和 BPF 峰会 (Linux Storage, Filesystem, Memory Management, and BPF Summit) 上主持了一场会议,介绍了使用 BPF 捕获堆栈跟踪的 API,以及如何使这些 API 更实用。BPF 程序可以捕获正在运行的进程的当前堆栈跟踪(stack trace),包括系统调用执行中位于内核的部分,这对于诊断性能问题等非常有用。但现有的 API 存在一些重大问题。

获取 BPF 中堆栈跟踪的现有方法是创建一个用于保存 trace 的元素的 BPF 映射(map),然后从 BPF 端调用 bpf_get_stackid() ,该函数返回此映射的唯一 ID,Nakryiko 解释道。然后,在用户空间中,程序可以进行正常的映射查找从而来检索堆栈跟踪。内核还捕获并存储一些相关信息,例如 ELF 构建 ID 和文件偏移量,这有助于确定堆栈跟踪对应于哪个程序,以便进行离线分析。他表示,这个 API 听起来相当简单,但不幸的是它有一些麻烦之处。

它可以捕获用户空间堆栈跟踪或内核堆栈跟踪,但不能同时捕获两者。内核支持捕获两者,只是 BPF API 无法表达出想同时要两者,Nakryiko 说。此外,也无法知道实际捕获的堆栈有多大;程序员只能希望他们的堆栈展开代码在正确的位置停止。这都是缺陷,但不是大问题,因为没有人抱怨过。

最后一个麻烦是内核会自动执行堆栈去重。如果捕获的堆栈跟踪与现有的堆栈跟踪匹配,内核将返回现有 ID。这种行为在理论上听起来很棒,因为去重可以节省空间,但用于存储堆栈跟踪的映射没有处理哈希冲突的逻辑。堆栈跟踪会被哈希并放置到映射中相应的位置,但每个位置只能容纳一个堆栈跟踪。因此,哈希冲突(哈希相同但实际上 trace 内容不同)在捕获大量堆栈跟踪时既频繁又不可避免。API 允许程序员指定在冲突时保留旧堆栈跟踪还是新堆栈跟踪,但这只是给他们留下了两个糟糕的选择:丢失数据或损坏数据。基于哈希的堆栈去重还使得清除映射中的条目产生了天生的竞争冲突。

Nakryiko 提出的新 API 通过让 BPF 程序处理内存管理来解决这些问题:它提供一个缓冲区,内核在其中捕获堆栈跟踪,然后 BPF 程序可以自由地以对应用程序最有意义的方式与用户空间共享该缓冲区。他指出,Meta 的所有用例(除一个外)都已切换到新的 API。但是,仍然有一些潜在的改进空间。目前,堆栈是同步捕获的。这是一个问题,因为 API 可以从任何地方调用,包括不允许出现页面错误(page fault)的上下文中。

如果正在检查的程序的一部分被换出(paged out),这意味着存储在这些页面上的信息可能无法被采集到。这只会影响用户空间堆栈跟踪,因为内核始终保持在内存中。这对使用基于 DWARF 的堆栈展开的程序来说是一个特殊问题,因为当进行捕获时,DWARF 调试信息不太可能存在于内存中。

Nakryiko 希望新的 API 是异步的,以便它可以等待必要的(供用户空间捕获)信息被换入。但是,这对 /kernel/ 堆栈跟踪不起作用,因为内核无法像用户空间那样暂停下来等待。另一方面,在内核返回用户空间之前,捕获用户空间堆栈跟踪可以被推迟而不会改变返回的信息,因为进程直到那时才会被冻结。他说,“返回用户空间”是一个很好的上下文,因为内核可以等待内存被换入等等。

所有这些单独的约束在提出的 API 设计中被汇集起来。Nakryiko 建议使用一个函数返回堆栈跟踪的唯一 ID。ID 就像是预留的一样——它是稳定的,可以被记录、传递给用户空间等等。一旦收到 ID,就会将堆栈跟踪捕获到保留的缓冲区中。内核跟踪是同步捕获的,但用户空间跟踪是在返回用户空间时捕获的。在相应映射中查找堆栈捕获将返回 EAGAIN ,直到捕获内容准备好。内核不会执行去重,使得删除元素以一种合理的方式工作。一位观众询问这是否意味着可能存在具有不同 ID 的相同堆栈跟踪,Nakryiko 确认情况确实如此。Daniel Borkmann 指出,如果操作是异步的,那么也可以扩展映射本身,而现有的 API 无法做到这一点。

Nakryiko 说,API 的一部分还没有想好:如何让用户知道堆栈跟踪何时准备好。他说,最简单的解决方案是不发送通知,而是强制用户轮询。稍微复杂一些的是,每当任何跟踪准备好时,都会发出映射范围的 epoll 通知。或者,映射中的每个槽位都可以有单独的 epoll 通知——但这将是浪费文件描述符。最后,最有效的方法是建立一个 BPF 环形缓冲区,将 ID 放入其中,以便在它们准备好时进行有效的通知和消费。

一位观众指出,Nakryiko 基本上描述了 io_uring 背后的机制,并建议说这可能是最合适的机制。Nakryiko 对 io_uring 是否适合不太确定,但承认了这种可能性。另一位观众询问他们是否可以将可扩展的环形缓冲区放入 BPF arena 中。Nakryiko 认为这不会有帮助,因为 BPF 子系统中已经有很多环形缓冲区,在 arena 中重新实现一个不会真正起到帮助。他确实注意到,如果他们切换到将整个堆栈跟踪放入环形缓冲区,他们就可以在环形缓冲区中包含丢弃通知(drop notification)。

Yu 指出,API 可以使用回调而不是通知:在完成时运行另一个 BPF 程序,并让用户决定如何通知用户空间。这引发了关于不同机制的长时间讨论,以及它们如何在灵活性与简单性之间权衡。Nakryiko 确实说他不赞成试图将太多功能从内核中移出的机制,因为内核已经对堆栈跟踪有良好的内置支持。一个例子是用户空间返回探测如何破坏堆栈跟踪——内核拥有所有必要的信息来修复这个问题,而更灵活的机制只会把这个工作推给用户。

在会议时间结束之前,讨论没有得出明确的结论,但内核中 BPF 及其他地方已经有很多机制用于在操作完成时通知用户空间,因此这似乎不太可能成为 API 设计的症结所在。考虑到 BPF 与 BPF 类型格式 (BTF, BPF Type Format) 的紧密集成以及对内核和用户空间中跟踪点(tracepoints)的支持,BPF 已经具有相当好的调试支持;看起来一些正在进行的工作可以帮助进一步扩展这种支持。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~



浏览 6
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报