深入了解 gRPC:協議
gRPC 是一個高性能、通用的開源RPC框架,其由 Google 主要面向移動應用開發並基於HTTP/2 協議標準而設計,基於 ProtoBuf(Protocol Buffers) 序列化協議開發,且支持眾多開發語言。本文作者深入研究了 gRPC 協議,對協議本身作出解構。
gRPC 是基於 HTTP/2 協議的,要深刻理解 gRPC,理解下 HTTP/2 是必要的,這裡先簡單介紹一下 HTTP/2 相關的知識,然後再介紹下 gRPC 是如何基於 HTTP/2 構建的。
HTTP/1.xHTTP 協議可以算是現階段 Web 上面最通用的協議了,在之前很長一段時間,很多應用都是基於 HTTP/1.x 協議,HTTP/1.x 協議是一個文本協議,可讀性非常好,但其實並不高效,筆者主要碰到過幾個問題:
Parser如果要解析一個完整的 HTTP 請求,首先我們需要能正確的讀出 HTTP header。HTTP header 各個 fields 使用
分隔,然後跟 body 之間使用
分隔。解析完 header 之後,我們才能從 header 裡面的content-length
拿到 body 的 size,從而讀取 body。
這套流程其實並不高效,因為我們需要讀取多次,才能將一個完整的 HTTP 請求給解析出來,雖然在代碼實現上面,有很多優化方式,譬如:
一次將一大塊數據讀取到 buffer 裡面避免多次 IO read
讀取的時候直接匹配
的方式流式解析
但上面的方式對於高性能服務來說,終歸還是會有開銷。其實最主要的問題在於,HTTP/1.x 的協議是 文本協議,是給人看的,對機器不友好,如果要對機器友好,二進位協議才是更好的選擇。
如果大家對解析 HTTP/1.x 很感興趣,可以研究下 http-parser,一個非常高效小巧的 C library,見過不少框架都是集成了這個庫來處理 HTTP/1.x 的。
Request/ResponseHTTP/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 的):
Flag 和 R:保留位,可以先不管。
Stream Identifier:標識所屬的 stream,如果為 0,則表示這個 frame 屬於整條連接。
Frame Payload:根據不同 Type 有不同的格式。
可以看到,Frame 的格式定義還是非常的簡單,按照官方協議,可以非常方便的寫一個出來。
MultiplexingHTTP/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 ControlHTTP/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 時代,我們採用純文本協議,並且使用
來分隔,如果我們要傳輸的元數據很多,就會導致 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 gRPCgRPC 是 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-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。
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


※MQTT, XMPP, WebSockets還是AMQP?泛談實時通信協議選型
※揪出一個導致GC慢慢變長的JVM設計缺陷
※團隊交流用QQ、微信群還是Slack?為什麼50人跨地域團隊放棄實時群聊工具
※如何使用火焰圖來降低伺服器負載
※被忽視的點陣圖資料庫:Pilosa查詢十億級計程車搭乘數據案例
TAG:高可用架構 |
※你了解HTTP協議嗎?
※帶你了解ZARA、H&M、UR
※Mate RS 了解一下?
※HTC Vive Pro最全拆解報告,了解一下
※你所不了解的《ALBUM》
※GoPro HERO6的HEVC,麻煩了解一下
※引爆EOS上漲的eosDAC是什麼,深入了解,必須的
※PICK了解一下
※深入淺出MyBatis:「映射器」全了解
※Harden Vol.2高仿籃球鞋深入評測 了解一下?
※JOYCE | GD暫別入伍?PEACEMINUSONE了解一下
※LOL:HKA戰績不佳主教練Tabe離隊,LPL粉絲建議WE、RNG了解一下
※赴美讀研的話,GRE/GMAT了解一下?
※Virgil Abloh x AIR JORDAN 1今日發售!碼數貨存你必須要了解!
※你不了解的OPPO R15「演唱會」
※打造最強Urban Style?PUMA#街頭歸你#系列了解一下!
※15件事就能讓你了解VISVIM。
※帶你了解莆田版的NIKE EPIC REACT FLYKNIT
※UCCVR即將發布《AR產業白皮書》:你要了解的AR內容98%都在這裡
※SmartMesh:60秒了解顛覆世界的SMT移動雷電