深入了解 gRPC:協議

經過很長一段時間的開發,TiDB 終于發了 RC3。RC3 版本對于 TiKV 來說最重要的功能就是支持了 gRPC,也就意味著后面大家可以非常方便的使用自己喜歡的語言對接 TiKV 了。

gRPC 是基于 HTTP/2 協議的,要深刻理解 gRPC,理解下 HTTP/2 是必要的,這里先簡單介紹一下 HTTP/2 相關的知識,然后在介紹下 gRPC 是如何基于 HTTP/2 構建的。

HTTP/1.x

HTTP 協議可以算是現階段 Web 上面最通用的協議了,在之前很長一段時間,很多應用都是基于 HTTP/1.x 協議,HTTP/1.x 協議是一個文本協議,可讀性非常好,但其實并不高效,筆者主要碰到過幾個問題:

Parser

如果要解析一個完整的 HTTP 請求,首先我們需要能正確的讀出 HTTP header。HTTP header 各個 fields 使用 \r\n 分隔,然后跟 body 之間使用 \r\n\r\n 分隔。解析完 header 之后,我們才能從 header 里面的 content-length 拿到 body 的 size,從而讀取 body。

這套流程其實并不高效,因為我們需要讀取多次,才能將一個完整的 HTTP 請求給解析出來,雖然在代碼實現上面,有很多優化方式,譬如:

  • 一次將一大塊數據讀取到 buffer 里面避免多次 IO read
  • 讀取的時候直接匹配 \r\n 的方式流式解析

但上面的方式對于高性能服務來說,終歸還是會有開銷。其實最主要的問題在于,HTTP/1.x 的協議是 文本協議,是給人看的,對機器不友好,如果要對機器友好,二進制協議才是更好的選擇。

如果大家對解析 HTTP/1.x 很感興趣,可以研究下 http-parser,一個非常高效小巧的 C library,見過不少框架都是集成了這個庫來處理 HTTP/1.x 的。

Request/Response

HTTP/1.x 另一個問題就在于它的交互模式,一個連接每次只能一問一答,也就是client 發送了 request 之后,必須等到 response,才能繼續發送下一次請求。

這套機制是非常簡單,但會造成網絡連接利用率不高。如果需要同時進行大量的交互,client 需要跟 server 建立多條連接,但連接的建立也是有開銷的,所以為了性能,通常這些連接都是長連接一直保活的,雖然對于 server 來說同時處理百萬連接也沒啥太大的挑戰,但終歸效率不高。

Push

用 HTTP/1.x 做過推送的同學,大概就知道有多么的痛苦,因為 HTTP/1.x 并沒有推送機制。所以通常兩種做法:

  • Long polling 方式,也就是直接給 server 掛一個連接,等待一段時間(譬如 1 分鐘),如果 server 有返回或者超時,則再次重新 poll。
  • Web-socket,通過 upgrade 機制顯式的將這條 HTTP 連接變成裸的 TCP,進行雙向交互。

相比 Long polling,筆者還是更喜歡 web-socket 一點,畢竟更加高效,只是 web-socket 后面的交互并不是傳統意義上面的 HTTP 了。

Hello HTTP/2

雖然 HTTP/1.x 協議可能仍然是當今互聯網運用最廣泛的協議,但隨著 Web 服務規模的不斷擴大,HTTP/1.x 越發顯得捉緊見拙,我們急需另一套更好的協議來構建我們的服務,于是就有了 HTTP/2。

HTTP/2 是一個二進制協議,這也就意味著它的可讀性幾乎為 0,但幸運的是,我們還是有很多工具,譬如 Wireshark, 能夠將其解析出來。

在了解 HTTP/2 之前,需要知道一些通用術語:

  • Stream: 一個雙向流,一條連接可以有多個 streams。
  • Message: 也就是邏輯上面的 request,response。
  • Frame::數據傳輸的最小單位。每個 Frame 都屬于一個特定的 stream 或者整個連接。一個 message 可能有多個 frame 組成。

Frame Format

Frame 是 HTTP/2 里面最小的數據傳輸單位,一個 Frame 定義如下(直接從官網 copy 的):

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

Length:也就是 Frame 的長度,默認最大長度是 16KB,如果要發送更大的 Frame,需要顯式的設置 max frame size。
Type:Frame 的類型,譬如有 DATA,HEADERS,PRIORITY 等。
Flag 和 R:保留位,可以先不管。
Stream Identifier:標識所屬的 stream,如果為 0,則表示這個 frame 屬于整條連接。
Frame Payload:根據不同 Type 有不同的格式。

可以看到,Frame 的格式定義還是非常的簡單,按照官方協議,可以非常方便的寫一個出來。

Multiplexing

HTTP/2 通過 stream 支持了連接的多路復用,提高了連接的利用率。Stream 有很多重要特性:

  • 一條連接可以包含多個 streams,多個 streams 發送的數據互相不影響。
  • Stream 可以被 client 和 server 單方面或者共享使用。
  • Stream 可以被任意一段關閉。
  • Stream 會確定好發送 frame 的順序,另一端會按照接受到的順序來處理。
  • Stream 用一個唯一 ID 來標識。

這里在說一下 Stream ID,如果是 client 創建的 stream,ID 就是奇數,如果是 server 創建的,ID 就是偶數。ID 0x00 和 0x01 都有特定的使用場景。

Stream ID 不可能被重復使用,如果一條連接上面 ID 分配完了,client 會新建一條連接。而 server 則會給 client 發送一個 GOAWAY frame 強制讓 client 新建一條連接。

為了更大的提高一條連接上面的 stream 并發,可以考慮調大 SETTINGS_MAX_CONCURRENT_STREAMS,在 TiKV 里面,我們就遇到過這個值比較小,整體吞吐上不去的問題。

這里還需要注意,雖然一條連接上面能夠處理更多的請求了,但一條連接遠遠是不夠的。一條連接通常只有一個線程來處理,所以并不能充分利用服務器多核的優勢。同時,每個請求編解碼還是有開銷的,所以用一條連接還是會出現瓶頸。

在 TiKV 有一個版本中,我們就過分相信一條連接跑多 streams 這種方式沒有問題,就讓 client 只用一條連接跟 TiKV 交互,結果發現性能完全沒法用,不光處理連接的線程 CPU 跑滿,整體的性能也上不去,后來我們換成了多條連接,情況才好轉。

Priority

因為一條連接允許多個 streams 在上面發送 frame,那么在一些場景下面,我們還是希望 stream 有優先級,方便對端為不同的請求分配不同的資源。譬如對于一個 Web 站點來說,優先加載重要的資源,而對于一些不那么重要的圖片啥的,則使用低的優先級。

我們還可以設置 Stream Dependencies,形成一棵 streams priority tree。假設 Stream A 是 parent,Stream B 和 C 都是它的孩子,B 的 weight 是 4,C 的 weight 是 12,假設現在 A 能分配到所有的資源,那么后面 B 能分配到的資源只有 C 的 1/3。

Flow Control

HTTP/2 也支持流控,如果 sender 端發送數據太快,receiver 端可能因為太忙,或者壓力太大,或者只想給特定的 stream 分配資源,receiver 端就可能不想處理這些數據。譬如,如果 client 給 server 請求了一個視頻,但這時候用戶暫停觀看了,client 就可能告訴 server 別在發送數據了。

雖然 TCP 也有 flow control,但它僅僅只對一個連接有效果。HTTP/2 在一條連接上面會有多個 streams,有時候,我們僅僅只想對一些 stream 進行控制,所以 HTTP/2 單獨提供了流控機制。Flow control 有如下特性:

  • Flow control 是單向的。Receiver 可以選擇給 stream 或者整個連接設置 window size。
  • Flow control 是基于信任的。Receiver 只是會給 sender 建議它的初始連接和 stream 的 flow control window size。
  • Flow control 不可能被禁止掉。當 HTTP/2 連接建立起來之后,client 和 server 會交換 SETTINGS frames,用來設置 flow control window size。
  • Flow control 是 hop-by-hop,并不是 end-to-end 的,也就是我們可以用一個中間人來進行 flow control。

這里需要注意,HTTP/2 默認的 window size 是 64 KB,實際這個值太小了,在 TiKV 里面我們直接設置成 1 GB。

HPACK

在一個 HTTP 請求里面,我們通常在 header 上面攜帶很多該請求的元信息,用來描述要傳輸的資源以及它的相關屬性。在 HTTP/1.x 時代,我們采用純文本協議,并且使用 \r\n 來分隔,如果我們要傳輸的元數據很多,就會導致 header 非常的龐大。另外,多數時候,在一條連接上面的多數請求,其實 header 差不了多少,譬如我們第一個請求可能 GET /a.txt,后面緊接著是 GET /b.txt,兩個請求唯一的區別就是 URL path 不一樣,但我們仍然要將其他所有的 fields 完全發一遍。

HTTP/2 為了結果這個問題,使用了 HPACK。雖然 HPACK 的 RFC 文檔 看起來比較恐怖,但其實原理非常的簡單易懂。

HPACK 提供了一個靜態和動態的 table,靜態 table 定義了通用的 HTTP header fields,譬如 method,path 等。發送請求的時候,只要指定 field 在靜態 table 里面的索引,雙方就知道要發送的 field 是什么了。

對于動態 table,初始化為空,如果兩邊交互之后,發現有新的 field,就添加到動態 table 上面,這樣后面的請求就可以跟靜態 table 一樣,只需要帶上相關的 index 就可以了。

同時,為了減少數據傳輸的大小,使用 Huffman 進行編碼。這里就不再詳細說明 HPACK 和 Huffman 如何編碼了。

小結

上面只是大概列舉了一些 HTTP/2 的特性,還有一些,譬如 push,以及不同的 frame 定義等都沒有提及,大家感興趣,可以自行參考 HTTP/2 RFC 文檔

Hello gRPC

gRPC 是 Google 基于 HTTP/2 以及 protobuf 的,要了解 gRPC 協議,只需要知道 gRPC 是如何在 HTTP/2 上面傳輸就可以了。

gRPC 通常有四種模式,unary,client streaming,server streaming 以及 bidirectional streaming,對于底層 HTTP/2 來說,它們都是 stream,并且仍然是一套 request + response 模型。

Request

gRPC 的 request 通常包含 Request-Headers, 0 或者多個 Length-Prefixed-Message 以及 EOS。

Request-Headers 直接使用的 HTTP/2 headers,在 HEADERS 和 CONTINUATION frame 里面派發。定義的 header 主要有 Call-Definition 以及 Custom-Metadata。Call-Definition 里面包括 Method(其實就是用的 HTTP/2 的 POST),Content-Type 等。而 Custom-Metadata 則是應用層自定義的任意 key-value,key 不建議使用 grpc- 開頭,因為這是為 gRPC 后續自己保留的。

Length-Prefixed-Message 主要在 DATA frame 里面派發,它有一個 Compressed flag 用來表示該 message 是否壓縮,如果為 1,表示該 message 采用了壓縮,而壓縮算啊定義在 header 里面的 Message-Encoding 里面。然后后面跟著四字節的 message length 以及實際的 message。

EOS(end-of-stream) 會在最后的 DATA frame 里面帶上了 END_STREAM 這個 flag。用來表示 stream 不會在發送任何數據,可以關閉了。

Response

Response 主要包含 Response-Headers,0 或者多個 Length-Prefixed-Message 以及 Trailers。如果遇到了錯誤,也可以直接返回 Trailers-Only。

Response-Headers 主要包括 HTTP-Status,Content-Type 以及 Custom-Metadata 等。Trailers-Only 也有 HTTP-Status ,Content-Type 和 Trailers。Trailers 包括了 Status 以及 0 或者多個 Custom-Metadata。

HTTP-Status 就是我們通常的 HTTP 200,301,400 這些,很通用就不再解釋。Status 也就是 gRPC 的 status, 而 Status-Message 則是 gRPC 的 message。Status-Message 采用了 Percent-Encoded 的編碼方式,具體參考這里

如果在最后收到的 HEADERS frame 里面,帶上了 Trailers,并且有 END_STREAM 這個 flag,那么就意味著 response 的 EOS。

Protobuf

gRPC 的 service 接口是基于 protobuf 定義的,我們可以非常方便的將 service 與 HTTP/2 關聯起來。

  • Path : /Service-Name/{method name}
  • Service-Name : ?( {proto package name} "." ) {service name}
  • Message-Type : {fully qualified proto message name}
  • Content-Type : "application/grpc+proto"

后記

上面只是對 gRPC 協議的簡單理解,可以看到,gRPC 的基石就是 HTTP/2,然后在上面使用 protobuf 協議定義好 service RPC。雖然看起來很簡單,但如果一門語言沒有 HTTP/2,protobuf 等支持,要支持 gRPC 就是一件非常困難的事情了。

悲催的是,Rust 剛好沒有 HTTP/2 支持,也僅僅有一個可用的 protobuf 實現。為了支持 gRPC,我們 team 付出了很大的努力,也走了很多彎路,從最初使用純 Rust 的 rust-grpc 項目,到后來自己基于 c-grpc 封裝了 grpc-rs,還是有很多可以說的,后面在慢慢道來。如果你對 gRPC 和 rust 都很感興趣,歡迎參與開發。

gRPC-rs: https://github.com/pingcap/grpc-rs

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容

  • gRPC 是一個高性能、通用的開源RPC框架,其由 Google 主要面向移動應用開發并基于HTTP/2 協議標準...
    劉琨_10f5閱讀 1,309評論 4 6
  • gRPC 是基于 HTTP/2 協議的,要深刻理解 gRPC,理解下 HTTP/2 是必要的。本篇文章會先簡單介紹...
    tcgx閱讀 1,285評論 1 1
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • HTTP/2 是一個二進制協議,這也就意味著它的可讀性幾乎為 0,但幸運的是,我們還是有很多工具,譬如 Wires...
    angeChen閱讀 1,420評論 0 2
  • 看到玻璃中有人 我便問你是誰啊 他說,我是你啊 我又問你可以出來嗎 他說除非你進來 我把手伸進玻璃 當我們的手觸碰...
    楊孜閱讀 845評論 3 0