TiDB與gRPC的那點事

TiDB與gRPC的那點事

作者|黃東旭

通過對 gRPC 的誕生背景與設計原則的介紹,作者分享了 TiDB 選擇 gRPC 的原因,並介紹了在這個過程中為了適應 TiDB 而對 gRPC 做出的調整與完善。最後,關於性能方面,介紹了調優的思路。

如果有關注 TiDB 的朋友可能注意到,我們在上個月的 RC3 版本中已經完成了將 TiDB 中的 RPC 框架替換成了 gRPC,這個工作其實已經鋪墊了快一年了,如果裝逼一點說的話,其實 gRPC 開源的第一天看了一眼設計和哲學,就決定在 TiDB 中使用它。

今天抽空寫一下背後的一些思考和在這個過程中的一些經驗,本次分享不太會介紹大家怎麼去用 gRPC,可能更加偏向一些為什麼的問題。

gRPC 背景介紹

其實做分佈式系統那麼久,幾乎也是天天和 RPC 打交道,要說 各個模塊是系統的筋肉,那 RPC 就是整個系統的血管,數據的流通,信令的傳遞,都離不開 RPC。

RPC 並不是一個固定的東西,可重可輕,重的如同 MS 的 DCOM,JAVA 的 EJB,輕的 HTTP 也可以說是 RPC,甚至自己寫個 TCP 的文本通信協議也算。

大家也都知道 Google 內部其實沒怎麼用 gRPC,大量使用的是 Stubby,它作為 gRPC 的前身,也是一個 Protobuf RPC 的實現,因為大量依賴了 Google 的其他基礎服務所以不太方便開放出來給社區使用。

隨著 SPDY / QUIC,乃至 HTTP/2 的成熟,Google 決定用這些更加標準的組件來構建一個新的 RPC 框架,也就是 gRPC。不過這個項目過於龐大,而且 Google 內部 Stubby 已經用了超過 10 年,很難直接替換,畢竟程序員最煩的事情之一就是去改跑著好好的老代碼。。。

不過 anyway,儘管 gRPC 沒有在 Google 內部廣泛使用,也是給社區帶來了一個非常好的基礎組件,現在為止包括ETCD / Kubernetes / TiDB在內的大量社區頂級開源分佈式項目都在使用它。

為何選擇 gRPC?

有人說,RPC 多簡單啊,不就是一個長連接,Sender 和 Reciver 來回發包嘛,頂多再搞個服務發現做 Failover,搞得那麼複雜為啥。另外要強大不是已經有 EJB 什麼的嘛,gRPC 的意義何在?我想從官方的 gRPC 的設計動機和原則說起:

1、Google 應該是踐行服務化的先驅之一,在業界沒那麼推崇微服務的時代,Google 就已經大規模的微服務化。

微服務的精髓之一就是服務之間傳遞的是可序列化消息,而不是對象和引用,這個思想是和 DCOM 及 EJB 完全相反的。只有數據,不包含邏輯;這個設計的好處不用我多說也很好理解,參考 CSP 。

2、Protobuf 作為一個良好的序列化方案,注意,只是 序列化(儘管 pb 也有定義 rpc service 的能力,Protobuf 默認生成的代碼並不包含 RPC 的實現),它並不像 Thrift 天生就帶一個 RPC Framework,相對的來說比較輕。

在 gRPC 的設計中,一個很重要的原則就是 Payload agnostic,RPC 框架不應該規定用的是什麼 payload 格式,可以是 Protobuf,JSON,XML,這也讓 gRPC 的設計和層次更加清晰。

3、比傳統的 Request / Response 更豐富的 API Interface,這個是我們使用 gRPC 的重要理由,gRPC 不僅支持傳統的一應一答的模式,更是支持三種 Streaming 的調用方式,現代的業務經常會需要傳輸大的數據流,Streaming API 的設計讓這些業務寫起來輕鬆很多。

4、有了 Streaming 就不可避免地需要引入 Flow-control ,這點 gRPC 的處理很聰明,直接依賴了 HTTP/2,在流控這邊不怎麼用操心,順帶還可以用 HTTP 反向代理做負載均衡。

但是另一方面也會帶來更多的調優複雜度,畢竟和 Web 的使用場景不太一樣,比如默認的 INITIAL_WINDOW_SIZE 在 gRPC 裡是 64k,太小,吞吐上不去,需要人工改大。

5、另一方面由於直接使用了 HTTP/2,TLS 的支持就幾乎是天然的,對於 TiDB 這樣的商業數據庫而言,傳輸層加密是一個很重要的功能,在 gRPC 中直接就可以支持。本著不重新造輪子的原則,直接用 gRPC 就好了。

gRPC-rs 順勢而生

下面是 TiDB 整個項目的架構圖:

TiDB與gRPC的那點事

大家也都知道,TiDB 的底層存儲 (TiKV) 是使用 Rust 開發的,至於為啥用 Rust 我在其他文章裡說的比較多了,也不是今天的重點就不展開了。

當時我們決定採用 gRPC 的時候擺在我們面前的一個很現實的問題是 gRPC 並沒有 Rust 語言的實現,而且另一個更大的問題是,Rust 甚至還沒有 HTTP/2 的實現。

但是呢,不能因為這個原因不用呀,我們公司的做事風格還是擁抱社區,如果沒有社區就自己創造社區。

剛好那個時候我在舊金山,在 Mozilla 總部和 Rust core team 的團隊提到這個事情,後來對方介紹了 Yandex 的一個工程師,也是 Rust proto3 庫的作者,他開了個坑開始實現 Rust HTTP/2 library 和 gRPC 的 pure Rust 實現,應該是 2016 年 9 月前後,一開始我們非常期待啊,也一直在幫助這個庫完善。

後來大概在 2017 年 3 月,整個 rust gRPC 覺得大概可用了,然後 Yandex 這個哥們進度有點慢了,我們於是只好把這個坑接過來自己填,同時往 TiKV 上整合。

大概花了一個多月的時間,完成以後在我們的測試平臺上一測,發現穩定性有很大的問題,經過大概兩個月艱苦的修 Bug 的過程,仍然看不到希望。

而且畢竟不是官方的作品,和主幹 Features 的合併也牽扯了很大精力,雖然也想過把這個項目捐給 gRPC 官方,但是估計 gRPC 官方也沒有人能維護這個項目,所以也還是我們自己維護,最後沒辦法,我們發版本的壓力也很大,只好另想辦法。

大家也都知道 gRPC 的官方主要維護的就三種語言:C / Java / Go,至於 C++ / Python / Ruby 什麼的都是在 C 的 gRPC core 之上進行封裝的,但是沒有 Rust。

TiDB與gRPC的那點事

幸運的是,Rust 對於 C ABI 支持很好,畢竟後端直接就是 llvm ,性能上更沒有什麼損失,直接可以封裝一下得到一個 Rust 的 gRPC 庫。其實現在看看,一開始就應該這樣,在追求純 Rust 實現 gRPC 庫上我們浪費了一些時間,是一個失誤。

在我們官方 Blog 的一篇文章裡,我們描述了我們的 gRPC-rs 的設計:

https://zhuanlan.zhihu.com/p/27995238,

這裡我也不想贅述,總的來說,從最後的完成時間來看,估計也就花了大概 1 人月的時間,而且整個 core 的穩定性也有保證。

值得一提的是在我們的 gRPC-rs 中,並不是簡單地做了一層 gRPC core 的 wrapper 就完事了,我們使用了 futures-rs,將 Futures 引入 RPC 的調用 API 中的一個好處就是很多異步邏輯可以用近似同步的書寫方式(組合子)來寫,程序看起來會更加清晰。

詳細內容就不展開了,有興趣的可以看看

github.com/pingcap/grpc-rs ,

也歡迎參與一起開發。

TiDB與gRPC的那點事

性能調優

在完成 gRPC 庫的 Rust 語言移植後,擺在我們眼前的一個重大的問題就是性能問題,在 gRPC 之前,我們使用的是一個自己寫的很裸的 Protobuf RPC 實現,簡單得不能再簡單,長連接,Protobuf Payload,只有 Req / Resp 模式,但是簡單也有簡單的好處,幾乎沒有太多性能的損失,但是也有簡單的壞處:

之前的實現 scale 起來比較麻煩,用 gRPC 的話 scale 只需要改改 gRPC 線程數就好。最開始直接換成 gRPC 後,延遲性能和吞吐都有 30% 以上的下降,同時觀察到 CPU 的消耗是原來的 200%,然後就開始了調優之路。

其實 gRPC 本身的設計並不差,核心 task 的異步化調用的設計採用了組合子還是蠻巧妙的 :

https://github.com/grpc/grpc/blob/master/doc/combiner-explainer.md,

另外基於 epoll 封裝了一套類似 IOCP 機制,在官方的設計文檔中有很好的解釋

https://github.com/grpc/grpc/blob/master/doc/epoll-polling-engine.md。

但是由於整體依賴了 HTTP/2,所以比裸的 RPC 還是多出了很多工作,主要集中在 HTTP/2 的包處理上,所以我們的性能調優也是集中在 HTTP/2 這邊。

比如,上面提到的用於 HTTP/2 流控的 INITIAL_WINDOW_SIZE ,默認 64k,調大有助於提高吞吐,比如參見社區的這個 issue:

https://github.com/grpc/grpc-go/issues/760

另外 HTTP/2 是單連接的,實際測試發現也制約了吞吐,我們實踐中不管是 TiDB 連接 TiKV 還是 TiKV 之間的連接都是採用多個 gRPC client 的方式來同時建立多個 HTTP/2 連接。

如果你知道自己的 workload 的大小,通過適當的調整 GRPC_WRITE_BUFFER_HINT 改變 write buffer 的大小也能顯著減少 syscall 的調用:

https://github.com/grpc/grpc/issues/9121;

GRPC_ARG_MAX_CONCURRENT_STREAMS規定在一個 HTTP/2 連接中最多存在多少 stream,在 gRPC 中一次 RPC 就是一個 stream。在 TiKV 的應用場景中,適當調高該參數同樣有助於提高吞吐。

還有就是 gRPC 本身不適用於傳送大文件的場景,見 issue:

https://github.com/grpc/grpc-go/issues/414 。

TiKV 之間發送 snapshot 就是採用 issue 中推薦的方案,把大文件拆成多個 chunk 後使用 client streaming 發送。

總結

總體感覺,現在 gRPC 這個項目還不是太成熟,從不斷在重構 iomgr 這部分就能看出來,現在的 poll engine 的設計還是有很大的進步空間。

目前的效果 TiKV 吞吐已經和原來我們的手寫的 RPC 框架持平,但是 CPU 的消耗略高一些,但是功能上已經讓我們新功能的開發簡化很多,總體來說一定是利大於弊的,我們也在緊跟 gRPC 社區,相信這些性能問題都能被解決。

黃東旭,知名開源軟件作者,代表作品分佈式 Redis 緩存方案 Codis,以及分佈式關係型數據庫 TiDB。曾就職與微軟亞洲研究院,網易有道及豌豆莢,現任 PingCAP 聯合創始人兼 CTO,資深基礎軟件工程師,架構師。擅長分佈式系統以及數據庫開發,在分佈式存儲領域有豐富的經驗和獨到的見解。


分享到:


相關文章: