Libseccomp: RFE:seccomp_rule_add 从 v2.4.0 开始变得非常慢

创建于 2019-05-01  ·  23评论  ·  资料来源: seccomp/libseccomp

你好,
该问题是由 v2.3.3 和 v2.4.0 之间的提交 ce3dda9a1747cc6a4c044eafe5a2eb653c974919 引入的。 考虑以下示例: foo.c.zip
它增加了非常多的规则。 并且在上述提交之后工作慢了大约 100 倍。

foo.c使用 v2.4.1 的执行时间: 0.448
foo.c使用 v2.3.3 的执行时间: 0.077

我挖了一点,发现 db_col_transaction_start() 复制了已经存在的过滤器集合并使用 arch_filter_rule_add() 来复制过滤器规则。 但是 arch_filter_rule_add() 调用 arch_syscall_translate() 调用 arch_syscall_resolve_name() 在 O(给定架构上的系统调用数)中工作。 因此,添加一条规则至少在 O(已添加规则的数量 * 已使用架构上的系统调用数量)中起作用,这在 IMO 中非常糟糕。
在上面的示例中,我计算了对 arch_filter_rule_add() 的调用次数,它等于201152

在提交之前,对 arch_filter_rule_add() 的调用次数是896 。 根据我对代码的理解,db_col_transaction_start() 也复制了已经存在的过滤器集合,并且不使用 arch_filter_rule_add()。 这给了我们估计:添加规则的时间大约为 O(已添加的规则数 + 给定架构上的系统调用数),这要好得多。

但是 IMO 它不应该与已经添加的规则的数量相关,因为添加 n 规则在 O(n^2) 中起作用。 但这是一个不同讨论的主题,因此对于小型过滤器或不经常生成的过滤器应该不是问题。

为什么这个问题很重要?
一些过滤器需要执行程序PID(例如,允许线程只向自己发送信号)。 所以如果受限程序需要执行相当多的次数,就会变成非常明显的开销。 我有一个包含大约 300 条规则的过滤器,每次运行沙盒进程时 libseccomp 开销约为 0.16 秒(我运行该进程数十次)。

预先感谢您的帮助!

enhancement prioritlow

最有用的评论

由于此更改,我们看到用户超时。 它确实使事情变慢了一个数量级。

所有23条评论

嗨@varqox。

是的,系统调用解析器函数可以使用一些改进,事实上,如果您查看代码,您会看到如下几条注释:

/* XXX - plenty of room for future improvement here */

如果您想改进该代码,我们应该使用帮助!

正如@pcmoore提到的,有很多机会可以使用 libseccomp 加速 seccomp 过滤器的 _creation_。 您的上述研究概述了可以改进的几个领域之一。 这不是我的用户关心的问题,所以我没有关注它。

关于 _runtime_ 性能,我目前正在为大型过滤器使用二叉树,例如您在 foo.c 中提供的过滤器。 我的内部客户的初步结果看起来很有希望,但我很想对这些变化另眼相看。 请参阅拉取请求https://github.com/seccomp/libseccomp/pull/152

好的,我看到系统调用解析可以改进,但这不是问题的根本原因。 在我看来,这就是在 db_col_transaction_start() 中创建快照。 调用了arch_filter_rule_add() ,因为解析系统调用很慢,这在原始规则中已解决。

我看到如下:我们想要复制整套当前过滤器(又名 struct db_filter)及其所有规则,因此我们从头开始_构造_所有过滤器,而不是利用我们已经拥有的并_复制_所有过滤器。 我们不必从头开始构建,我们已经完全构建了过滤器,我们只想要一个副本。 也许我错过了一些东西,但看起来可能对 db_col_transaction_start() 函数做了很多改进。

使用内部 libseccomp db 集合中的所有状态,复制它不是一项简单的任务,从原始规则重新生成集合要容易得多(从代码的角度来看)。 跟踪原始规则还允许我们提供“删除”现有规则的能力(可能的未来功能)。

这并不是说事务代码无法改进——它肯定可以——但当前代码之所以如此,是有原因的,主要是简单性。

由于此更改,我们看到用户超时。 它确实使事情变慢了一个数量级。

另一个想法,我们可能可以改变这一点,以便我们只在事务开始时复制规则,而不是整个树,并且只在失败的事务上重新创建树。 它并不完美,但这应该可以收回很大一部分时间。

我们需要做点什么,因为容器和 exec 进程的启动时间出现了巨大的性能下降,导致人们将其固定为 2.3 倍

我不会进一步评论这个问题的“巨大”_性质,这个观点已经被多次提出,我认为它既是相对的,也是依赖于用例的。 但是,我确实想提醒大家,v2.4 之前的 libseccomp 版本容易受到已公开的潜在漏洞的影响(问题 #139)。

对于那些关心这个问题的人,它目前被标记为 v2.5 版本。

您进行了重构,并且在次要版本中对性能产生了“巨大的”影响,并且吹嘘说它取决于用例并没有帮助。 请认真对待这一点,因为随着发行版更新到 2.4,人们将开始注意到这一点

@crosbymichael更改不仅仅是重构,还需要修复问题并支持内核中的更改(最值得注意的是需要支持多路复用和直接调用系统调用,例如 32 位 x86 上的套接字系统调用)。

我_不会_吹掉这个,我一直在思考解决这个问题的方法(见我上面的评论),事实上我已经把它标记为下一个小版本的东西。 在这一点上,我很难不认为您的评论具有煽动性,如果这不是您的意图,我建议您在将来发表评论时更加小心。 如果您对此问题的进展不满意,我们随时欢迎您提交补丁/PR 以供审核,以提供帮助。

请注意自己和其他正在考虑尝试解决此问题的人...

最近有人提醒我,为什么我们要做与交易相关的事情(预先复制所有内容); 我们这样做是因为我们需要能够在不失败的情况下回滚事务。 为什么?
正常的 seccomp_rule_add() 操作即使在失败的情况下也需要保持过滤器完好无损; 如果作为正常规则添加的一部分,我们使多部分事务(例如 x86/s390/s390x/等上的套接字/ipc 系统调用)失败,我们必须能够在事务开始时恢复到过滤器而不会失败(不管内存压力等)。

由于树的性质和树内部的链接,在没有规则的情况下复制树将继续具有挑战性,但我们可能能够有选择地选择何时需要创建内部事务,很多时候跳过它不需要的情况下。

我花了更多时间研究这个问题,由于我们在规则添加期间破坏性地修改决策树的方式,我不确定我们是否可以避免使用事务包装规则添加。 这意味着与其想办法在内部限制我们对事务的使用,我们需要找到一种方法来加快它们的速度,谢天谢地,我想我可能已经找到了一个解决方案:影子树。

目前,我们每次创建新事务时都会构建一个新树,并在成功时将其丢弃,正如我们所见,在某些用例中这可能会非常缓慢。 我的想法是,我们不是在提交时丢弃重复树,而是尝试将我们刚刚添加到重复树的规则添加到重复树(使其成为当前过滤器的副本)并将其保留为“影子事务”以加快下一个交易快照。 一些注意事项:

  • db_col_transaction_start()应该尝试使用影子事务(如果存在),但如果不存在,它应该回退到当前行为。
  • db_col_transaction_abort()的行为方式应该和现在一样; 这意味着失败的事务将清除影子事务(它需要树来恢复过滤器),但下一个成功的事务将恢复影子。 失败的交易应该足够少,以至于这不应该是一个主要问题。
  • 我们可能需要清除其他操作上的影子事务,例如arch/ABI ops?,但这是我们需要检查的东西。 无论如何,清除影子交易应该是微不足道的。
  • 这不仅具有加快规则添加速度的优势,而且还具有总体上加快交易速度的优势。 现在这可能并不重要,但是当我们向用户公开事务功能时它会很有用(如果我们想要做一个类似 BSD 的“质押”机制,这将是必要的)。

今晚晚饭后我有一些时间,所以我快速通过了实施上述影子交易的想法。 代码仍然很粗糙,我的测试(如下)甚至更粗糙,但看起来我们看到了这种方法的一些性能提升:

  • 基线测试开销
# time for i in {0..20000}; do /bin/true; done
real    0m10.479s
user    0m7.641s
sys     0m3.924s
  • 当前主分支
# time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

real    0m16.303s
user    0m12.584s
sys     0m4.501s
  • 已修补
time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

real    0m15.021s
user    0m11.540s
sys     0m4.387s

如果我们减去测试开销,我们会看到这个“测试”的性能大约提高了 20%,但我预计复杂过滤器集的好处会比这更好(好得多?)。

@varqox和/或@crosbymichael一旦我清理了一些补丁并创建了一个 PR,你能在你的环境中测试它吗?

我的示例测试用例已经在这里:

你好,
该问题是由 v2.3.3 和 v2.4.0 之间的提交ce3dda9引入的。 考虑以下示例: foo.c.zip
它增加了非常多的规则。 并且在上述提交之后工作慢了大约 100 倍。

foo.c使用 v2.4.1 的执行时间: 0.448
foo.c使用 v2.3.3 的执行时间: 0.077

但是只要 PR 准备好,我就可以在我的环境中对其进行测试。

@varqox ,是的,我看到您在原始报告中包含了一个测试用例,但我更想了解它在实际使用中的表现。 如果您可以试用 PR #180 并进行报告,我将不胜感激 - 谢谢!

@pcmoore

感谢您制作此 PR。
我构建并测试了您的 PR #180,结果对我的测试用例很有希望。 我关注这个问题是因为客户使用 docker health-check 并且遇到了libseccomp 2.4.x中的性能问题。
在我的测试用例中,这个 PR 的性能与libseccomp 2.3.3相当。 详情如下:

环境

MacBook Pro(15 英寸,2015 年中)上的 Ubuntu 19.04 VM(2 CPU,2G 内存)
内核 5.0.0-32-generic
码头工人 CE 19.03.2

测试用例:

准备20个容器:

for i in $(seq 1 20)
do
  docker run -d --name bb$i busybox sleep 3d
done

通过同时在所有容器上触发docker exec来运行测试

for i in $(seq 1 20)
do 
  /usr/bin/time -f "%E real" docker exec bb$i true & 
done

结果

libseccomp 2.3.3

0:01.05 real
0:01.12 real
0:01.16 real
0:01.20 real
0:01.23 real
0:01.27 real
0:01.31 real
0:01.35 real
0:01.37 real
0:01.38 real
0:01.40 real
0:01.41 real
0:01.40 real
0:01.40 real
0:01.45 real
0:01.46 real
0:01.47 real
0:01.48 real
0:01.48 real
0:01.49 real

libseccomp 2.4.1

0:00.98 real
0:01.63 real
0:01.67 real
0:01.95 real
0:02.55 real
0:02.70 real
0:02.70 real
0:02.96 real
0:03.04 real
0:03.16 real
0:03.17 real
0:03.21 real
0:03.23 real
0:03.27 real
0:03.24 real
0:03.29 real
0:03.27 real
0:03.29 real
0:03.28 real
0:03.27 real

您的 PR 构建

0:00.95 real
0:01.12 real
0:01.20 real
0:01.23 real
0:01.28 real
0:01.29 real
0:01.31 real
0:01.37 real
0:01.38 real
0:01.40 real
0:01.43 real
0:01.43 real
0:01.44 real
0:01.45 real
0:01.42 real
0:01.47 real
0:01.48 real
0:01.48 real
0:01.48 real
0:01.50 real

杂项说明

  • 在我建立这个 PR 之前,我在configure.ac中设置2.4.1中的AC_INIT
  • 这个自定义构建安装在/usr/local/lib中,我通过运行ldd /usr/bin/runc来验证它,以确保在测试期间使用自定义构建。
  • 我进行了几次测试,结果差异很小。 所以我相信它们不是偶然的结果。

太好了,感谢@xinfengliu 的帮助!

@pcmoore
感谢您的公关。
就我而言,它将 libseccomp 性能恢复到与 v2.3.3 相当的水平。

结果

foo.c

g++ foo.c -lseccomp -o foo -O3
for ((i=0; i<10; ++i)); do time ./foo; done 

libseccomp 2.3.3

./foo  0.01s user 0.00s system 98% cpu 0.018 total
./foo  0.02s user 0.00s system 98% cpu 0.020 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.018 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.018 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total

平均: 0.0188 s

libseccomp 2.4.2

./foo  0.19s user 0.00s system 99% cpu 0.195 total
./foo  0.19s user 0.00s system 99% cpu 0.194 total
./foo  0.19s user 0.00s system 99% cpu 0.193 total
./foo  0.19s user 0.00s system 99% cpu 0.196 total
./foo  0.19s user 0.00s system 99% cpu 0.195 total
./foo  0.20s user 0.00s system 99% cpu 0.196 total
./foo  0.19s user 0.00s system 99% cpu 0.194 total
./foo  0.20s user 0.00s system 99% cpu 0.197 total
./foo  0.19s user 0.00s system 99% cpu 0.195 total
./foo  0.19s user 0.00s system 99% cpu 0.194 total

平均: 0.1949 s

公关#180

./foo  0.01s user 0.01s system 98% cpu 0.012 total
./foo  0.01s user 0.00s system 97% cpu 0.013 total
./foo  0.01s user 0.00s system 96% cpu 0.013 total
./foo  0.01s user 0.01s system 97% cpu 0.014 total
./foo  0.01s user 0.00s system 97% cpu 0.012 total
./foo  0.01s user 0.00s system 98% cpu 0.013 total
./foo  0.01s user 0.00s system 98% cpu 0.012 total
./foo  0.01s user 0.00s system 98% cpu 0.013 total
./foo  0.01s user 0.00s system 97% cpu 0.013 total
./foo  0.01s user 0.00s system 97% cpu 0.011 total

平均: 0.0126 s

在这个综合测试中,这个 PR 似乎比 v2.3.3 有一些加速。

在我的沙箱中构建和加载(从 seccomp_init() 到 seccomp_load())过滤器以及一些沙箱初始化开销

libseccomp 2.3.3

Measured: 0.0052 s
Measured: 0.0040 s
Measured: 0.0046 s
Measured: 0.0042 s
Measured: 0.0038 s
Measured: 0.0038 s
Measured: 0.0039 s
Measured: 0.0036 s
Measured: 0.0042 s
Measured: 0.0044 s
Measured: 0.0036 s
Measured: 0.0037 s
Measured: 0.0044 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0040 s
Measured: 0.0037 s
Measured: 0.0043 s
Measured: 0.0042 s
Measured: 0.0035 s
Measured: 0.0034 s
Measured: 0.0038 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0037 s
Measured: 0.0038 s

平均: 0.0039 s

libseccomp 2.4.2

Measured: 0.0496 s
Measured: 0.0480 s
Measured: 0.0474 s
Measured: 0.0475 s
Measured: 0.0479 s
Measured: 0.0479 s
Measured: 0.0492 s
Measured: 0.0485 s
Measured: 0.0491 s
Measured: 0.0490 s
Measured: 0.0484 s
Measured: 0.0483 s
Measured: 0.0480 s
Measured: 0.0482 s
Measured: 0.0474 s
Measured: 0.0483 s
Measured: 0.0507 s
Measured: 0.0472 s
Measured: 0.0482 s
Measured: 0.0471 s
Measured: 0.0498 s
Measured: 0.0489 s
Measured: 0.0474 s
Measured: 0.0494 s
Measured: 0.0483 s
Measured: 0.0498 s
Measured: 0.0492 s

平均: 0.0466 s

公关#180

Measured: 0.0058 s
Measured: 0.0059 s
Measured: 0.0054 s
Measured: 0.0046 s
Measured: 0.0059 s
Measured: 0.0048 s
Measured: 0.0045 s
Measured: 0.0051 s
Measured: 0.0052 s
Measured: 0.0053 s
Measured: 0.0048 s
Measured: 0.0048 s
Measured: 0.0045 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0059 s
Measured: 0.0044 s
Measured: 0.0046 s
Measured: 0.0046 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0062 s
Measured: 0.0047 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0044 s

平均: 0.0049 s

尽管在综合测试中 PR 比 v2.3.3 提供更好的时间,但在现实世界中它稍微慢一些(可能是因为更复杂的规则和运行 seccomp_merge() 来合并两个大过滤器)。 但是,它仍然比 v2.4.2 提供大约十倍的加速。

感谢您验证性能@varqox! 一旦@drakenclimber回复了最后一轮评论(我解决了他可能提出的任何剩余问题),我们将把它合并。

啊,没关系,我只是注意到@drakenclimber确实将 PR 标记为已批准。 我现在要继续合并。

我刚刚合并了 PR #180,所以我认为我们可以将其标记为已关闭,如果有人注意到任何剩余的性能问题,请随时发表评论和/或重新打开。 感谢大家的耐心和帮助!

@pcmoore您是否计划尽快发布这些更改?

这目前是 libseccomp v2.5 发布里程碑的一部分,您可以使用以下链接跟踪我们在 v2.5 发布方面的进展:

此页面是否有帮助?
0 / 5 - 0 等级