選擇 Go 還是 Rust?CloudWeGo-Volo 基於 Rust 語言的探索實踐

語言: CN / TW / HK

圖片

本文整理自 CloudWeGo 開源一週年技術沙龍活動中位元組跳動基礎架構服務框架資深研發工程師吳迪的演講分享,技術沙龍主題為《位元組高效能開源微服務框架:CloudWeGo》。

本文將從以下三個方面介紹 CloudWeGo 開源的國內首個 Rust RPC 框架 Volo:

  1. CloudWeGo 選擇 Rust 語言進行探索的原因;

  2. 建立 RPC 框架 Volo 的原因;

  3. Rust 語言和 Go 語言如何選擇。

  4. CloudWeGo 選擇 Rust 語言進行探索的原因

1. CloudWeGo 選擇 Rust 語言進行探索的原因

CloudWeGo 正式官宣新一代 Rust RPC 框架 Volo 開源!很多朋友會有疑問,CloudWeGo 為什麼會選擇 Rust 這門語言進行探索呢?本文首先介紹一下其中的原因。

Volo 開源官宣:https://mp.weixin.qq.com/s/XcceLyKxWOVtoMIJBuwXWQ

1.1 Go 的代價

  • 深度優化困難

Volo 早期的團隊成員來自於 Kitex 專案(CloudWeGo 開源的 Golang 微服務 RPC 框架)。當時我們投入了大量的時間和精力優化 Kitex 以及其他相關基礎庫的效能,最終卻發現實現 Go 的深度優化有些困難。我們僅僅可以做一些演算法層面和實現層面的優化,如果想往下繼續做其他層面的優化,比如指令層面的優化,是很難以低成本的方式實現的。而且在大多數情況下很多優化是要和 runtime 以及編譯器作鬥爭的。

  • 工具鏈和包管理不夠成熟

例如,使用 Kitex 框架時需要先使用對應的 Kitex 工具生成程式碼,才能正常編譯使用。雖然這種情況可能在 Frugal 工具成熟之後有所改善,但是在 IDL 有更新的情況下,還是需要使用 Kitex 重新生成對應的結構體。這個問題並不是 Kitex 的問題,而是 Go 語言本身的問題,Go 語言在編譯時沒有提供類似的能力。

  • 抽象能力較弱

Go 語言的抽象能力是比較弱的,而且 Go 語言裡面的抽象並不是零成本抽象,而是有代價的抽象。

那麼使用 Go 語言需要付出的三個代價具體應該如何理解呢?下面進行具體分析。

1.1.1 深度優化困難

如圖所示,這是 Kitex 專案生成程式碼的簡單示例。這兩段程式碼的目的是在解析出錯的時候,把一些資訊返回給上層。在 Kitex 新版本程式碼公開之後,業務團隊同學反映他們線上序列化和反序列化這部分的效能相差了 20%,經排查之後,我們發現了這個改動。

圖片

Kitex 新版本的程式碼

圖片

Kitex 舊版本的程式碼

這個改動的本意是希望能給客戶提供更多錯誤上下文的資訊。但是它帶來了什麼問題呢?如下圖,它把彙編程式碼直接一對一地生成到主流程之中,也就是說 Go 語言的編譯器會逐行逐句地進行翻譯,並且不會做重排。

圖片

那麼這會帶來什麼問題呢?由於我們主流程中的程式碼與正常流程相比變多了,所以我們重點關注一下 L1-icache-load-misses 這一行,新版本的程式碼比舊版本的程式碼在 L1 指令 cache 層面 cache-misses 高出 20%,這也就是我們的程式碼效率降低 20% 的原因。那麼我們是如何解決這個問題的呢?

我們的解決方案如下圖所示。在 err != nil 的情況下,直接手動加一條 goto 語句,把所有錯誤處理這部分的程式碼放到函式末尾,即 return 之後。這相當於在編譯器沒有實現指令重排的情況下,用人工方式做一次指令重排。最後優化的效果是非常明顯的,可以看到 cache-misses 比之前的那一次還要降低 25%。

圖片

上述例子只是使用 Go 語言時在做深度優化方面遇到的難題。在抽象能力方面,使用 Go 語言也會遇到一些困難。

1.1.2 零成本抽象(Zero-Cost Abstraction)

什麼是零成本抽象呢?使用 C++ 和 Rust 的同學對這個概念可能有所瞭解。零成本抽象是指我們不需要對沒有使用的功能付出編譯和執行的開銷,也就是使用者不需要給沒有使用的東西付費,對應地,如果使用者對於已經使用的東西也沒有再繼續優化的空間,因為它已經預設提供了最佳實踐。總結如下:

  • 不用的東西,不需要為之付出代價;
  • 用到的東西,你也不可能做得更好。

那麼為什麼說 Go 語言裡面沒有零成本抽象呢?以 Thrift 編解碼為例,我們最開始使用的是 Apache Thrift,它為了支援多種不同 Protocol、Transport 組合,抽象出了 TProtocol Interface、TTransport Interface,但 Kitex 直接依賴具體的 BinaryProtocal 的實現(struct)。可以試想,Apache Thrift 這麼做的代價是什麼呢?這就是 Go 裡面 Interface 帶來的代價。

Go 裡面 Interface 是動態分發的,也就是執行時通過型別元資料和指標去動態呼叫所需方法,它會在執行時多做一次記憶體定址。但這並不是最關鍵的,最關鍵的是它會使得編譯器沒有辦法 inline 以及沒有辦法做很多優化。一般比較注重效能的語言都會同時提供靜態分發和動態分發兩種方式的抽象能力,但是 Go 語言只提供了 Interface 動態分發能力,也就可以理解為在 Go 語言中抽象和效能是不可兼得的,這也就是 Go 語言抽象能力比較弱的原因。

1.2 Sonic

Sonic 是 CloudWeGo 開源的一個 JSON 庫,這個庫有很多 CloudWeGo 的使用者都使用過。最初這個庫組成部分如下圖所示,有 2/3 的程式碼都是 Assembly 彙編。

圖片

在 Sonic 庫中僅有的 27% 的 Go 原始碼如下圖所示。雖然它被統計到了 Go 程式碼中,但實際上是彙編程式碼。所以我們可以總結出,世界上最快的 Go 語言程式大概就是用匯編程式碼寫就的。

圖片

1.3 效能最好的 Go JSON 庫

儘管 Sonic 裡面採用了各種黑科技,甚至有 2/3 的程式碼都是經過人工精調的彙編程式碼,但是 Sonic 的綜合性能還是不如 Rust 最通用的 Serde JSON 庫。如圖所示,綠色柱狀圖代表 Serde JSON 庫,藍色柱狀圖代表 Sonic 庫。根據這個 Benchmark,即使是和 C、C++ 的庫相比,用 Rust 語言編寫的這個庫在各方面綜合表現也是最佳的。

試想,又有多少 Go 元件能夠得到如此大量的人力投入從而進行深度優化呢?這只是一個例子,其實我們之前在 Kitex 中的很多優化也是要和編譯器以及 runtime 作鬥爭的。因此我們認識到在 Go 語言中想做深度優化是非常困難的。

圖片

1.4 關於 Rust

我們為什麼要選擇 Rust 這門語言呢?在解答這個問題之前,要先了解這門語言。所以先介紹一下 Rust 語言的發展歷史。

1.4.1 Rust 歷史

Rust 語言由 Graydon Hoare 私人研發,他是 Mozilla 做程式語言的工程師,專門給語言開發編譯器和工具集。當時 Mozilla 要開發 Servo 引擎,想要保證安全的同時又能擁有高效能,於是就選擇了 Rust 語言。2010 - 2015 年期間,Rust 是有 GC 的,後來社群一致表示支援 Rust 必須要有高效能,所以 GC 被取締。2015 年,Rust 釋出 1.0 版本,這也表示正式官宣 Rust 的穩定性。

Rust 是以三年為單位進行社群規劃和迭代的。2015 - 2018 年,Rust 達成了生產力的承諾,也就是它的工具文件還有編譯器變得更加智慧,也對開發者更加友好了。2018 - 2021 年,Rust 做了更多非同步生態的完善。之前的 Rust 是沒有非同步生態的,但是自 2018 年開始,它正式引入了非同步功能。

圖片

1.4.2 Rust 2024

2021 - 2024 年,Rust 有一個 2024 規劃,主題叫做 Scaling Enpowerment(擴充套件授權)。之所以取這個名字,是因為 Rust 有一個目標——“empower everyone to build reliable and efficient software”。Rust 最關注也是大家經常詬病的一點,就是 Rust 的整個學習曲線非常陡峭,所以在這個規劃中寫道 “Flatten the learning curve”。

圖片

1.4.3 Rust 三大優勢

在 2022 年,很多開源專案已經呈現爆炸式增長。我們瞭解到 Rust 這門語言後,發現它有三大非常重要的優勢:第一是高效能;第二是很強的安全性;第三是協作方便。因此我們想嘗試在服務端使用 Rust 語言開發微服務,以此解決我們面臨的一些效能上的問題。

  • 效能

很多使用者都對效能有很高的要求,也想知道 Rust 的效能如何。下圖是各語言的 Benchmark 對比結果,可以看出 Rust 的效能是非常優秀的,遠超過 Go 語言,甚至比 C++ 的效能更好。

當然我們要著重說明,這個 Benchmark 要求所有語言必須使用相同的演算法,並且不得經過額外優化。畢竟如果都用匯編程式碼寫,其實各語言效能相差無幾。但是在真正的開發過程中,又有多少程式碼能夠經過那麼大量的人工精細優化呢?另外,有人可能會對 Rust 的效能比 C 和 C++ 更優秀產生質疑,其實這也是因為 Rust 對於程式設計師的輸入要求得更加嚴格,所以編譯器可以做更進一步的優化。

圖片

  • 安全性

因為在 Rust 語言的安全性方面可查閱到大量資料,因此不再過多贅述。只闡述一個重要結論:Rust 1.0 之後,在非 Unsafe 程式碼中是不可能出現記憶體安全問題的。這個結論是通過數學證明過的,因此非常可靠。我們應該如何理解這個結論呢?可以從它的推論入手,即:一切記憶體 / 併發安全問題,都是 unsafe 程式碼導致的。也就是如果真的出現安全問題,我們可以限制在一個非常小的範圍內進行排查。因為畢竟絕大多數的 Rust 語言程式碼都是 Safe Rust,而不是 Unsafe Rust。

  • 協作

Rust 是一門真正通過工程實踐形成的語言,它有非常智慧的編譯器完善的文件叢集的工具鏈成熟的包管理,因此 Rust 非常適合協作。我們在使用時可以專注於邏輯功能的實現,而不用擔心記憶體安全和併發安全的問題等等。還有非常重要的一點就是可以限制別人的程式碼,因為如果別人的程式碼有記憶體安全問題或併發安全問題,將無法進行編譯。所以在做 Code Review 時,我們只需關注邏輯上的功能正確性就可以,因為只要能夠通過編譯提交上來的程式碼,安全性是不必擔心的。這雖然是 Rust 語言的優點,但也給使用者帶來一些不便之處。我們常聽說 Rust 開發者很難,也正是因為編譯。

1.4.4 Rust 的影響力

如下圖,Rust 已經連續七年位居 Stack Overflow 最受開發者喜愛的程式語言榜榜首。此外,有一個非常重量級的專案叫做 “Rust for Linux”,除了 C 語言之外,Rust 是 Linux 核心迄今為止接受的唯一語言。這些成績足以看出 Rust 在開源業界的重量級和影響力。

圖片

2. 建立 RPC 框架 Volo 的原因

明確了 CloudWeGo 選擇 Rust 語言的原因以及 Rust 的優勢,我也闡述一下創造 Volo 框架的原因以及 Volo 的特點。

2.1 生態現狀

創造 Volo 框架與當時的生態情況是有關的。我們當時調研過整個社群的生態,發現沒有生產可用的 Async Thrift 實現。哪怕是社群中最成熟的 Tonic 框架,它的服務治理功能也是比較弱的,而且易用性也不夠強。更重要的是當時在 Rust 語言社群,還沒有基於 Generic Associated Type(GAT,Rust 語言最新的⼀個重量級 Feature)和 Type Alias Impl Trait(TAIT,另⼀個重量級 Feature)的易用性強的抽象。

2.2 易用性

為什麼單獨說明 GAT 和 TAIT 這兩個特性呢?按照 Rust 官方團隊的說法,這是自 Rust 1.0 以來語言層面和 Type System 層面最大的變化。舉例簡單說明,下圖是一個現有的社群方案,程式碼是沒有使用 GAT 和 TAIT 的超時中介軟體的編寫,我們可以發現如果要保證效能不受損耗,需要編寫大量程式碼。

圖片

而在 Volo 框架中,因為採用了 GAT 和 TAIT 這兩個特性,編寫程式碼如下圖所示。我們可以明顯對比出程式碼量和易用性方面的差距是非常明顯的。Rust 以難學難用而聞名,我們希望儘可能地降低使用者使用 Volo 框架和 Rust 語言編寫微服務的難度,提供給使用者最符合人體工程學和直覺的編碼體驗,因此我們把框架易用性作為重要目標之一。只有讓大家真正地使用 Volo,Volo 才能體現它的價值。所以 Volo 框架基於 GAT 和 TAIT 特性,大大提升了使用者編寫中介軟體的便利程度

圖片

除此之外,我們提供了 Volo 命令列工具生成預設 Layout,並且 Volo 的命令列工具提供 IDL 管理的能力,這在業界是首例。我們還提供了過程巨集等能夠再度降低 Service 編寫難度的功能。當然還有很多其他的精心設計,比如很多 API 都是儘量以最符合人體工程學的方式給出的,也可以避免誤用。

2.3 擴充套件性

  • 基於 Service 的抽象

受益於 Rust 強大的表達和抽象能力,開發者可以基於非常靈活的 Service 抽象,用統一的形式對 RPC 的元資訊請求和響應做一些處理,比如服務發現、負載均衡等服務治理功能都是直接實現 Service 即可。

圖片

  • 基於 RPC 元資訊 的控制

另外,在我們的框架設計中,所有框架行為都是受到 RPC 元資訊控制的。因此我們只要在 Service 中對 RPC 元資訊進行修改,就能直接控制框架的行為,從而實現所需的功能。

下圖是 Volo 自帶的負載均衡中介軟體實現中最關鍵的一部分,即紅色線框圈出的程式碼。只要把 Load Balance 選出來的地址放到 RPC 元資訊中就可以,其他程式碼可以直接忽視掉。

圖片

2.4 效能

如果過多談論框架的效能對比,容易引戰。但是基於 Rust 語言的效能優勢以及 CloudWeGo 團隊對於極致效能的追求,我們可以預想到 Volo 的效能也是非常高的。

如果把 Volo 和 Kitex 進行跨語言的對比也是不太公平的,但是因為很多使用者都關注效能資料,為了讓使用者對 Volo 框架的效能有大致的瞭解,我們只給出比較簡單的效能資料。在與 Kitex 相同的測試條件(限制 4C)下,Volo 極限 QPS 為 35W。同時,我們內部正在驗證基於 Monoio(CloudWeGo 開源的 Rust Async Runtime)的版本,極限 QPS 可以達到 44W。

當然還有很多其他的效能指標,比如響應時間也是非常影響使用者體驗的。所以除了 Benchmark,我們選取了由 Go 遷移到 Volo 框架的兩個業務,呈現真實的業務落地收益。

業務 A(Proxy 類)  。A 業務的 IO 比較多,遷移到 Volo 框架後的各方面資料如下:

  • CPU Usage 630% -> 380%
  • MEM 9GB -> 2GB
  • P99 150-200ms -> 20-35ms
  • AVG 4-5ms -> 1.5ms

可以看出不論是 CPU、記憶體還是延時的指標,都有非常明顯的提升。下圖中間紅線代表 Volo 上線的時間,也就是紅線左側這一部分是 Go 的指標,紅線右側是 Rust 的指標,左右對比可以更直觀看出 Volo 框架給業務 A 帶來的收益。

圖片

業務 B(有大量業務邏輯)  。業務 B 是一個計算密集型的業務,使用 Volo 框架後 CPU 400% -> 130%。因此在計算密集型的業務中,CPU 的提升更加明顯。

2.5 相關生態

隨著 Volo 框架開源,一起開源的所有生態如下:

  • Volo 是 RPC 框架的名字,包含了 Volo-Thrift 和 Volo-gRPC 兩部分。
  • Volo-rs 組織:Volo 的相關生態。
  • Pilota:Volo 使用的 Thrift 與 Protobuf 編譯器及編解碼的純 Rust 實現(不依賴 protoc)。
  • Motore:Volo 參考 Tower 設計的,使用了 GAT 和 TAIT 的 middleware 抽象層。
  • Metainfo:Volo 用於進行元資訊透傳的元件,定義了一套元資訊透傳的標準。

全景圖如下:

圖片

2.6 倉庫地址

以下是所有相關生態的倉庫地址。歡迎大家來提 Issue 或 PR,一起共建 Volo!

  • Volo:https://github.com/cloudwego/volo
  • Volo-rs:https://github.com/volo-rs
  • Pilota:https://github.com/cloudwego/pilota
  • Motore:https://github.com/cloudwego/motore
  • Metainfo:https://github.com/cloudwego/metainfo

3. Rust 語言和 Go 語言如何選擇

瞭解 Volo 框架後,關於 Rust 語言和 Go 如何選擇的問題,我有一些主觀的建議和想法。

3.1 和 C++、Go 對比

如果 Go 的服務想用另一種語言重寫,目前還是 Rust 語言和 C++ 可選性高一些,因此我將這三種語言進行對比,以期為面臨選擇程式語言的使用者提供一些參考。

圖片

在學習難度方面,Rust 語言和 C++ 學習難度比較高,而 Go 語言的學習難度比較低。

在效能方面,Rust 語言和 C++ 的效能比較高。我給 Go 語言的效能評級為中等,畢竟和 Python 這些服務相比,Go 語言還是要強很多的。

在安全性方面,C++ 的安全性比較低,Go 語言安全性中等,Rust 語言安全性比較高。因為 Go 語言 雖然能夠通過 GC 防住一些記憶體安全的問題,但是它沒有辦法防住類似 Data Race 這種併發安全的問題,而且大多數時候這類問題其實很難排查。Rust 能夠做到可防可控,應防盡防,只要有記憶體安全問題或併發安全問題,都無法成功編譯。

在協作方面,Rust 語言的協作能力比較高,Go 語言和 C++ 的協作等級是中等。首先,C++ 沒有官方提供的包管理工具,它必須藉助第三方社群提供的包管理工具,但是不同的專案使用的包管理工具可能是不一樣的,所以這是對使用者來說非常不便的;其次,在開發者可以保證自己的程式碼沒有 Bug、符合最佳實踐的情況下,還是不可避免地會和一些第三方的庫以及比較老舊社群一流的庫產生交集,並且產生混用的情形;最後,如果涉及到大型專案,需要團隊協作開發,我們無法保證團隊中其他人寫出的程式碼也不存在記憶體安全問題。至於 Go 語言,它的編譯時及工具鏈的能力相對來說比較弱,因此也定級為中等。

在特性和使用成本方面,使用者應該都有所瞭解,不再過多贅述。從使用成本上來講,我的評級為給 C++ 為高使用成本,Go 語言和 Rust 語言的使用成本是中等。C++ 的業務上線之後經常出狀況,而且排查問題困難是很常見的情況。而使用 Go 語言做一些通用的程式設計是可以的,但是一旦涉及到定製化的需求在實現上就有一定的困難,比如需要根據不同的平臺系統做系統級程式設計,使用 Go 語言做起來就非常麻煩。語言只是工具,我們還是要根據不同的場景選用更為合適的語言。

那麼 Go 語言和 Rust 語言的使用成本為什麼是中等呢?因為我們不能只關注編寫程式碼的效率,還要考慮運維和 Debug 的成本。Go 語言可能也會產生 Panic,我們內部也經常會有一些併發的問題,然後需要不斷地排查。而 Rust 語言前置了這部分成本,相比於其他語言框架在上線之後測試、保證穩定性,我們把這部分的時間精力用在了開發期間,這樣也避免了線上事故帶來的損失。因此我給 Go 語言和 Rust 語言評定的使用成本是中等。

3.2 Rust & Go

如果將 Rust 語言和 Go 語言單獨做對比,我們應該如何解讀它們呢?這是一個非常經典的問題。可以嘗試從以下四方面考慮:

  • 合作關係,取長補短

我們團隊認為其實二者並不是對立關係,而是合作關係,它們是取長補短的。畢竟語言只是工具,很多時候我們只是需要一個更加得心應手的工具而已。

  • (效能 >> 開發效率) || (安全性 >> 開發效率) ->  Rust

對於需要極致效能,重計算的應用,以及需要穩定性並能接受一定開發速度損失的應用,推薦使用 Rust,Rust 在極致效能優化和安全性上的優勢可以在這類應用中得以發揮。

  • 迭代速度要求高 -> Go

對於效能不敏感的應用、重 IO 的應用以及需要快速開發快速迭代勝過穩定性的應用,推薦使用 Go 語言,這種應用使用 Rust 並不會帶來明顯的收益。

  • 考慮團隊技術儲備和人才儲備

當然,還有一個很重要的考慮因素,是團隊現有的技術棧,即技術儲備和人才儲備。

4. 小結

希望以上內容能讓大家初步瞭解 Volo 以及相關的生態。目前 Volo 還處於早期發展階段,歡迎各位感興趣的同學加入我們,共同建設 CloudWeGo 以及 Rust 開源社群。我們誠心期待更多開發者加入,也期待 Volo 能夠助力越來越多的企業快速構建雲原生架構。