导读:Python 被很多互联网系统广泛使用,但在另外一方面,它也存在一些性能问题,不过 Sentry 工程师分享的在关键模块上用另外一门语言 Rust 来代替 Python 的情况还是比较罕见,也在 Python 圈引发了热议,高可用架构小编将文章翻译转载如下。


python rust 结合 rust vs python_Rust

Sentry 是一个帮助在线业务进行监控及错误分析的云服务,它每月处理超过十亿次错误。我们已经能够扩展我们的大多数系统,但在过去几个月,Python 写的 source map 处理程序已经成为我们性能瓶颈所在。(译者:source map 就是将压缩或者混淆过的代码与原始代码的对应表)

从上周开始,基础设施团队决定调查 source map 处理程序的性能瓶颈。——我们的 Javascript 客户端已经成为我们最受欢迎的程序,其中一个原因是我们通过 source map 反混淆 JavaScript 的能力。然而,处理操作不是没有代价的。我们必须获取,解压缩,反混淆然后反向扩张,使 JavaScript 堆栈跟踪可读。

当我们在 4 年前编写了原始处理流水线时,source map 生态系统才刚刚开始演化。随着它成长为一个复杂而成熟的 source map 处理程序,我们花了很多时间用 Python 来处理问题。

截至昨天,我们通过 Rust 模块替换我们老的 Python 的 souce map 处理模块,大大减少了处理时间和我们的机器上的 CPU 利用率。

为了解释这一切,我们需要先理解 source map 和用 Python 的缺点。

Python 的 Source Maps

随着我们的用户的应用程序变得越来越复杂,他们的 source map 也越来越复杂。在 Python 中解析 JSON 本身是足够快的,因为它们只是字符串而已。问题在于反序列化。每个 source map token 产生一个 Python 对象,我们有一些 source map 可能有几百万个 token。

将 source map token 反序列化的问题使得我们为基本 Python 对象支付巨大的成本。另外,所有这些对象都参与引用计数和垃圾收集,这进一步增加了开销。处理 30MB source map 使得单个 Python 进程在内存中扩展到〜 800MB,执行数百万次内存分配,并使垃圾收集器非常忙碌(译者注:token 是短生命周期对象,有新生代就好多了,这时候就体现出我大 Java 的优势了)。

由于这种反序列化需要对象头和垃圾回收机制,我们能在 Python 层做改进的空间非常小。

Rust 的 Source Maps

在调查发现问题在于 Python 的性能缺陷后,我们决定尝试 Rust source map 解析器的性能,这是为我们的 CLI 工具编写的。在将 Rust 解析器应用于问题很大的 source map 之后,其表明单独使用该库进行解析可以将处理时间从 > 20 秒减少到 < 0.5 秒。这意味着即使忽略任何优化,只是将 Python 解析器替换为 Rust 解析器就可以缓解我们的性能瓶颈。

我们证明 Rust 确实更快后,就清理了一些 Sentry 内部 API,以便我们可以用新的库替换原来的实现。这个 Python 库命名为 libsourcemap,是我们自己的 Rust source map 的一个薄包装。

优化结果

部署该库后,专门用于 source map 处理的机器压力大大降低。

python rust 结合 rust vs python_高可用架构_02

最糟糕的 source map 处理时间减少到原来的十分之一。

python rust 结合 rust vs python_Rust_03

更重要的是,平均处理时间减少到〜 400 ms。

python rust 结合 rust vs python_Python_04

JavaScript 是我们最受欢迎的项目语言,这种变化达到了将所有事件的端到端处理时间减少到〜 300 ms。

python rust 结合 rust vs python_python rust 结合_05

在 Python 中 嵌入 Rust

有很多方法可以暴露 Rust 库给 Python。我们选择将 Rust 代码编译成一个 dylib,并提供一些 ol'C 函数,通过 CFFI 和 C 头文件暴露给 Python。有了 C 语言头文件,CFFI 生成一些 shim( shim 是一个小型的函数库,用于透明地拦截 API 调用,修改传递的参数、自身处理操作、或把操作重定向到其他地方),可以调用 Rust。这样,libsourcemap 可以打开在运行时从 Rust 生成的动态共享库。

这个过程有两个步骤。第一个是在 setup.py 运行时配置 CFFI 的构建模块:

python rust 结合 rust vs python_python rust 结合_06

在构建模块之后,头文件通过 C 预处理器来处理,以便扩展宏( CFFI 本身无法执行的过程)。此外,这将告诉 CFFI 在哪里放置生成的 shim 模块。所有完成的之后,加载模块:

python rust 结合 rust vs python_高可用架构_07

下一步是编写一些包装器代码来为 Rust 对象提供一个 Python API,这样能够转发异常。这发生在两个过程中:首先,确保在 Rust 代码中,我们尽可能使用结果对象。此外,我们需要处理好 panic,以确保他们不会跨越 DLL 边界。第二,我们定义了一个可以存储错误信息的帮助结构 ; 并将其作为 out 参数传递给可能失败的函数。

在 Python 中,我们提供了一个上下文管理器:

python rust 结合 rust vs python_python rust 结合_08

我们有一个特定错误类( special_errors)的字典,但如果没有找到具体的错误,将会抛一个通用的 SourceMapError。

从那里,我们实际上可以定义 source map 的基类:

python rust 结合 rust vs python_高可用架构_09

在 Rust 中暴露 C ABI

我们从包含一些导出函数的 C 头开始,如何从 Rust 导出它们? 有两个工具:特殊的# [no_mangle] 属性和 std :: panic 模块 ; 提供了 Rust panic 处理器。我们自己建立了一些 helper 来处理这个:一个函数用来通知 Python 发生了一个异常和两个异常处理 helper,一个通用的,另一个包装了返回值。有了这个,包装方法如下:

python rust 结合 rust vs python_python rust 结合_10

构建扩展

要实际构建扩展,我们必须在 setuptools 中做一些不太优雅的事情。幸运的是,在这件事上我们没有花太多时间,因为我们已经有一个类似的工具来处理。

这个做法最方便的部分是源代码用 cargo 编译,二进制安装最终的 dylib,消除任何最终用户使用 Rust 工具链的需要。

那些做得好,那些没做好?

我在 Twitter 上被问到:“ Rust 会有什么替代品?”说实话,Rust 很难替代。原因是,除非你想用性能更好的语言重写整个 Python 组件,否则只能使用本机扩展。在这种情况下,对语言的要求是相当苛刻的:它不能有一个侵入式运行时,不能有一个 GC,并且必须支持 C ABI。现在,我认为适合的语言是 C,C++ 和 Rust。

哪方面工作的好:

  • 结合 Rust 和 Python 与 CFFI。有一些替代品,链接到 libpython,但构建更复杂。
  • 在老一些的 CentOS 版本使用 Docker 来构建可移植的 Linux 容器。虽然这个过程是乏味的,然而不同的 Linux 发兴版和内核之间的稳定性的差异使得 Docker 和 CentOS 成为可接受的构建解决方案。
  • Rust 生态系统。我们使用 crates.io 的 serde 反序列化和 base64 库,两个库工作非常好。此外,mmap 支持使用由社区 memmap 提供的另一库。

哪方面工作的不好:

  • 迭代和编译时间真的可以更好。我们每次更改字符时都编译模块和头文件。
  • setuptools 步骤非常脆弱。我们可能花了更多的时间来使 setuptools 工作。幸运的是,我们以前做过一次,所以这次更容易。

虽然 Rust 对我们的工作帮助很大,毫无疑问,有很多需要改进。特别是,用于导出 C ABI(并使其对 Python 有用)的基础设施应该有很大改进空间。编译时间也不是很长(译者的话,不是很长的意思是可能够我沏杯茶,怀念 go 的编译速度)。希望增量编译将有所帮助。

下一步

其实我们还有更多的改进空间。我们可以以更高效的格式启动缓存,比如一组存储在内存中的结构体而不是使用解析 JSON。特别是,如果与文件系统缓存配对,我们几乎可以完全消除加载的成本,因为我们平分了索引,这可以使用 mmap 非常有效。

鉴于这个好的结果,我们很可能会评估 Rust 更多在未来处理一些 CPU 密集型的业务。然而,对于大多数其他操作,程序花更多的时间等待 IO。

小结

虽然这个项目取得了巨大的成功,但是我们只花了很少的时间来实现。它降低了我们的处理时间,它也将帮助我们水平扩展。Rust 一直是这个工作的完美工具,因为它允许我们将昂贵的操作使用本地库完成,而且不必使用 C 或 C ++(这不太适合这种复杂的任务)。虽然很容易在 Rust 中编写 source map 解析器,但是使用 C / C++ 来完成的话,代码更多,且没那么有意思。

我们确实喜欢 Python,并且是许多 Python 开源计划的贡献者。虽然 Python 仍然是我们最喜欢的语言,但我们相信在合适的地方使用合适的语言。Rust 被证明是这项工作的最佳工具,我们很高兴看到 Rust 和 Python 将来会带给我们什么。


python rust 结合 rust vs python_高可用架构_11