谷歌開源、高性能 RPC 框架:gRPC 使用體驗

作者:datumhu,騰訊 IEG 後開開發工程師

在廣告系統實踐中,精排服務基於 gRPC 協議調用 TF-Serving 在線推理服務。相信很多業務已經使用過 gRPC 相關語言的框架進行服務調用,尤其是基於谷歌雲的出海業務的服務調用更繞不開 gRPC,所以很有必要理解 gRPC 的原理。本文通過簡要介紹抓包分析一次 gRPC 的調用過程,逐步認識 gRPC。

概述

gRPC 是谷歌推出的一個開源、高性能的 RPC 框架。默認情況下使用 protoBuf 進行序列化和反序列化,並基於 HTTP/2 傳輸報文,帶來諸如多請求複用一個 TCP 連接 (所謂的多路複用)、雙向流、流控、頭部壓縮等特性。gRPC 目前提供 C、Go 和 JAVA 等語言版本,對應 gRPC、gRPC-Go 和 gRPC-JAVA 等開發框架。

在 gRPC 中,開發者可以像調用本地方法一樣,通過 gRPC 的客戶端調用遠程機器上 gRPC 服務的方法,gRPC 客戶端封裝了 HTTP/2 協議數據幀的打包、以及網絡層的通信細節,把複雜留給框架自己,把便捷提供給用戶。gRPC 基於這樣的一個設計理念:定義一個服務,及其被遠程調用的方法 (方法名稱、入參、出參)。在 gRPC 服務端實現這個方法的業務邏輯,並在 gRPC 服務端處理來着遠程客戶端對這個 RPC 方法的調用。在 gRPC 客戶端也擁有這個 RPC 方法的存根 (stub)。gRPC 的客戶端和服務端都可以用任何支持 gRPC 的語言來實現,例如一個 gRPC 服務端可以是 C++ 語言編寫的,以供 Ruby 語言的 gRPC 客戶端和 JAVA 語言的 gRPC 客戶端調用,如下圖所示:

gRPC 默認使用 ProtoBuf 對請求 / 響應進行序列化和反序列化,這使得傳輸的請求體和響應體比 JSON 等序列化方式包體更小、更輕量。

gRPC 基於 HTTP/2 協議傳輸報文,HTTP/2 具有多路複用、頭部壓縮等特性,基於 HTTP/2 的幀設計,實現了多個請求複用一個 TCP 連接,基本解決了 HTTP/1.1 的隊頭阻塞問題,相對 HTTP/1.1 帶來了巨大的性能提升。下面對 HTTP/2 進行簡介。

HTTP/2 簡介

HTTP 是一個成功的應用層協議。但是由於 HTTP 的隊頭阻塞等特性導致基於 HTTP 的應用程序性能有較大影響。隊頭阻塞是指順序請求的一個請求必須處理完才能處理後續的其他請求,當一個請求被阻塞時會給應用程序帶來延遲。雖然 HTTP/1.1 提供了流水線 (request pipeline) 的請求操作,但是由於受到 HTTP 自身協議的限制,無法消除 HTTP 的隊頭阻塞帶來的延遲。爲了減少延遲,需要 HTTP 的客戶端與服務器建立多個連接實現併發處理請求,降低延遲。然而,在高併發情況下,大量的網絡連接可能耗盡系統資源,可以使用連接池模式只維持固定的連接數可以防止服務的資源耗盡。連接池連接數的設置在對性能要求極高的應用程序也是一個挑戰,需要根據實際機器配置的壓測情況確定。

另外,HTTP 頭字段重複且冗長,導致網絡傳輸不必要的冗餘報文,以及初始 TCP 擁塞窗口很快被填滿。一個 TCP 連接處理大量請求是會導致較大的延遲。

HTTP/2 通過優化 HTTP 的報文定義,允許同一個網絡連接上併發交錯的處理請求和響應,並通過減少 HTTP 頭字段的重複傳輸、壓縮 HTTP 頭,提高了處理性能。

HTTP 每次網絡傳輸會攜帶通信的資源、瀏覽器屬性等大量冗餘頭信息,爲了減少這些重複傳輸的開銷,HTTP/2 會壓縮這些頭部字段:

  1. 基於 HTTP/2 協議的客戶端和服務器使用 "頭部表" 來跟蹤與存儲發送的鍵值對,對於相同的鍵值對數據,不用每次請求和響應都發送;

  2. 頭部表在 HTTP/2 的連接有效期內一直存在,由客戶端和服務器共同維護更新;

  3. 每個新的 HTTP 頭鍵值對要麼追加,要麼替換頭部表原來的值。

舉個例子,有兩個請求,在 HTTP/1.x 中,請求 1 和請求 2 都要發送全部的頭數據;在 HTTP/2 中,請求 1 發送全部的頭數據,請求 2 僅僅發送變更的頭數據,這樣就可以減少冗餘的數據,降低網絡開銷。如下圖所示:

這裏再舉個例子說明 HTTP/1.x 和 HTTP/2 處理請求的差異,瀏覽器打開網絡要請求 / index.html、styles.css 和 / scripts.js 三個文件,基於 HTTP/1.x 建立的連接只能先請求 / index.html, 得到響應後再請求 styles.css,得到響應後再獲取 / scripts.js。而基於 HTTP/2 一個網絡連接在獲取 / index.html 後, 可以同時獲取 styles.css 和 / scripts.js 文件,如下圖所示:

HTTP/2 對服務資源和網絡更友好,相對與 HTTP/1.x,處理同樣量級的請求,HTTP/2 的需要建立的 TCP 連接數更少。這主要得益於 HTTP/2 使用二進制數據幀來傳輸數據,使得一個 TCP 連接可以同時處理多個請求而不用等待一個請求處理完成再處理下一個。從而充分發掘了 TCP 的併發能力。

HTTP/2 幀

在 HTTP/2 中,幀是網絡通信的基本單位,HTTP/2 主要定義了 10 種不同的幀類型,每種幀類型在建立和管理連接或者單個 stream 流有不同的作用。不同的幀類型都有公共字段:Length(3 字節),Type(1 字節), Flags(1 字節), Stream Identifier(4 字節) 和 Frame Payload(變長)。

HTTP/2 幀都以固定的 9 字節大小作爲幀頭,後面跟着變長的包體 Paylload。如下圖所示:

幀頭字段說明:

  1. Length 幀的數據 (Frame Payload) 長度,不包括幀頭長度,3 個字節 (24bit), 幀最大長度爲 1<<24 - 1(16383);

  2. Type 幀類型,1 個字節 (8bit), 目前 HTTP/2 定義了 10 中幀類型,常見的幀類型有 DATA 幀、HEADERs 幀、SETTINGS 幀等,10 種幀類型如下圖所示:

  1. Flags 幀標誌,1 個字節 (8bit),沒有特定幀類型的幀標誌應該被忽略,在發送時幀標誌需要保持未設置 (0x0). 常見的標誌位有 END_HEADERS 表示 HTTP/2 數據頭結束,相當於 HTTP 頭後的空行(“\r\n”),END_STREAM 表示單方向數據發送結束(即 EOS,End of Stream),相當於 HTTP/1.x 裏 Chunked 分塊結束標誌(“0\r\n\r\n”);

  2. R 保留字段 1bit, 發送時保持未設置 (0x0), 接收時忽略;

  3. Stream Identifier 流標識符,31bit. 一個無符號整數。由客戶端發起的 Stream 數據流用奇數編號 ID 的流標識符;由服務器發起的數據流使用偶數編號 ID 的流標識符。流標識符零 (0x0) 用於連接控制消息;零流標識符不能用於建立新的 stream 流。

HTTP/2 請求模型

HTTP/2 的請求模型如下圖所示:

Connection 連接:對應一個 TCP 連接,可以承載一個或者多個 Stream。

Stream 流:對應一個雙向通信的數據流,可以承載一個或者多個 Message。每個數據流都有一個唯一的流標識符和可選的優先級信息,用於承載雙向消息。

Stream 流有幾個重要特性:

  1. 單個 HTTP/2 連接可以承載多個併發的 stream 流,通信雙方都可能交叉地收到多個 stream 流的數據幀;

  2. stream 流可以單方面建立與使用,也可以由客戶端和服務器雙方共享消息通道;

  3. 客戶端或者服務器都可以關閉 stream 流;

  4. 發送方在 stream 流按順序發送數據幀,接收到按照順序接收數據幀。特別地,HEADS 幀和 DATA 幀的順序在語言上是較爲重要的;

  5. stream 流由無符號整數標識。stream 流標識符是由發起流的端點分配給 stream 流的。

Message 消息:對應 HTTP/1.x 的請求 Request 或響應 response. 包含一個或者多個 Frame 數據幀。

**Frame 數據幀:**HTTP/2 網絡通信的基本單位,承載的是壓縮和編碼後的二進制流,不同 Stream 數據流的幀可以交錯發送,並根據幀頭的流 ID(數據流標識符) 進行區分和組裝。

關於 HTTP/2 主要介紹這些,更多參考:https://github.com/halfrost/Halfrost-Field/blob/master/contents/Protocol/HTTP:2-HTTP-Frames-Definitions.md

gRPC 協議

前面對 HTTP/2 幀作了簡要說明,這節開始介紹 gRPC 協議,gRPC 基於 HTTP/2 / 協議進行通信,使用 ProtoBuf 組織二進制數據流,gRPC 的協議格式如下圖:

從以上圖可知,gRPC 協議在 HTTP 協議的基礎上,對 HTTP/2 的幀的有效包體 (Frame Payload) 做了進一步編碼:gRPC 包頭(5 字節)+gRPC 變長包頭,其中:

  1. 5 字節的 gRPC 包頭由:1 字節的壓縮標誌 (compress flag) 和 4 字節的 gRPC 包頭長度組成;

  2. gRPC 包體長度是變長的,其是這樣的一串二進制流:使用指定序列化方式 (通常是 ProtoBuf) 序列化成字節流,再使用指定的壓縮算法對序列化的字節流壓縮而成的。如果對序列化字節流進行了壓縮,gRPC 包頭的壓縮標誌爲 1。

  3. 對比 tRPC 協議可知,gRPC 的幀頭和包頭比 tRPC 協議幀頭和包頭要小,當然 HTTP/2 的幀類型更復雜一些。tRPC 協議幀定義如下圖:

gRPC 調用抓包分析

下面基於官方提供的 gRPC-Go helloword 例子,使用 Wireshark 分析通過 tcpdump 抓包 gRPC 調用的報文,加深對 gRPC 協議的理解。

1. 抓包準備

  1. 下載 Wireshark 抓包工具,下載地址:https://www.wireshark.org/;

  2. 安裝 Go 環境;

  3. 安裝 protoc-gen-go: go get -u github.com/golang/protobuf/protoc-gen-go;

  4. 下載 grpc-go/examples/helloworld gRPC-Go 的 helloword Go 工程。

2. 抓包

  1. 運行 helloword 的服務端 greeter_server:

  1. 使用 tcpdump 命令準備抓一次 helloword 的調用:

sudo tcpdump -iany port 50051 -w grpc.cap

  1. 運行 helloword 的客戶端 greeter_client:

完成一次調用,tcpdump 抓到一次調用的報文,保存爲 grpc.cap。

3.Wireshark 配置

打開 Wireshark 主面板,選擇 ProtoBuf 文件路徑:Wireshark-->Preferences-->Protocols-->Protobuf-->Protobuf Search Paths。

選擇 helloworld 的 proto 文件地址。

Wireshark 打開 grpc.cap 文件,選中 greeter_client 發送端口號和 greeter_server 發送端口號的報文記錄,右鍵 Decode As... 爲 HTTP/2:

Wireshark 過濾框輸入 HTTP2 就可得到一次完整的 gRPC 調用細節:

4.gRPC 調用分析

從以上抓包得到的 gRPC 調用圖可知,gRPC 客戶端 (port:62880) 一次調用服務端 (port:50051) 的 RPC 方法通常會包括多次 HTTP/2 幀的發送,本文分析中抓包的一個幀序列例子:Magic-->SETTINGS(雙向四個)-->HEADERS-->DATA(GRPC-PROTOBUF)-->WINDOW_UPDATE,PING-->PING-->HEADERS,DATA,HEADERS-->WINDOW_UPDATE,PING-->PING。

下面對調用過程中的每個幀做簡要分析。

1)客戶端發送 Magic 幀 Magic 幀的爲固定內容:PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n。如下圖所示:客戶端發送 Magic 幀後雙方就會使用 HTTP/2 相關協議進行通信。

2)客戶端和服務端發 SETTINGS 幀

接着 Magic 幀後,接下來就是發送 SETTINGS 幀,SETTINGS 幀主要用於傳遞影響兩端網絡通信的配置參數,以及確認收到這些參數。

客戶端和服務器首先發兩個 SETTINGS 幀傳遞配置參數信息,接着服務端發了一個確認的 SETTINGS 幀後,客戶端也發出了一個確認的 SETTINGS 幀:

a. 客戶端發第一個 SETTINGS, 幀類型 = 0x4,幀標誌爲 0x00, 流標識符爲 0:

b. 服務端向客戶端回了一個 SETTINGS 幀,幀類型 = 0x4,幀標誌爲 0x00, 流標識符爲 0,同時告訴客戶端,服務端願意接收的最大幀大小爲 16384 bytes。同時我們看到,SETTINGS 幀的參數類型爲 SETTINGS_MAX_FRAME_SIZE(0x5),參數類型表示服務端願意接受的包體大小,初始值 爲 16364 個字節。此外,SETTINGS 幀長度爲 6:

c. 隨後服務端再發出一個確認的 SETTINGS 幀,幀類型 = 0x4,幀標誌爲 0x01, 流標識符爲 0:

d. 客戶端收到服務端的確認 SETTINGS 幀後,也發出一個 SETTINGS 幀進行確認,幀類型 = 0x4,幀標誌爲 0x01, 流標識符爲 0,雙方進行確認後下面就可以開始傳輸頭幀 (HEADERS) 和數據幀 (DATA) 了:

3)客戶端發送 HEADERS 幀

客戶端和服務器雙方發送 SETTINGS 幀進行雙方參數確認後,下一步客戶端向服務端發送一個 HEADERS 幀, 當前 HEADERS 幀長度爲 92,幀類型 = 0x1,幀標誌爲 0x04(End Headers,0=End Stream:False,1=End Headers:True,0=Padded:False,0=Priority:False),流標識符爲 1,HEADERS 幀還額外帶有 Head Block Fragment 頭塊片段 (header 列表是零個或多個字段的集合。當通過網絡連接傳輸時,使用 HTTP 頭壓縮 [COMPRESSION] 將 header 列表序列化爲 header block 塊。然後將序列化的 header block 塊分成字節流,稱爲 header 塊片段);

同時還可以看到一些 HTTP 請求頭 (8 個) 信息,比如: method:POST,:scheme:http,:path:/helloworld.Greeter/SayHello 等等,如下圖所示:

4)客戶端發送 DATA 幀

HEADERS 幀發送完之後,接下來客戶端給服務器發送 DATA 幀,當前數據幀的長度爲 18 字節 (不包含 HTTP/2 幀頭),幀標識爲 0x01:End Stream,流標識符爲 1,然後是 HTTP/2 的有效包體數據信息(18 字節),也就是經過 protobuf 序列化的字節流的 gRPC 數據;當前的 gRPC 數據由 gRPC 包頭(5 字節)+gRPC 包體(13 字節) 組成,gRPC 包頭的壓縮標誌爲 Not Compressed(未壓縮),gRPC 包頭長度爲 13 字節,gRPC 的包體內容爲 "who are you", 對應的 protobuf 中 message 的 Name 字段承載的信息 = WireType < 本身佔 1 個字節>枚舉值爲 2[string, 編碼 0a]+value 長度<本身佔 1 個字節>[string 需要顯式的告知 value 長度]11 個字節(編碼 0b)+ 字段 value 信息 "who are you"<11 個字節>,如下圖所示:

5)服務端發送 WINDOW_UPDATE 幀和 PING 幀

客戶端發完 DATA 幀後,服務器先回復了兩個幀,分別是 WINDOW_UPDATE 幀和 PING 幀,

WINDOW_UPDATE 幀 主要用於流量控制。當前的 WINDOW_UPDATE 幀的長度爲 4,幀類型爲 WINDOW_UPDATE(8),幀標誌爲 0x00,流標誌符爲 0,Window Size Increment(流量窗口增量)爲 18(收到客戶端發送的 DATA 幀長度 18)。

PING 幀 用於測量最小往返時間 (RTT) 以及確定連接是否存活。當前 PING 幀的長度爲 8,幀類型爲 PING(6),幀標誌爲 0x00(ACK=False),流標誌符爲 0。

此次 WINDOW_UPDATE 幀和 PING 幀的發送情況如下圖所示:

6)客戶端回覆 PING 幀

客戶端收到服務器的 PING 幀後,會回一個 PING 幀確認 (ACK=True) 以及回覆 Pong 信息,當前 PING 幀的長度爲 8,幀類型爲 PING(6),幀標誌爲 0x01(ACK=True),流標誌符爲 0,Pong 信息爲一串 16 位的 UUID 串,如下圖所示:

7)服務端回覆 HEADERS 幀 + DATA 幀 (gRPC)+HEADERS 幀 (終止流)

服務端收到客戶端的 PING 幀確認客戶端存活狀態後,

a. 首先是一個 HEADERS 幀,該幀的幀長度爲 14,幀類型 Type 爲 HEADS(1),幀標誌 Flags 爲 End Headers(0x04),流標誌符爲 1,

HEAD 長度爲 54,head 數量爲 2,分別爲 status: 200 OK、content-type:application/grpc;

b. 然後是一個 DATA 幀,該幀的幀長度爲 20,幀類型 Type 爲 DATA(0),幀標誌 Flags 爲 0x00,流標誌符爲 1,HTTP/2 的有效包體數據信息,也即是 gRPC 數據信息爲 15 個字節 (5 字節的 gRPC 包頭 + 15 字節的 gRPC 包體內容 (”I am datumhu“));

c. 最後是一個終止流的 HEADERS 幀,該 HEADERS 幀的幀長度爲 24,幀類型 Type 爲 HEADS(1),幀標誌 Flags 爲 End Headers,End Stream(0x05),流標誌符爲 1,HEAD 長度爲 40 字節,head 數量爲 2,分別爲 grpc-status: 0、grpc-message:;

如下圖所示:

8)客戶端回覆 WINDOW_UPDATE 幀和 PING 幀

客戶端收到服務端的 DATA 響應後,給服務器發送一個 WINDOW_UPDATE 幀和 PING 幀,其中 WINDOW_UPDATE 的窗口大小增量爲 20(收到服務端響應的 DATA 幀長度),如下圖所示:

9)服務端回 PING 幀

最後服務器收到客戶端的 PING 幀後,回覆一個 PING 幀確認 (ACK=1),如下圖所示:

以上一次 gRPC 調用的數據流圖概括爲如下:

總結

本文首先概述了 gRPC 的原理,由於 gRPC 是基於 HTTP/2 協議進行網絡傳輸,隨後簡介了 HTTP/2 通過多路複用和頭部壓縮等優化措施,基本解決了 HTTP/1.x 包頭阻塞的問題,相對 HTTP/1.1 帶來了性能提升。HTTP/2 多路複用和頭部壓縮的關鍵在於 HTTP/2 通過幀的設計優化了 HTTP 協議語義。所以接着介紹了 HTTP/2 的幀結構和 gRPC 的協議。最後通過抓包一次完整的 gRPC 調用,分析了 GRPC-HTTP2 的數據流過程,希望能夠加深對 gRPC 的理解。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/6XXJfbnIaKzSFtXyDDB72g