返回资讯中心
外部精选
软件工程
#开发者实时流

开发者优化 eBPF 代理性能,重复文件打开速度提升 54%

来源标题:Speeding Up eBPF File Opens by 54% (Not AI Gen)

开发者通过对 eBPF 代理做一系列小改动,将重复文件打开速度提升了 54%,冷打开速度提升约 15%。这些改动遵循“1% 更好”的思路,相关代码已在 GitHub 开源,对应 commit 9c521bd 至 cfc7538。

Hacker News 最新nathannaveen2026/11/4 · 今天22 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

我和我哥把我们的 eBPF agent 设计得很快(用了位掩码等手段),不过最近我们又狠抓了一轮性能优化。

比如,我最近实现了一个 inode 缓存 https://nathannaveen.dev/posts/dropping-ebpf-cpu-cost-by-90/,我哥则在策略路径上实现了最长前缀匹配 https://neilnaveen.dev/posts/speeding-up-ebpf-filepath-checks/。这两处改动都挺大,而本文要讲的改动则比较小(思路类似于英国自行车队的“进步 1%”理念)。

但就是这些小改动,把重复文件打开的耗时降低了 54%,冷打开也快了约 15%。

这篇博客读起来不太像博客,更像一份变更日志。我之所以决定分享,是因为我觉得其中有些改动挺有意思,也许有人会感兴趣。

我们的仓库是开源的,所以本文提到的所有内容都能在 https://github.com/bomfather/agent 找到。本文讨论的所有改动都位于 9c521bd 和 cfc7538 两个 commit 之间。

做 inode 缓存时我们写了一个性能测试,之后每提交一个 commit 我都会跑一遍。然后我把性能结果交给 LLM,让它找出我可以修复的低效之处。

我知道有人心里会想:作者在标题里写了“Not AI Gen”,这家伙在撒谎。唉……我只是个凡人,不想自己去看性能结果,所以就交给我的 AI 了。我不用 AI 写代码,不代表我不知道 AI 擅长什么。

前面说过,这些都是我为了用最小的改动换取最大的性能提升而做的简单改动,所以没有哪个是革命性的想法。

我知道最长前缀匹配(LPM)不是人人都熟悉的东西,但我不打算在本文里解释它,因为这不是本文的重点。如果你不了解它,我建议你去查一查,因为它挺酷,理解起来也不算复杂。

我们的 LPM 缓存会创建 128 字节的块,并给每个块分配一个 ID。然后把这些 ID 和文件路径的剩余部分存进 LPM trie map。

之前,即使尾部不足 128 字节,也照样会走一遍查找。于是内核会清空 chunk map 的 key,把剩余的字节拷贝进去,再搜索这个 map。但这个 key 实际上并不在 map 里,因为它不是完整的 128 字节块。

这次的改动只是一个 if 语句,检查块长度是否小于 128 字节。如果是,我们就知道不必走那一整套 map 查找流程。

我觉得这个改动挺酷,因为大多数路径都短于 128 字节,所以它在唯一的一次迭代中就省掉了 map miss。

代码:https://github.com/bomfather/agent/commit/2754d343585c6b8acd1184bca28faf73468c21b9。

如我所说,我想做的是 1% 的改进。所以这是个极小极小的改动,但影响相当显著。

这更像是给我之前的方案打补丁,而不是一项新的改进。

那么……呃……在我上一篇博客 https://nathannaveen.dev/posts/dropping-ebpf-cpu-cost-by-90/ 里,我说我们会先检查 inode 缓存的 key 是否在缓存中,只有在不在时才构建路径。是的……嗯,结果发现 lsm_file_open 一直都会先构建路径。我本意是在构建文件路径之前先检查缓存,但估计我忘了写这段代码(写到这里我尴尬地笑了)。

好吧,我对这个问题的解决办法就是把修复补上,然后把这桩糗事悄悄掩盖过去 :) 我确实就是这么做的:https://github.com/bomfather/agent/commit/d50233e1bc4a0790b8ee08dcc9196ce7d0b82328。

好的一面是,即使在检查缓存之前就构建文件路径,我们也看到了很好的性能提升,而在实现这个改动之后性能更好。结局好,一切都好。

我们的 agent 可以拦截 ptrace,因为服务器上通常不需要 ptrace(如果你确实在用 ptrace,就不必启用 ptrace 拦截)。

这很好,但大多数进程都会读 /proc/<pid>/stat。而 /proc/<pid>/stat 在内核里会调用 ptrace_may_access,所以我们的 ptrace hook 会把它当成一次 trace 读取。于是开启 ptrace 拦截时,agent 会把对 /proc/<pid>/stat 的访问也拦掉。

为了解决这个问题,我们让 lsm_inode_permission 检查 inode,当 inode 是名为 stat 的 procfs 文件时,就在对应 pid 的 task 上存一个一次性 token。然后在接下来的 ptrace_access_check 中,如果 gate 已设置、pid 匹配且 mode 是读,我们就放行该操作。随后清除 token,为下一次访问 /proc/<pid>/stat 做准备。

但如果某个进程打开了 /proc/<pid>/stat 却没走到 ptrace 检查,gate 就会一直保持设置状态,下一次 ptrace 即使本该被拦截也能通过。为解决这个问题,我们用 tp_exit_openat 和 tp_exit_openat2 在 ptrace hook 没有消费 token 时清除它。

我加快这一步的做法是:当 ptrace 拦截启用时,只加载 lsm_inode_permission、tp_exit_openat 和 tp_exit_openat2 这三个程序。之所以能这样做,是因为只有在 ptrace 拦截启用、且有人试图访问 /proc/<pid>/stat 时才会用到这些函数。

代码:https://github.com/bomfather/agent/commit/20ecdab9b54dc8f40182dd8b27f192078e1f973f

我在 commit 9c521bd 和 cfc7538 上分别跑了之前用于 inode 缓存的基准测试。

这个基准测试会把同一个文件打开 200,000 次,以模拟重复打开;然后打开 20,000 个全新的文件,让这些新打开都命中不了 inode 缓存。之后重复 5 次,以确保各轮之间不会有太大离散度。

那么,经过这些改动,重复打开的耗时从 4.68 µs 降到 2.15 µs,大约降低了 54%。冷打开则从 6.33 µs 降到 5.36 µs,大约降低了 15%。重复打开的 p99 从 8.3 µs 降到 3.46 µs,提升相当不错。但这些改动对冷打开的 p99 帮助不大。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →