深入了解gRPC:協(xié)議

gRPC 是基于 HTTP/2 協(xié)議的,要深刻理解 gRPC,理解下 HTTP/2 是必要的。本篇文章會(huì)先簡(jiǎn)單介紹一下 HTTP/2 相關(guān)的知識(shí),然后再介紹下 gRPC 是如何基于 HTTP/2 構(gòu)建的。

HTTP/1.x

HTTP 協(xié)議可以算是現(xiàn)階段 Web 上面最通用的協(xié)議了,在之前很長(zhǎng)一段時(shí)間,很多應(yīng)用都是基于 HTTP/1.x 協(xié)議,HTTP/1.x 協(xié)議是一個(gè)文本協(xié)議,可讀性非常好,但其實(shí)并不高效,筆者主要碰到過(guò)幾個(gè)問(wèn)題:

parser

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

這套流程其實(shí)并不高效,因?yàn)槲覀冃枰x取多次,才能將一個(gè)完整的 HTTP 請(qǐng)求給解析出來(lái),雖然在代碼實(shí)現(xiàn)上面,有很多優(yōu)化方式,譬如:

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

但上面的方式對(duì)于高性能服務(wù)來(lái)說(shuō),終歸還是會(huì)有開(kāi)銷(xiāo)。其實(shí)最主要的問(wèn)題在于,HTTP/1.x 的協(xié)議是 文本協(xié)議,是給人看的,對(duì)機(jī)器不友好,如果要對(duì)機(jī)器友好,二進(jìn)制協(xié)議才是更好的選擇。

如果大家對(duì)解析 HTTP/1.x 很感興趣,可以研究下 http-parser,一個(gè)非常高效小巧的 C library,見(jiàn)過(guò)不少框架都是集成了這個(gè)庫(kù)來(lái)處理 HTTP/1.x 的。

Request/Response

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

這套機(jī)制是非常簡(jiǎn)單,但會(huì)造成網(wǎng)絡(luò)連接利用率不高。如果需要同時(shí)進(jìn)行大量的交互,client 需要跟 server 建立多條連接,但連接的建立也是有開(kāi)銷(xiāo)的,所以為了性能,通常這些連接都是長(zhǎng)連接一直保活的,雖然對(duì)于 server 來(lái)說(shuō)同時(shí)處理百萬(wàn)連接也沒(méi)啥太大的挑戰(zhàn),但終歸效率不高。

Push

用 HTTP/1.x 做過(guò)推送的同學(xué),大概就知道有多么的痛苦,因?yàn)?HTTP/1.x 并沒(méi)有推送機(jī)制。所以通常兩種做法:

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

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

Hello HTTP/2

雖然 HTTP/1.x 協(xié)議可能仍然是當(dāng)今互聯(lián)網(wǎng)運(yùn)用最廣泛的協(xié)議,但隨著 Web 服務(wù)規(guī)模的不斷擴(kuò)大,HTTP/1.x 越發(fā)顯得捉緊見(jiàn)拙,我們急需另一套更好的協(xié)議來(lái)構(gòu)建我們的服務(wù),于是就有了 HTTP/2。

HTTP/2 是一個(gè)二進(jìn)制協(xié)議,這也就意味著它的可讀性幾乎為 0,但幸運(yùn)的是,我們還是有很多工具,譬如 Wireshark, 能夠?qū)⑵浣馕龀鰜?lái)。

在了解 HTTP/2 之前,需要知道一些通用術(shù)語(yǔ):

  • Stream: 一個(gè)雙向流,一條連接可以有多個(gè) streams。
  • Message: 也就是邏輯上面的 request,response。
  • Frame::數(shù)據(jù)傳輸?shù)淖钚挝?。每個(gè) Frame 都屬于一個(gè)特定的 stream 或者整個(gè)連接。一個(gè) message 可能有多個(gè) frame 組成。

Frame Format

Frame 是 HTTP/2 里面最小的數(shù)據(jù)傳輸單位,一個(gè) Frame 定義如下:

http2-grpc.jpg
  • Length:也就是 Frame 的長(zhǎng)度,默認(rèn)最大長(zhǎng)度是 16KB,如果要發(fā)送更大的 Frame,需要顯式的設(shè)置 max frame size。
  • Type:Frame 的類(lèi)型,譬如有 DATA,HEADERS,PRIORITY 等。
  • Flag 和 R:保留位,可以先不管。
  • Stream Identifier:標(biāo)識(shí)所屬的 stream,如果為 0,則表示這個(gè) frame 屬于整條連接。
  • Frame Payload:根據(jù)不同 Type 有不同的格式。
    可以看到,F(xiàn)rame 的格式定義還是非常的簡(jiǎn)單,按照官方協(xié)議,可以非常方便的寫(xiě)一個(gè)出來(lái)。

Multiplexing

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

  • 一條連接可以包含多個(gè) streams,多個(gè) streams 發(fā)送的數(shù)據(jù)互相不影響。
  • Stream 可以被 client 和 server 單方面或者共享使用。
  • Stream 可以被任意一段關(guān)閉。
  • Stream 會(huì)確定好發(fā)送 frame 的順序,另一端會(huì)按照接受到的順序來(lái)處理。
  • Stream 用一個(gè)唯一 ID 來(lái)標(biāo)識(shí)。

這里在說(shuō)一下 Stream ID,如果是 client 創(chuàng)建的 stream,ID 就是奇數(shù),如果是 server 創(chuàng)建的,ID 就是偶數(shù)。ID 0x00 和 0x01 都有特定的使用場(chǎng)景。

Stream ID 不可能被重復(fù)使用,如果一條連接上面 ID 分配完了,client 會(huì)新建一條連接。而 server 則會(huì)給 client 發(fā)送一個(gè) GOAWAY frame 強(qiáng)制讓 client 新建一條連接。

為了更大的提高一條連接上面的 stream 并發(fā),可以考慮調(diào)大 SETTINGS_MAX_CONCURRENT_STREAMS,在 TiKV 里面,我們就遇到過(guò)這個(gè)值比較小,整體吞吐上不去的問(wèn)題。

這里還需要注意,雖然一條連接上面能夠處理更多的請(qǐng)求了,但一條連接遠(yuǎn)遠(yuǎn)是不夠的。一條連接通常只有一個(gè)線程來(lái)處理,所以并不能充分利用服務(wù)器多核的優(yōu)勢(shì)。同時(shí),每個(gè)請(qǐng)求編解碼還是有開(kāi)銷(xiāo)的,所以用一條連接還是會(huì)出現(xiàn)瓶頸。

在 TiKV 有一個(gè)版本中,我們就過(guò)分相信一條連接跑多 streams 這種方式?jīng)]有問(wèn)題,就讓 client 只用一條連接跟 TiKV 交互,結(jié)果發(fā)現(xiàn)性能完全沒(méi)法用,不光處理連接的線程 CPU 跑滿,整體的性能也上不去,后來(lái)我們換成了多條連接,情況才好轉(zhuǎn)。

Priority

因?yàn)橐粭l連接允許多個(gè) streams 在上面發(fā)送 frame,那么在一些場(chǎng)景下面,我們還是希望 stream 有優(yōu)先級(jí),方便對(duì)端為不同的請(qǐng)求分配不同的資源。譬如對(duì)于一個(gè) Web 站點(diǎn)來(lái)說(shuō),優(yōu)先加載重要的資源,而對(duì)于一些不那么重要的圖片啥的,則使用低的優(yōu)先級(jí)。

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

Flow Control

HTTP/2 也支持流控,如果 sender 端發(fā)送數(shù)據(jù)太快,receiver 端可能因?yàn)樘?,或者壓力太大,或者只想給特定的 stream 分配資源,receiver 端就可能不想處理這些數(shù)據(jù)。譬如,如果 client 給 server 請(qǐng)求了一個(gè)視頻,但這時(shí)候用戶(hù)暫停觀看了,client 就可能告訴 server 別在發(fā)送數(shù)據(jù)了。

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

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

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

HPACK

在一個(gè) HTTP 請(qǐng)求里面,我們通常在 header 上面攜帶很多該請(qǐng)求的元信息,用來(lái)描述要傳輸?shù)馁Y源以及它的相關(guān)屬性。在 HTTP/1.x 時(shí)代,我們采用純文本協(xié)議,并且使用\r\n來(lái)分隔,如果我們要傳輸?shù)脑獢?shù)據(jù)很多,就會(huì)導(dǎo)致 header 非常的龐大。另外,多數(shù)時(shí)候,在一條連接上面的多數(shù)請(qǐng)求,其實(shí) header 差不了多少,譬如我們第一個(gè)請(qǐng)求可能GET /a.txt后面緊接著是GET /b.txt兩個(gè)請(qǐng)求唯一的區(qū)別就是 URL path 不一樣,但我們?nèi)匀灰獙⑵渌械?fields 完全發(fā)一遍。

HTTP/2 為了結(jié)果這個(gè)問(wèn)題,使用了 HPACK。雖然 HPACK 的 RFC 文檔看起來(lái)比較恐怖,但其實(shí)原理非常的簡(jiǎn)單易懂。

HPACK 提供了一個(gè)靜態(tài)和動(dòng)態(tài)的 table,靜態(tài) table 定義了通用的 HTTP header fields,譬如 method,path 等。發(fā)送請(qǐng)求的時(shí)候,只要指定 field 在靜態(tài) table 里面的索引,雙方就知道要發(fā)送的 field 是什么了。

對(duì)于動(dòng)態(tài) table,初始化為空,如果兩邊交互之后,發(fā)現(xiàn)有新的 field,就添加到動(dòng)態(tài) table 上面,這樣后面的請(qǐng)求就可以跟靜態(tài) table 一樣,只需要帶上相關(guān)的 index 就可以了。

同時(shí),為了減少數(shù)據(jù)傳輸?shù)拇笮?,使?Huffman 進(jìn)行編碼。這里就不再詳細(xì)說(shuō)明 HPACK 和 Huffman 如何編碼了。

小結(jié)

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

Hello gRPC

gRPC 是 Google 基于 HTTP/2 以及 protobuf 的,要了解 gRPC 協(xié)議,只需要知道 gRPC 是如何在 HTTP/2 上面?zhèn)鬏斁涂梢粤恕?/p>

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

Request

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

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

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

EOS(end-of-stream) 會(huì)在最后的 DATA frame 里面帶上了 END_STREAM 這個(gè) flag。用來(lái)表示 stream 不會(huì)在發(fā)送任何數(shù)據(jù),可以關(guān)閉了。

Response

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

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

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

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

Protobuf

gRPC 的 service 接口是基于 protobuf 定義的,我們可以非常方便的將 service 與 HTTP/2 關(guān)聯(lián)起來(lái)。

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

后記

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

轉(zhuǎn)載至公眾號(hào):pingcap2015

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • gRPC 是一個(gè)高性能、通用的開(kāi)源RPC框架,其由 Google 主要面向移動(dòng)應(yīng)用開(kāi)發(fā)并基于HTTP/2 協(xié)議標(biāo)準(zhǔn)...
    劉琨_10f5閱讀 1,340評(píng)論 4 6
  • 經(jīng)過(guò)很長(zhǎng)一段時(shí)間的開(kāi)發(fā),TiDB 終于發(fā)了 RC3。RC3 版本對(duì)于 TiKV 來(lái)說(shuō)最重要的功能就是支持了 gRP...
    siddontang閱讀 42,174評(píng)論 7 80
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,688評(píng)論 19 139
  • HTTP/2 是一個(gè)二進(jìn)制協(xié)議,這也就意味著它的可讀性幾乎為 0,但幸運(yùn)的是,我們還是有很多工具,譬如 Wires...
    angeChen閱讀 1,511評(píng)論 0 2
  • 文/海豚Lee 圖片/網(wǎng)絡(luò) 全文約3700余字,閱讀時(shí)間約7分鐘 01 最近,我看了一部很有趣的日本電影《佐賀的超...
    海豚的窗戶(hù)閱讀 1,167評(píng)論 0 1

友情鏈接更多精彩內(nèi)容