<轉> 從20秒到0.5秒:一個使用Rust語言來優化Python性能的案例

注: 轉自 微信公衆號「高可用架構」:從20秒到0.5秒:一個使用Rust語言來優化Python性能的案例javascript


導讀:Python 被不少互聯網系統普遍使用,但在另一方面,它也存在一些性能問題,不過 Sentry 工程師分享的在關鍵模塊上用另一門語言 Rust 來代替 Python 的狀況仍是比較罕見,也在 Python 圈引起了熱議,高可用架構小編將文章翻譯轉載以下。
clipboard.pnghtml

Sentry 是一個幫助在線業務進行監控及錯誤分析的雲服務,它每個月處理超過十億次錯誤。咱們已經可以擴展咱們的大多數系統,但在過去幾個月,Python 寫的 source map 處理程序已經成爲咱們性能瓶頸所在。(譯者:source map 就是將壓縮或者混淆過的代碼與原始代碼的對應表)java

從上週開始,基礎設施團隊決定調查 source map 處理程序的性能瓶頸。——咱們的 Javascript 客戶端已經成爲咱們最受歡迎的程序,其中一個緣由是咱們經過 source map 反混淆 JavaScript 的能力。然而,處理操做不是沒有代價的。咱們必須獲取,解壓縮,反混淆而後反向擴張,使 JavaScript 堆棧跟蹤可讀。python

當咱們在 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 處理的機器壓力大大下降。

clipboard.png

最糟糕的 source map 處理時間減小到原來的十分之一。

clipboard.png

更重要的是,平均處理時間減小到〜 400 ms。

clipboard.png

JavaScript 是咱們最受歡迎的項目語言,這種變化達到了將全部事件的端到端處理時間減小到〜 300 ms。

clipboard.png

在 Python 中 嵌入 Rust

有不少方法能夠暴露 Rust 庫給 Python。咱們選擇將 Rust 代碼編譯成一個 dylib,並提供一些 ol'C 函數,經過 CFFI 和 C 頭文件暴露給 Python。有了 C 語言頭文件,CFFI 生成一些 shim( shim 是一個小型的函數庫,用於透明地攔截 API 調用,修改傳遞的參數、自身處理操做、或把操做重定向到其餘地方),能夠調用 Rust。這樣,libsourcemap 能夠打開在運行時從 Rust 生成的動態共享庫。

這個過程有兩個步驟。第一個是在 setup.py 運行時配置 CFFI 的構建模塊:

clipboard.png

在構建模塊以後,頭文件經過 C 預處理器來處理,以便擴展宏( CFFI 自己沒法執行的過程)。此外,這將告訴 CFFI 在哪裏放置生成的 shim 模塊。全部完成的以後,加載模塊:

clipboard.png

下一步是編寫一些包裝器代碼來爲 Rust 對象提供一個 Python API,這樣可以轉發異常。這發生在兩個過程當中:首先,確保在 Rust 代碼中,咱們儘量使用結果對象。此外,咱們須要處理好 panic,以確保他們不會跨越 DLL 邊界。第二,咱們定義了一個能夠存儲錯誤信息的幫助結構 ; 並將其做爲 out 參數傳遞給可能失敗的函數。

在 Python 中,咱們提供了一個上下文管理器:

clipboard.png

咱們有一個特定錯誤類( special_errors)的字典,但若是沒有找到具體的錯誤,將會拋一個通用的 SourceMapError。

從那裏,咱們實際上能夠定義 source map 的基類:

clipboard.png

在 Rust 中暴露 C API

咱們從包含一些導出函數的 C 頭開始,如何從 Rust 導出它們? 有兩個工具:特殊的# [no_mangle] 屬性和 std :: panic 模塊 ; 提供了 Rust panic 處理器。咱們本身創建了一些 helper 來處理這個:一個函數用來通知 Python 發生了一個異常和兩個異常處理 helper,一個通用的,另外一個包裝了返回值。有了這個,包裝方法以下:

clipboard.png

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/blo...

相關文章
相關標籤/搜索