我們用Rust重寫了我們的專案……速度快了40倍

語言: CN / TW / HK

前言

Rust 已經悄然成為了最受歡迎的程式語言之一。作為一門新興底層系統語言,Rust 擁有著記憶體安全性機制、接近於 C/C++ 語言的效能優勢、出色的開發者社群和體驗出色的文件、工具鏈和IDE 等諸多特點。本文將介紹筆者使用 Rust 重寫專案並逐步落地生產環境的過程,以及在重寫過程選擇 Rust 的原因、遇到的問題以及使用 Rust 重寫帶來的成果。

我們目前正在使用 Rust 開發的專案叫做 KCL,目前全部實現程式碼已經在 Github 上開源。KCL 是一個基於約束的記錄及函式領域程式語言,致力於通過成熟的程式語言技術和實踐來改進特領域如雲原生 kubernetes 領域的大量繁雜配置編寫和安全策略校驗等,致力於構建圍繞配置的更好的模組化、擴充套件性和穩定性,更簡單的邏輯編寫,以及更快的自動化整合和良好的生態延展性。更具體的 KCL 使用場景請訪問 KCL 網站,本文中不再過多贅述。

KCL 之前是使用 Python 編寫的,出於使用者使用體驗、效能和穩定性的考慮,決定用 Rust 語言進行重寫,並獲得了以下好處:

  • 更少的 Bug,源於 Rust 強大的編譯檢查和錯誤處理方式
  • 語言端到端編譯執行效能提升了 66%
  • 語言前端解析器效能提升了 20 倍
  • 語言中端語義分析器效能提升了 40 倍
  • 語言編譯器編譯過程平均記憶體使用量變為原來 Python 版本的一半

我們遇到了什麼問題

就像社群中同類型專案 deno, swc, turbopack, rustc 等編譯器、構建系統或者執行時在技術上使用 Rust 做的事情類似,我們使用 Rust 完整構建了編譯器的前中端和執行時,取得了一定的階段性成果,但是我們大約在一年前並不是這個樣子的。

一年前,我們使用 Python 語言構建了整個 KCL 語言編譯器的實現,雖然在一開始的時候執行良好,Python 簡單易上手,生態豐富,團隊的研發效率也很高,但是隨著程式碼庫的擴張和工程師人數的增加,程式碼維護起來愈加困難,儘管我們在專案中強制編寫 Python 型別註解,採用更嚴格的 lint 工具,程式碼測試行覆蓋率也達到了 90% 以上,但是仍然會出現很多諸如 Python None 空物件,屬性未找到等執行時才會出現錯誤,並且重構 Python 程式碼時也需要小心翼翼,反應到 KCL 語言上就是一個接一個的 bug, 嚴重影響使用者使用體驗。

此外,當 KCL 使用物件是廣大開發者使用者時,程式語言或者說編譯器內部實現出現任何錯誤都是不可容忍的,這些也給我們的使用者使用體驗帶來了一系列問題,使用 Python 編寫的程式啟動速度較慢,效能無法滿足自動化系統線上編譯和執行的效率訴求,因為在我們的場景中,使用者修改 KCL 程式碼後需要能很快的展示編譯結果,顯然使用 Python 編寫的編譯器並不能很好地滿足使用需求。

為什麼選擇 Rust

筆者所在團隊基於如下原因選擇了 Rust

  • 使用 Go, Python, Rust 三種語言實現了簡單的程式語言棧式虛擬機器並作了效能對比,Go 和 Rust 在這個場景下效能接近,Python 有較大效能差距,綜合考慮下采用了 Rust,具體三種語言實現的棧式虛擬機器程式碼細節在 https://github.com/Peefy/StackMachine,感興趣的同學可以前往瀏覽
  • 越來越多的程式語言的編譯器或執行時特別是前端基礎設施專案採用 Rust 編寫或重構,此外基礎設施層,資料庫、搜尋引擎、網路設施、雲原生、UI 層和嵌入式等領域都有 Rust 的出現,至少在程式語言領域實現方面經過了可行性和穩定性驗證
  • 考慮到後續的專案發展會涉及區塊鏈和智慧合約方向,而社群中大量的區塊鏈和智慧合約專案採用 Rust 編寫
  • 通過 Rust 獲得更好的效能和穩定性,讓系統更容易維護、更加健壯的同時,可以通過 FFI 暴露 C API 供多語言使用和擴充套件,方便生態擴充套件與整合
  • Rust 對 WASM 的支援比較友好,社群中大量 WASM 生態是由 Rust 構建,KCL 語言和編譯器可以藉助 Rust 編譯到 WASM 並在瀏覽器中執行

基於以上原因綜合考慮選擇了 Rust 而不是 Go,整個重寫過程下來發現 Rust 綜合素質確實過硬(第一梯隊的效能,足夠的抽象程度),雖然在一些語言特性特別是生命週期等上手成本有一些,生態上還不夠豐富,總之程式語言可以做的事情,Rust 均可以做,具體可能還是要根據具體的場景和問題來做選擇。同時如果想要使用好 Rust, 還需要深入理解記憶體、堆疊、引用、變數作用域等這些其它高階語言往往不會深入接觸的內容。

使用 Rust 過程中遇到了哪些困難

雖然決定了使用 Rust 重寫整個 KCL 專案,其實團隊成員大部分成員是沒有使用 Rust 編寫一定程式碼體量專案的經驗,包括筆者個人自己也僅僅學習過 《The Rust Programming Language》 中的部分內容,依稀記得學習到 RcRefCell 等智慧指標內容就放棄了,那時沒想到 Rust 中還能有與 C++ 中類似的東西。

使用 Rust 前預估的風險主要是 Rust 語言接觸和學習的成本,這個確實在各種 Rust 的文章部落格中均有提到,因為 KCL 專案整體架構並未發生太大變化,只是部分模組設計和程式碼編寫針對 Rust 作了優化,因此整個重寫是在邊學邊實踐中進行。確實在剛開始使用 Rust 編寫整個專案的時候花費在知識查詢、編譯排錯的時間還是很多的,不過隨著專案的進行漸入佳境,筆者個人經驗使用 Rust 遇到的困難主要是心智轉換和開發效率兩方面:

心智轉換

首先 Rust 的語法語義很好地吸收和融合了函數語言程式設計中型別系統相關的概念,比如抽象代數型別 ADT 等,並且 Rust 中並無“繼承”等相關概念,如果不能很好地理解甚至連其他語言中稀鬆平常的結構定義在 Rust 中可能都需要花費不少時間,比如如下的 Python 程式碼可能在 Rust 中的定義是這個樣子的。

  • Python

```python from dataclasses import dataclass

class KCLObject: pass

@dataclass class KCLIntObject(KCLObject): value: int

@dataclass class KCLFloatObject(KCLObject): value: float ```

  • Rust

rust enum KCLObject { Int(u64), Float(f64), }

當然更多的時間是在與 Rust 編譯器本身的報錯作鬥爭,Rust 編譯器會經常使開發人員"碰壁",比如借用檢查報錯等,特別是對於編譯器來講,它處理的核心結構是抽象語法樹 AST,這是一個遞迴和巢狀的樹結構,在 Rust 中有時很難兼顧變數可變性與借用檢查的關係,就如 KCL 編譯器作用域 Scope 的結構定義結構那樣,對於存在迴圈引用的場景,用於需要顯示意識到資料的相互依賴關係,而大量使用 Rc, RefCellWeak 等 Rust 中常用的智慧指標結構。

```rust /// A Scope maintains a set of objects and links to its containing /// (parent) and contained (children) scopes. Objects may be inserted /// and looked up by name. The zero value for Scope is a ready-to-use /// empty scope.

[derive(Clone, Debug)]

pub struct Scope { /// The parent scope. pub parent: Option>>, /// The child scope list. pub children: Vec>>, /// The scope object mapping with its name. pub elems: IndexMap>>, /// The scope start position. pub start: Position, /// The scope end position. pub end: Position, /// The scope kind. pub kind: ScopeKind, } ```

開發效率

Rust 的開發效率可以用先抑後揚來形容。在剛開始上手寫專案時,如果團隊成員沒有接觸過函數語言程式設計相關概念以及相關的程式設計習慣,開發速度將顯著慢於 Python、Go 和 Java 等語言,不過一旦開始熟悉 Rust 標準庫常用的方法、最佳實踐以及常見 Rust 編譯器報錯修改,開發效率將大幅提升,並且原生就能寫出高質量、安全、高效的程式碼。

比如筆者個人當初遇到一個如下程式碼所示的與生命週期錯誤前前後後排查了很久的時間才發現原來是忘記標註生命引數導致生命週期不匹配。此外 Rust 的生命週期與型別系統、作用域、所有權、借用檢查等概念耦合在一起,導致了較高的理解成本和複雜度,且報錯資訊往往不像型別錯誤那麼明顯,生命週期不匹配錯誤報錯資訊有時也略顯呆板,可能會導致較高的排錯成本,當然熟悉相關概念寫多了之後效率會提高不少。

rust struct Data<'a> { b: &'a u8, } // func1 和 func2 一個省略了生命週期引數,一個沒有省略 // 對於 func2 的生命週期會由編譯器預設推導為 '_,可能導致生命週期不匹配錯誤 impl<'a> Data<'a> { fn func1(&self) -> Data<'a> {Data { b: &0 }} fn func2(&self) -> Data {Data { b: &0 }} }

使用 Rust 重寫收益比

經過團隊幾個人花費幾個月時間使用 Rust 完全重寫並穩定落地生產環境幾個月後,回顧整個過程感覺這件事情的收穫非常大,從技術角度層面來看,重寫的過程不僅僅鍛鍊了快速學習一門新的程式語言、程式設計知識並將其付諸實踐,並且整個重寫過程讓我們又反思了 KCL 編譯器中設計不合理的部分並進行修改,對一個程式語言而言,這是一個長週期的專案,我們收穫的是編譯器系統更加穩定、安全,且程式碼清晰,bug 更少、效能更好的技術產品服務於使用者,雖然沒有全部模組得到高達 40 倍的效能,因為部分模組如 KCL 執行時的效能瓶頸在於記憶體深拷貝操作,但筆者個人認為仍然是值得的。且當 Rust 使用時間到達一定時長後,心智和開發效率不再是限制因素,就像學車那樣,拿到駕照後更多是上路實踐和總結。

結語

筆者個人覺得使用 Rust 重寫專案後最重要的是不是我學會了一門新的程式語言,也不是 Rust 很流行很火因此我們在專案中採用一下,或者使用 Rust 編寫了多少炫技的程式碼,是真真正正地使得語言和編譯器本身更加穩定,能夠在生產環境平穩落地並長期使用,啟動速度和自動化效率不再受困擾,效能優於社群其他同類型領域程式語言,使我們語言和工具的使用者感受到體驗提升,這些都得益於 Rust 的無 GC、高效能、更好的錯誤處理記憶體管理、零抽象等特性。總之作為使用者,他們才是最大的受益者。

最後,如果大家喜歡 KCL 語言這個專案,或想使用體驗 KCL 用於自己的場景,或想使用 Rust 語言參與一個開源專案,歡迎大家訪問 https://github.com/KusionStack/community 加入我們的社群一起參與討論和共建 👏👏👏。

參考

  • https://github.com/KusionStack/KCLVM
  • https://github.com/Peefy/StackMachine
  • https://doc.rust-lang.org/book/
  • https://github.com/sunface/rust-course
  • https://www.influxdata.com/blog/rust-can-be-difficult-to-learn-and-frustrating-but-its-also-the-most-exciting-thing-in-software-development-in-a-long-time/