我們用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/