当前位置: 首页 > news >正文

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

Python 被很多互联网系统广泛使用,但在另外一方面,它也存在一些性能问题,社区也提出了很多优化的方案。今天分享的是Flask库的作者,Armin Ronacher用 Rust 来优化 Python 的方案。译文由高可用架构公众号翻译。

5ed65bf1e458e05e6e208c17136abb96.png

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 处理的机器压力大大降低。

0612968dccdcf7f4bcdec45b623838ef.png

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

0f68bbe2f4a8a1382f2f8544ef72e2cb.png

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

69c4c60e7ca3313814247f21ab554055.png

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

d118590d16b7be9d3fd2ce881f475600.png

在 Python 中 嵌入 Rust

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

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

78c283902eb6a2341af2e75806e9c5ae.png

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

1ed579cf1e6eebad6f1046db15439399.png

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

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

a1d3c3fe2f1a42daad0f6eed7810ab30.png

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

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

cbcd6b8fc4f72b15db08f18aad8135e2.png

在 Rust 中暴露 C ABI

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

ff4ec6ab1cabbbada988f667b2db766f.png

boxed_landingpad 的工作方式很简单。它调用闭包,用 panic :: catch_unwind 捕获 panic,解开结果,并在原始指针中加上成功值。如果发生错误,它会填充 err_out 并返回一个 指针。在 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

回复下方「关键词」,获取优质资源

回复关键词「 pybook03」,立即获取主页君与小伙伴一起翻译的《Think Python 2e》电子版

回复关键词「书单02」,立即获取主页君整理的 10 本 Python 入门书的电子版

回复关键词「book 数字」,将数字替换成 0 及以上数字,有惊喜好礼哦~

题图:pexels,CC0 授权。

相关文章:

  • 大数据的说法 正确的是_python数据挖掘试题四十道,你敢来挑战吗?
  • 内核aio_来分析分析IO、NIO、AIO 内部原理!
  • 安装没法点底部_该怎样安装活动冷库风机?
  • 如何用python抢票_亲测,手把手教你用Python抢票-阿里云开发者社区
  • python分布式框架celery项目开发_Python芹菜分布式并行框架,PythonCelery
  • debian vbox设置_Debian 8.0下安装配置VirtualBox
  • 为什么家里pm25比外面高_家里别挂窗帘了,越来越多人喜欢这样设计,简洁大方,真的很实用...
  • key redis 遍历_java遍历读取整个redis数据库实例
  • edit控件自动换行 mfc_MFC限制edit控件的字符输入长度
  • c++高斯投影正反算_论文推荐 | 李松林:常用海图投影平面上大椭圆航线的表象与曲率分析...
  • 影之诗闪卡动图_影之诗闪卡修改攻略咋
  • mysql设计表结构注意_数据库(表结构)设计技巧及注意事项
  • mysql 连接状态_查看mysql连接数和状态
  • centos docker mysql_CentOS7利用docker安装MySQL5.7
  • mysql max和count嵌套_mysql – 在同一个查询中使用MAX()和COUNT()
  • 30天自制操作系统-2
  • Android Studio:GIT提交项目到远程仓库
  • exports和module.exports
  • php面试题 汇集2
  • Spring框架之我见(三)——IOC、AOP
  • 初识 beanstalkd
  • 关于for循环的简单归纳
  • 后端_MYSQL
  • 将回调地狱按在地上摩擦的Promise
  • 聊聊springcloud的EurekaClientAutoConfiguration
  • 前端 CSS : 5# 纯 CSS 实现24小时超市
  • 前端_面试
  • 实战:基于Spring Boot快速开发RESTful风格API接口
  • 微信公众号开发小记——5.python微信红包
  • 物联网链路协议
  • 《码出高效》学习笔记与书中错误记录
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • #Linux(权限管理)
  • #我与Java虚拟机的故事#连载19:等我技术变强了,我会去看你的 ​
  • $forceUpdate()函数
  • (1)STL算法之遍历容器
  • (LNMP) How To Install Linux, nginx, MySQL, PHP
  • (第9篇)大数据的的超级应用——数据挖掘-推荐系统
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (转)利用PHP的debug_backtrace函数,实现PHP文件权限管理、动态加载 【反射】...
  • (转载)从 Java 代码到 Java 堆
  • ***汇编语言 实验16 编写包含多个功能子程序的中断例程
  • .a文件和.so文件
  • .NET Standard 的管理策略
  • .vimrc php,修改home目录下的.vimrc文件,vim配置php高亮显示
  • [asp.net core]project.json(2)
  • [C++11 多线程同步] --- 条件变量的那些坑【条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)】
  • [DAU-FI Net开源 | Dual Attention UNet+特征融合+Sobel和Canny等算子解决语义分割痛点]
  • [IDF]被改错的密码
  • [IM] [Webhook] Webhook实现IM平台机器人
  • [JavaEE系列] Thread类的基本用法
  • [JS] 常用正则表达式集(一)
  • [lesson17]对象的构造(上)
  • [Linux] LVS+Keepalived高可用集群部署