本文整理自 CloudWeGo 開源一周年技術(shù)沙龍活動中字節(jié)跳動基礎(chǔ)架構(gòu)服務(wù)框架資深研發(fā)工程師吳迪的演講分享,技術(shù)沙龍主題為《字節(jié)高性能開源微服務(wù)框架:CloudWeGo》。
本文將從以下三個方面介紹 CloudWeGo 開源的國內(nèi)首個 Rust RPC 框架 Volo:
- CloudWeGo 選擇 Rust 語言進行探索的原因;
- 創(chuàng)建 RPC 框架 Volo 的原因;
- Rust 語言和 Go 語言如何選擇。??
1. CloudWeGo 選擇 Rust 語言進行探索的原因
CloudWeGo 正式官宣新一代 Rust RPC 框架 Volo 開源!很多朋友會有疑問,CloudWeGo 為什么會選擇 Rust 這門語言進行探索呢?本文首先介紹一下其中的原因。
Volo 開源官宣:https://mp.weixin.qq.com/s/XcceLyKxWOVtoMIJBuwXWQ
1.1 Go 的代價
深度優(yōu)化困難
Volo 早期的團隊成員來自于 Kitex 項目(CloudWeGo 開源的 Golang 微服務(wù) RPC 框架)。當(dāng)時我們投入了大量的時間和精力優(yōu)化 Kitex 以及其他相關(guān)基礎(chǔ)庫的性能,最終卻發(fā)現(xiàn)實現(xiàn) Go 的深度優(yōu)化有些困難。我們僅僅可以做一些算法層面和實現(xiàn)層面的優(yōu)化,如果想往下繼續(xù)做其他層面的優(yōu)化,比如指令層面的優(yōu)化,是很難以低成本的方式實現(xiàn)的。而且在大多數(shù)情況下很多優(yōu)化是要和 runtime 以及編譯器作斗爭的。
工具鏈和包管理不夠成熟
例如,使用 Kitex 框架時需要先使用對應(yīng)的 Kitex 工具生成代碼,才能正常編譯使用。雖然這種情況可能在 Frugal 工具成熟之后有所改善,但是在 IDL 有更新的情況下,還是需要使用 Kitex 重新生成對應(yīng)的結(jié)構(gòu)體。這個問題并不是 Kitex 的問題,而是 Go 語言本身的問題,Go 語言在編譯時沒有提供類似的能力。
抽象能力較弱
Go 語言的抽象能力是比較弱的,而且 Go 語言里面的抽象并不是零成本抽象,而是有代價的抽象。
那么使用 Go 語言需要付出的三個代價具體應(yīng)該如何理解呢?下面進行具體分析。
1.1.1 深度優(yōu)化困難
如圖所示,這是 Kitex 項目生成代碼的簡單示例。這兩段代碼的目的是在解析出錯的時候,把一些信息返回給上層。在 Kitex 新版本代碼公開之后,業(yè)務(wù)團隊同學(xué)反映他們線上序列化和反序列化這部分的性能相差了 20%,經(jīng)排查之后,我們發(fā)現(xiàn)了這個改動。
Kitex 新版本的代碼
Kitex 舊版本的代碼
這個改動的本意是希望能給客戶提供更多錯誤上下文的信息。但是它帶來了什么問題呢?如下圖,它把匯編代碼直接一對一地生成到主流程之中,也就是說 Go 語言的編譯器會逐行逐句地進行翻譯,并且不會做重排。
那么這會帶來什么問題呢?由于我們主流程中的代碼與正常流程相比變多了,所以我們重點關(guān)注一下 L1-icache-load-misses? 這一行,新版本的代碼比舊版本的代碼在 L1 指令 cache? 層面 cache-misses 高出 20%,這也就是我們的代碼效率降低 20% 的原因。那么我們是如何解決這個問題的呢?
我們的解決方案如下圖所示。在 err != nil? 的情況下,直接手動加一條 goto? 語句,把所有錯誤處理這部分的代碼放到函數(shù)末尾,即 return? 之后。這相當(dāng)于在編譯器沒有實現(xiàn)指令重排的情況下,用人工方式做一次指令重排。最后優(yōu)化的效果是非常明顯的,可以看到 cache-misses 比之前的那一次還要降低 25%。
上述例子只是使用 Go 語言時在做深度優(yōu)化方面遇到的難題。在抽象能力方面,使用 Go 語言也會遇到一些困難。
1.1.2 零成本抽象(Zero-Cost Abstraction)
什么是零成本抽象呢?使用 C++ 和 Rust 的同學(xué)對這個概念可能有所了解。零成本抽象是指我們不需要對沒有使用的功能付出編譯和運行的開銷,也就是用戶不需要給沒有使用的東西付費,對應(yīng)地,如果用戶對于已經(jīng)使用的東西也沒有再繼續(xù)優(yōu)化的空間,因為它已經(jīng)默認提供了最佳實踐。總結(jié)如下:
- 不用的東西,不需要為之付出代價;
- 用到的東西,你也不可能做得更好。
那么為什么說 Go 語言里面沒有零成本抽象呢?以 Thrift 編解碼為例,我們最開始使用的是 Apache Thrift,它為了支持多種不同 Protocol、Transport 組合,抽象出了 TProtocol Interface、TTransport Interface,但 Kitex 直接依賴具體的 BinaryProtocal 的實現(xiàn)(struct)??梢栽囅?,Apache Thrift 這么做的代價是什么呢?這就是 Go 里面 Interface 帶來的代價。
Go 里面 Interface 是動態(tài)分發(fā)的,也就是運行時通過類型元數(shù)據(jù)和指針去動態(tài)調(diào)用所需方法,它會在運行時多做一次內(nèi)存尋址。但這并不是最關(guān)鍵的,最關(guān)鍵的是它會使得編譯器沒有辦法 inline 以及沒有辦法做很多優(yōu)化。一般比較注重性能的語言都會同時提供靜態(tài)分發(fā)和動態(tài)分發(fā)兩種方式的抽象能力,但是 Go 語言只提供了 Interface 動態(tài)分發(fā)能力,也就可以理解為在 Go 語言中抽象和性能是不可兼得的,這也就是 Go 語言抽象能力比較弱的原因。
1.2 Sonic
Sonic 是 CloudWeGo 開源的一個 JSON 庫,這個庫有很多 CloudWeGo 的用戶都使用過。最初這個庫組成部分如下圖所示,有 2/3 的代碼都是 Assembly 匯編。
在 Sonic 庫中僅有的 27% 的 Go 源代碼如下圖所示。雖然它被統(tǒng)計到了 Go 代碼中,但實際上是匯編代碼。所以我們可以總結(jié)出,世界上最快的 Go 語言程序大概就是用匯編代碼寫就的。
1.3 性能最好的 Go JSON 庫
盡管 Sonic 里面采用了各種黑科技,甚至有 2/3 的代碼都是經(jīng)過人工精調(diào)的匯編代碼,但是 Sonic 的綜合性能還是不如 Rust 最通用的 Serde JSON 庫。如圖所示,綠色柱狀圖代表 Serde JSON 庫,藍色柱狀圖代表 Sonic 庫。根據(jù)這個 Benchmark,即使是和 C、C++ 的庫相比,用 Rust 語言編寫的這個庫在各方面綜合表現(xiàn)也是最佳的。
試想,又有多少 Go 組件能夠得到如此大量的人力投入從而進行深度優(yōu)化呢?這只是一個例子,其實我們之前在 Kitex 中的很多優(yōu)化也是要和編譯器以及 runtime 作斗爭的。因此我們認識到在 Go 語言中想做深度優(yōu)化是非常困難的。
1.4 關(guān)于 Rust
我們?yōu)槭裁匆x擇 Rust 這門語言呢?在解答這個問題之前,要先了解這門語言。所以先介紹一下 Rust 語言的發(fā)展歷史。
1.4.1 Rust 歷史
Rust 語言由 Graydon Hoare 私人研發(fā),他是 Mozilla 做編程語言的工程師,專門給語言開發(fā)編譯器和工具集。當(dāng)時 Mozilla 要開發(fā) Servo 引擎,想要保證安全的同時又能擁有高性能,于是就選擇了 Rust 語言。2010 - 2015 年期間,Rust 是有 GC 的,后來社區(qū)一致表示支持 Rust 必須要有高性能,所以 GC 被取締。2015 年,Rust 發(fā)布 1.0 版本,這也表示正式官宣 Rust 的穩(wěn)定性。
Rust 是以三年為單位進行社區(qū)規(guī)劃和迭代的。2015 - 2018 年,Rust 達成了生產(chǎn)力的承諾,也就是它的工具文檔還有編譯器變得更加智能,也對開發(fā)者更加友好了。2018 - 2021 年,Rust 做了更多異步生態(tài)的完善。之前的 Rust 是沒有異步生態(tài)的,但是自 2018 年開始,它正式引入了異步功能。
1.4.2 Rust 2024
2021 - 2024 年,Rust 有一個 2024 規(guī)劃,主題叫做 Scaling Enpowerment(擴展授權(quán))。之所以取這個名字,是因為 Rust 有一個目標(biāo)——“empower everyone to build reliable and efficient software”。Rust 最關(guān)注也是大家經(jīng)常詬病的一點,就是 Rust 的整個學(xué)習(xí)曲線非常陡峭,所以在這個規(guī)劃中寫道 “Flatten the learning curve”。
1.4.3 Rust 三大優(yōu)勢
在 2022 年,很多開源項目已經(jīng)呈現(xiàn)爆炸式增長。我們了解到 Rust 這門語言后,發(fā)現(xiàn)它有三大非常重要的優(yōu)勢:第一是高性能;第二是很強的安全性;第三是協(xié)作方便。因此我們想嘗試在服務(wù)端使用 Rust 語言開發(fā)微服務(wù),以此解決我們面臨的一些性能上的問題。
性能
很多用戶都對性能有很高的要求,也想知道 Rust 的性能如何。下圖是各語言的 Benchmark 對比結(jié)果,可以看出 Rust 的性能是非常優(yōu)秀的,遠超過 Go 語言,甚至比 C++ 的性能更好。
當(dāng)然我們要著重說明,這個 Benchmark 要求所有語言必須使用相同的算法,并且不得經(jīng)過額外優(yōu)化。畢竟如果都用匯編代碼寫,其實各語言性能相差無幾。但是在真正的開發(fā)過程中,又有多少代碼能夠經(jīng)過那么大量的人工精細優(yōu)化呢?另外,有人可能會對 Rust 的性能比 C 和 C++ 更優(yōu)秀產(chǎn)生質(zhì)疑,其實這也是因為 Rust 對于程序員的輸入要求得更加嚴(yán)格,所以編譯器可以做更進一步的優(yōu)化。
安全性
因為在 Rust 語言的安全性方面可查閱到大量資料,因此不再過多贅述。只闡述一個重要結(jié)論:Rust 1.0 之后,在非 Unsafe 代碼中是不可能出現(xiàn)內(nèi)存安全問題的。這個結(jié)論是通過數(shù)學(xué)證明過的,因此非常可靠。我們應(yīng)該如何理解這個結(jié)論呢?可以從它的推論入手,即:一切內(nèi)存 / 并發(fā)安全問題,都是 unsafe 代碼導(dǎo)致的。也就是如果真的出現(xiàn)安全問題,我們可以限制在一個非常小的范圍內(nèi)進行排查。因為畢竟絕大多數(shù)的 Rust 語言代碼都是 Safe Rust,而不是 Unsafe Rust。
協(xié)作
Rust 是一門真正通過工程實踐形成的語言,它有非常智能的編譯器、完善的文檔、集群的工具鏈和成熟的包管理,因此 Rust 非常適合協(xié)作。我們在使用時可以專注于邏輯功能的實現(xiàn),而不用擔(dān)心內(nèi)存安全和并發(fā)安全的問題等等。還有非常重要的一點就是可以限制別人的代碼,因為如果別人的代碼有內(nèi)存安全問題或并發(fā)安全問題,將無法進行編譯。所以在做 Code Review 時,我們只需關(guān)注邏輯上的功能正確性就可以,因為只要能夠通過編譯提交上來的代碼,安全性是不必擔(dān)心的。這雖然是 Rust 語言的優(yōu)點,但也給使用者帶來一些不便之處。我們常聽說 Rust 開發(fā)者很難,也正是因為編譯。
1.4.4 Rust 的影響力
如下圖,Rust 已經(jīng)連續(xù)七年位居 Stack Overflow 最受開發(fā)者喜愛的編程語言榜榜首。此外,有一個非常重量級的項目叫做 “Rust for Linux”,除了 C 語言之外,Rust 是 Linux 內(nèi)核迄今為止接受的唯一語言。這些成績足以看出 Rust 在開源業(yè)界的重量級和影響力。
2. 創(chuàng)建 RPC 框架 Volo 的原因
明確了 CloudWeGo 選擇 Rust 語言的原因以及 Rust 的優(yōu)勢,我也闡述一下創(chuàng)造 Volo 框架的原因以及 Volo 的特點。
2.1 生態(tài)現(xiàn)狀
創(chuàng)造 Volo 框架與當(dāng)時的生態(tài)情況是有關(guān)的。我們當(dāng)時調(diào)研過整個社區(qū)的生態(tài),發(fā)現(xiàn)沒有生產(chǎn)可用的 Async Thrift 實現(xiàn)。哪怕是社區(qū)中最成熟的 Tonic 框架,它的服務(wù)治理功能也是比較弱的,而且易用性也不夠強。更重要的是當(dāng)時在 Rust 語言社區(qū),還沒有基于 Generic Associated Type(GAT,Rust 語言最新的?個重量級 Feature)和 Type Alias Impl Trait(TAIT,另?個重量級 Feature)的易用性強的抽象。
2.2 易用性
為什么單獨說明 GAT 和 TAIT 這兩個特性呢?按照 Rust 官方團隊的說法,這是自 Rust 1.0 以來語言層面和 Type System 層面最大的變化。舉例簡單說明,下圖是一個現(xiàn)有的社區(qū)方案,代碼是沒有使用 GAT 和 TAIT 的超時中間件的編寫,我們可以發(fā)現(xiàn)如果要保證性能不受損耗,需要編寫大量代碼。
而在 Volo 框架中,因為采用了 GAT 和 TAIT 這兩個特性,編寫代碼如下圖所示。我們可以明顯對比出代碼量和易用性方面的差距是非常明顯的。Rust 以難學(xué)難用而聞名,我們希望盡可能地降低用戶使用 Volo 框架和 Rust 語言編寫微服務(wù)的難度,提供給用戶最符合人體工程學(xué)和直覺的編碼體驗,因此我們把框架易用性作為重要目標(biāo)之一。只有讓大家真正地使用 Volo,Volo 才能體現(xiàn)它的價值。所以 Volo 框架基于 GAT 和 TAIT 特性,大大提升了用戶編寫中間件的便利程度。
除此之外,我們提供了 Volo 命令行工具生成默認 Layout,并且 Volo 的命令行工具提供 IDL 管理的能力,這在業(yè)界是首例。我們還提供了過程宏等能夠再度降低 Service 編寫難度的功能。當(dāng)然還有很多其他的精心設(shè)計,比如很多 API 都是盡量以最符合人體工程學(xué)的方式給出的,也可以避免誤用。
2.3 擴展性
基于 Service 的抽象
受益于 Rust 強大的表達和抽象能力,開發(fā)者可以基于非常靈活的 Service 抽象,用統(tǒng)一的形式對 RPC 的元信息請求和響應(yīng)做一些處理,比如服務(wù)發(fā)現(xiàn)、負載均衡等服務(wù)治理功能都是直接實現(xiàn) Service 即可。
基于RPC元信息的控制
另外,在我們的框架設(shè)計中,所有框架行為都是受到 RPC 元信息控制的。因此我們只要在 Service 中對 RPC 元信息進行修改,就能直接控制框架的行為,從而實現(xiàn)所需的功能。
下圖是 Volo 自帶的負載均衡中間件實現(xiàn)中最關(guān)鍵的一部分,即紅色線框圈出的代碼。只要把 Load Balance 選出來的地址放到 RPC 元信息中就可以,其他代碼可以直接忽視掉。
2.4 性能
如果過多談?wù)摽蚣艿男阅軐Ρ?,容易引?zhàn)。但是基于 Rust 語言的性能優(yōu)勢以及 CloudWeGo 團隊對于極致性能的追求,我們可以預(yù)想到 Volo 的性能也是非常高的。
如果把 Volo 和 Kitex 進行跨語言的對比也是不太公平的,但是因為很多用戶都關(guān)注性能數(shù)據(jù),為了讓使用者對 Volo 框架的性能有大致的了解,我們只給出比較簡單的性能數(shù)據(jù)。在與 Kitex 相同的測試條件(限制 4C)下,Volo 極限 QPS 為 35W。同時,我們內(nèi)部正在驗證基于 Monoio(CloudWeGo 開源的 Rust Async Runtime)的版本,極限 QPS 可以達到 44W。
當(dāng)然還有很多其他的性能指標(biāo),比如響應(yīng)時間也是非常影響用戶體驗的。所以除了 Benchmark,我們選取了由 Go 遷移到 Volo 框架的兩個業(yè)務(wù),呈現(xiàn)真實的業(yè)務(wù)落地收益。
業(yè)務(wù) A(Proxy 類) 。A 業(yè)務(wù)的 IO 比較多,遷移到 Volo 框架后的各方面數(shù)據(jù)如下:
- CPU Usage 630% -> 380%
- MEM 9GB -> 2GB
- P99 150-200ms -> 20-35ms
- AVG 4-5ms -> 1.5ms
可以看出不論是 CPU、內(nèi)存還是延時的指標(biāo),都有非常明顯的提升。下圖中間紅線代表 Volo 上線的時間,也就是紅線左側(cè)這一部分是 Go 的指標(biāo),紅線右側(cè)是 Rust 的指標(biāo),左右對比可以更直觀看出 Volo 框架給業(yè)務(wù) A 帶來的收益。
業(yè)務(wù) B(有大量業(yè)務(wù)邏輯) 。業(yè)務(wù) B 是一個計算密集型的業(yè)務(wù),使用 Volo 框架后 CPU 400% -> 130%。因此在計算密集型的業(yè)務(wù)中,CPU 的提升更加明顯。
2.5 相關(guān)生態(tài)
隨著 Volo 框架開源,一起開源的所有生態(tài)如下:
- Volo 是 RPC 框架的名字,包含了 Volo-Thrift 和 Volo-gRPC 兩部分。
- Volo-rs 組織:Volo 的相關(guān)生態(tài)。
- Pilota:Volo 使用的 Thrift 與 Protobuf 編譯器及編解碼的純 Rust 實現(xiàn)(不依賴 protoc)。
- Motore:Volo 參考 Tower 設(shè)計的,使用了 GAT 和 TAIT 的 middleware 抽象層。
- Metainfo:Volo 用于進行元信息透傳的組件,定義了一套元信息透傳的標(biāo)準(zhǔn)。
全景圖如下:
2.6 倉庫地址
以下是所有相關(guān)生態(tài)的倉庫地址。歡迎大家來提 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 框架后,關(guān)于 Rust 語言和 Go 如何選擇的問題,我有一些主觀的建議和想法。
3.1 和 C++、Go 對比
如果 Go 的服務(wù)想用另一種語言重寫,目前還是 Rust 語言和 C++ 可選性高一些,因此我將這三種語言進行對比,以期為面臨選擇編程語言的用戶提供一些參考。
在學(xué)習(xí)難度方面,Rust 語言和 C++ 學(xué)習(xí)難度比較高,而 Go 語言的學(xué)習(xí)難度比較低。
在性能方面,Rust 語言和 C++ 的性能比較高。我給 Go 語言的性能評級為中等,畢竟和 Python 這些服務(wù)相比,Go 語言還是要強很多的。
在安全性方面,C++ 的安全性比較低,Go 語言安全性中等,Rust 語言安全性比較高。因為 Go 語言 雖然能夠通過 GC 防住一些內(nèi)存安全的問題,但是它沒有辦法防住類似 Data Race 這種并發(fā)安全的問題,而且大多數(shù)時候這類問題其實很難排查。Rust 能夠做到可防可控,應(yīng)防盡防,只要有內(nèi)存安全問題或并發(fā)安全問題,都無法成功編譯。
在協(xié)作方面,Rust 語言的協(xié)作能力比較高,Go 語言和 C++ 的協(xié)作等級是中等。首先,C++ 沒有官方提供的包管理工具,它必須借助第三方社區(qū)提供的包管理工具,但是不同的項目使用的包管理工具可能是不一樣的,所以這是對用戶來說非常不便的;其次,在開發(fā)者可以保證自己的代碼沒有 Bug、符合最佳實踐的情況下,還是不可避免地會和一些第三方的庫以及比較老舊社區(qū)一流的庫產(chǎn)生交集,并且產(chǎn)生混用的情形;最后,如果涉及到大型項目,需要團隊協(xié)作開發(fā),我們無法保證團隊中其他人寫出的代碼也不存在內(nèi)存安全問題。至于 Go 語言,它的編譯時及工具鏈的能力相對來說比較弱,因此也定級為中等。
在特性和使用成本方面,用戶應(yīng)該都有所了解,不再過多贅述。從使用成本上來講,我的評級為給 C++ 為高使用成本,Go 語言和 Rust 語言的使用成本是中等。C++ 的業(yè)務(wù)上線之后經(jīng)常出狀況,而且排查問題困難是很常見的情況。而使用 Go 語言做一些通用的編程是可以的,但是一旦涉及到定制化的需求在實現(xiàn)上就有一定的困難,比如需要根據(jù)不同的平臺系統(tǒng)做系統(tǒng)級編程,使用 Go 語言做起來就非常麻煩。語言只是工具,我們還是要根據(jù)不同的場景選用更為合適的語言。
那么 Go 語言和 Rust 語言的使用成本為什么是中等呢?因為我們不能只關(guān)注編寫代碼的效率,還要考慮運維和 Debug 的成本。Go 語言可能也會產(chǎn)生 Panic,我們內(nèi)部也經(jīng)常會有一些并發(fā)的問題,然后需要不斷地排查。而 Rust 語言前置了這部分成本,相比于其他語言框架在上線之后測試、保證穩(wěn)定性,我們把這部分的時間精力用在了開發(fā)期間,這樣也避免了線上事故帶來的損失。因此我給 Go 語言和 Rust 語言評定的使用成本是中等。
3.2 Rust & Go
如果將 Rust 語言和 Go 語言單獨做對比,我們應(yīng)該如何解讀它們呢?這是一個非常經(jīng)典的問題??梢試L試從以下四方面考慮:
合作關(guān)系,取長補短
我們團隊認為其實二者并不是對立關(guān)系,而是合作關(guān)系,它們是取長補短的。畢竟語言只是工具,很多時候我們只是需要一個更加得心應(yīng)手的工具而已。
(性能 >> 開發(fā)效率) || (安全性 >> 開發(fā)效率) ->Rust
對于需要極致性能,重計算的應(yīng)用,以及需要穩(wěn)定性并能接受一定開發(fā)速度損失的應(yīng)用,推薦使用 Rust,Rust 在極致性能優(yōu)化和安全性上的優(yōu)勢可以在這類應(yīng)用中得以發(fā)揮。
迭代速度要求高 -> Go
對于性能不敏感的應(yīng)用、重 IO 的應(yīng)用以及需要快速開發(fā)快速迭代勝過穩(wěn)定性的應(yīng)用,推薦使用 Go 語言,這種應(yīng)用使用 Rust 并不會帶來明顯的收益。
考慮團隊技術(shù)儲備和人才儲備
當(dāng)然,還有一個很重要的考慮因素,是團隊現(xiàn)有的技術(shù)棧,即技術(shù)儲備和人才儲備。
4. 小結(jié)
希望以上內(nèi)容能讓大家初步了解 Volo 以及相關(guān)的生態(tài)。目前 Volo 還處于早期發(fā)展階段,歡迎各位感興趣的同學(xué)加入我們,共同建設(shè) CloudWeGo 以及 Rust 開源社區(qū)。我們誠心期待更多開發(fā)者加入,也期待 Volo 能夠助力越來越多的企業(yè)快速構(gòu)建云原生架構(gòu)。