炼数成金 门户 大数据 Python 查看内容

从20秒到0.5秒:一个使用Rust语言来优化Python性能的案例

2016-10-27 22:17| 发布者: 炼数成金_小数| 查看: 28656| 评论: 0|原作者: Armin Ronacher|来自: 高可用架构
摘要: Sentry 是一个帮助在线业务进行监控及错误分析的云服务,它每月处理超过十亿次错误。我们已经能够扩展我们的大多数系统,但在过去几个月,Python 写的 source map 处理程序已经成为我们性能瓶颈所在。(译者:source ...
工具 Python Hadoop BI C++
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 处理的机器压力大大降低。
最糟糕的 source map 处理时间减少到原来的十分之一。
更重要的是,平均处理时间减少到~ 400 ms。
JavaScript 是我们更受欢迎的项目语言,这种变化达到了将所有事件的端到端处理时间减少到~ 300 ms。
在 Python 中 嵌入 Rust
有很多方法可以暴露 Rust 库给 Python。我们选择将 Rust 代码编译成一个 dylib,并提供一些 ol'C 函数,通过 CFFI 和 C 头文件暴露给 Python。有了 C 语言头文件,CFFI 生成一些 shim( shim 是一个小型的函数库,用于透明地拦截 API 调用,修改传递的参数、自身处理操作、或把操作重定向到其他地方),可以调用 Rust。这样,libsourcemap 可以打开在运行时从 Rust 生成的动态共享库。

这个过程有两个步骤。第一个是在 setup.py 运行时配置 CFFI 的构建模块:
在构建模块之后,头文件通过 C 预处理器来处理,以便扩展宏( CFFI 本身无法执行的过程)。此外,这将告诉 CFFI 在哪里放置生成的 shim 模块。所有完成的之后,加载模块:
下一步是编写一些包装器代码来为 Rust 对象提供一个 Python API,这样能够转发异常。这发生在两个过程中:首先,确保在 Rust 代码中,我们尽可能使用结果对象。此外,我们需要处理好 panic,以确保他们不会跨越 DLL 边界。第二,我们定义了一个可以存储错误信息的帮助结构 ; 并将其作为 out 参数传递给可能失败的函数。

在 Python 中,我们提供了一个上下文管理器:
我们有一个特定错误类( special_errors)的字典,但如果没有找到具体的错误,将会抛一个通用的 SourceMapError。

从那里,我们实际上可以定义 source map 的基类:
在 Rust 中暴露 C ABI
我们从包含一些导出函数的 C 头开始,如何从 Rust 导出它们? 有两个工具:特殊的# [no_mangle] 属性和 std :: panic 模块 ; 提供了 Rust panic 处理器。我们自己建立了一些 helper 来处理这个:一个函数用来通知 Python 发生了一个异常和两个异常处理 helper,一个通用的,另一个包装了返回值。有了这个,包装方法如下:
boxed_landingpad 的工作方式很简单。它调用闭包,用 panic :: catch_unwind 捕获 panic,解开结果,并在原始指针中加上成功值。如果发生错误,它会填充 err_out 并返回一个 NULL 指针。在 lsm_view_free 中,只需要从原始指针重新构建。

构建扩展
要实际构建扩展,我们必须在 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 将来会带给我们什么。

译者注:不熟悉 source map 的同学请看阮一峰的这篇文章  http://www.ruanyifeng.com/blog/2013/01/ javascript_source_map.html

欢迎加入本站公开兴趣群
软件开发技术群
兴趣范围包括:Java,C/C++,Python,PHP,Ruby,shell等各种语言开发经验交流,各种框架使用,外包项目机会,学习、培训、跳槽等交流
QQ群:26931708

Hadoop源代码研究群
兴趣范围包括:Hadoop源代码解读,改进,优化,分布式系统场景定制,与Hadoop有关的各种开源项目,总之就是玩转Hadoop
QQ群:288410967 

鲜花

握手

雷人

路过

鸡蛋

最新评论

热门频道

  • 大数据
  • 商业智能
  • 量化投资
  • 科学探索
  • 创业

即将开课

  GMT+8, 2017-12-12 21:57 , Processed in 0.170980 second(s), 24 queries .