《從20秒到0.5秒:一個使用Rust語言來優化Python性能的案例》要點:
本文介紹了從20秒到0.5秒:一個使用Rust語言來優化Python性能的案例,希望對您有用。如果有疑問,可以聯系我們。
導讀:Python 被很多互聯網系統廣泛使用,但在另外一方面,它也存在一些性能問題,不過 Sentry 工程師分享的在關鍵模塊上用另外一門語言 Rust 來代替 Python 的情況還是比較罕見,也在 Python 圈引發了熱議,高可用架構小編將文章翻譯轉載如下.
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 的缺點.
隨著我們的用戶的應用程序變得越來越復雜,他們的 source map 也越來越復雜.在 Python 中解析 JSON 本身是足夠快的,因為它們只是字符串而已.問題在于反序列化.每個 source map token 產生一個 Python 對象,我們有一些 source map 可能有幾百萬個 token.
將 source map token 反序列化的問題使得我們為基本 Python 對象支付巨大的成本.另外,所有這些對象都參與引用計數和垃圾收集,這進一步增加了開銷.處理 30MB source map 使得單個 Python 進程在內存中擴展到? 800MB,執行數百萬次內存分配,并使垃圾收集器非常忙碌(譯者注:token 是短生命周期對象,有新生代就好多了,這時候就體現出我大 Java 的優勢了).
由于這種反序列化需要對象頭和垃圾回收機制,我們能在 Python 層做改進的空間非常小.
在調查發現問題在于 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.
有很多方法可以暴露 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 的基類:
我們從包含一些導出函數的 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 對我們的工作幫助很大,毫無疑問,有很多需要改進.特別是,用于導出 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
文章出處:高可用架構
轉載請注明本頁網址:
http://www.snjht.com/jiaocheng/4404.html