深入探究BPF LPM Trie性能与优化策略

本文深入分析了BPF LPM Trie数据结构的性能瓶颈,探讨了查找、更新、删除操作的具体性能数据,并提出了多比特比较和路径压缩等优化方案,对网络数据包路由和防火墙规则评估具有重要价值。

深入探究BPF LPM Trie性能与优化

一切始于生产环境中出现的神秘软锁定消息。这一行神秘的代码将我们带入了我们使用的最基本数据结构之一——BPF LPM Trie的性能研究之旅。

BPF Trie映射(BPF_MAP_TYPE_LPM_TRIE)在网络数据包路由中广泛用于IP和IP+端口匹配,确保请求在返回结果前通过正确的服务。这种数据结构的性能对我们的客户服务至关重要,但当前实现的性能表现仍有很大改进空间。

Trie数据结构回顾

Trie(字典树)是一种树形数据结构,允许您存储和搜索给定键的数据,每个节点存储一定数量的键位。搜索通过遍历路径执行,这实际上从遍历路径重建键,意味着节点不需要存储其完整键。

与需要每个节点存储完整键的传统二叉搜索树(BST)不同,Trie在数据具有冗余时(例如键中常见前缀)非常内存高效,因为共享数据只需要一组节点。这就是为什么Trie通常用于高效存储字符串,例如单词字典。

Trie的设计使其非常适合执行最长前缀匹配和使用CIDR处理IP路由。CIDR是为了更有效地利用IP地址空间而引入的,但带来了额外的复杂性,因为IP地址的网络部分现在可以出现在任何位置。

BPF LPM Trie性能基准测试

在AMD EPYC 9684X 96核机器上运行BPF自测基准测试的结果显示:

操作 吞吐量 标准差 延迟
查找 7.423M 操作/秒 0.023M 操作/秒 134.710 ns/操作
更新 2.643M 操作/秒 0.015M 操作/秒 378.310 ns/操作
删除 0.712M 操作/秒 0.008M 操作/秒 1405.152 ns/操作
释放 0.573K 操作/秒 0.574K 操作/秒 1.743 ms/操作

释放具有10K条目的BPF LPM Trie的时间明显很长。我们最近遇到了一个问题,这个过程耗时过长,导致生产环境中出现软锁定消息。

为什么BPF LPM Trie速度慢?

内核中的LPM Trie实现(kernel/bpf/lpm_trie.c)具有我们在介绍中讨论的一些优化。它能够在叶节点执行多比特比较,但由于每个内部节点只有两个子指针,如果树密集填充了大量仅相差一位的数据,这些多比特比较会退化为单比特比较。

这种双孩子设计影响了Trie的高度。在最坏情况下,完全满的Trie基本上变成了高度为log2(nr_entries)的二叉搜索树,而Trie的高度会影响搜索键所需的比较次数。

BPF LPM Trie还不实现级别压缩。这同样源于Trie中的节点只能有2个子节点的事实。IP路由表往往有许多共同的前缀,通常在上层看到密集打包的Trie,这使得级别压缩对包含IP路由的Trie非常有效。

性能下降原因分析

随着条目数量从1个增加到100K个,LPM Trie的查找吞吐量(以百万操作/秒计)逐渐下降。当达到100万个条目时,吞吐量约为150万操作/秒,并随着条目数量增加而继续下降。

最初,这是由于L1 dcache未命中率。Trie中需要遍历的所有节点都是潜在的缓存未命中机会。在大约80K条目时,dTLB未命中率成为瓶颈。

因为BPF LPM Trie尝试从内核内存的自由列表中动态分配单个节点,这些节点可以位于任意地址。这意味着遍历Trie路径几乎肯定会引起缓存未命中,并可能引起dTLB未命中。随着条目数量和Trie高度的增加,这种情况会变得更糟。

未来方向

通过了解BPF LPM Trie的当前限制,我们现在可以为互联网的未来构建更高性能、更高效的解决方案。

我们已经将这些基准测试贡献给上游Linux内核——但这只是开始。我们计划改进BPM LPM Trie的性能,特别是对我们工作负载大量使用的查找函数。本文涵盖了net/ipv4/fib_trie.c代码已经使用的许多优化,因此自然的第一个步骤是重构该代码,以便可以使用通用的级别压缩Trie实现。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计