深入瞭解 gRPC:協議

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

經過很長一段時間的開發,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

相關關鍵詞:
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.