gRPC 是基于 HTTP/2 協(xié)議的捷雕,要深刻理解 gRPC衔峰,理解下 HTTP/2 是必要的悼瓮。本篇文章會(huì)先簡單介紹一下 HTTP/2 相關(guān)的知識(shí)徊哑,然后再介紹下 gRPC 是如何基于 HTTP/2 構(gòu)建的笛钝。
HTTP/1.x
HTTP 協(xié)議可以算是現(xiàn)階段 Web 上面最通用的協(xié)議了质况,在之前很長一段時(shí)間愕宋,很多應(yīng)用都是基于 HTTP/1.x 協(xié)議,HTTP/1.x 協(xié)議是一個(gè)文本協(xié)議结榄,可讀性非常好中贝,但其實(shí)并不高效,筆者主要碰到過幾個(gè)問題:
parser
如果要解析一個(gè)完整的 HTTP 請求臼朗,首先我們需要能正確的讀出 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 請求給解析出來跪呈,雖然在代碼實(shí)現(xiàn)上面段磨,有很多優(yōu)化方式,譬如:
- 一次將一大塊數(shù)據(jù)讀取到 buffer 里面避免多次 IO read
- 讀取的時(shí)候直接匹配 \r\n 的方式流式解析
但上面的方式對于高性能服務(wù)來說耗绿,終歸還是會(huì)有開銷苹支。其實(shí)最主要的問題在于,HTTP/1.x 的協(xié)議是 文本協(xié)議缭乘,是給人看的沐序,對機(jī)器不友好琉用,如果要對機(jī)器友好堕绩,二進(jìn)制協(xié)議才是更好的選擇。
如果大家對解析 HTTP/1.x 很感興趣邑时,可以研究下 http-parser奴紧,一個(gè)非常高效小巧的 C library,見過不少框架都是集成了這個(gè)庫來處理 HTTP/1.x 的晶丘。
Request/Response
HTTP/1.x 另一個(gè)問題就在于它的交互模式黍氮,一個(gè)連接每次只能一問一答,也就是 client 發(fā)送了 request 之后浅浮,必須等到 response沫浆,才能繼續(xù)發(fā)送下一次請求。
這套機(jī)制是非常簡單滚秩,但會(huì)造成網(wǎng)絡(luò)連接利用率不高专执。如果需要同時(shí)進(jìn)行大量的交互,client 需要跟 server 建立多條連接郁油,但連接的建立也是有開銷的本股,所以為了性能攀痊,通常這些連接都是長連接一直保活的拄显,雖然對于 server 來說同時(shí)處理百萬連接也沒啥太大的挑戰(zhàn)苟径,但終歸效率不高。
Push
用 HTTP/1.x 做過推送的同學(xué)躬审,大概就知道有多么的痛苦棘街,因?yàn)?HTTP/1.x 并沒有推送機(jī)制。所以通常兩種做法:
- Long polling 方式盒件,也就是直接給 server 掛一個(gè)連接蹬碧,等待一段時(shí)間(譬如 1 分鐘),如果 server 有返回或者超時(shí)炒刁,則再次重新 poll恩沽。
- Web-socket,通過 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ā)顯得捉緊見拙,我們急需另一套更好的協(xié)議來構(gòu)建我們的服務(wù),于是就有了 HTTP/2蜒灰。
HTTP/2 是一個(gè)二進(jìn)制協(xié)議弦蹂,這也就意味著它的可讀性幾乎為 0,但幸運(yùn)的是强窖,我們還是有很多工具凸椿,譬如 Wireshark, 能夠?qū)⑵浣馕龀鰜怼?/p>
在了解 HTTP/2 之前翅溺,需要知道一些通用術(shù)語:
- Stream: 一個(gè)雙向流脑漫,一條連接可以有多個(gè) streams。
- Message: 也就是邏輯上面的 request咙崎,response优幸。
- Frame::數(shù)據(jù)傳輸?shù)淖钚挝弧C總€(gè) Frame 都屬于一個(gè)特定的 stream 或者整個(gè)連接褪猛。一個(gè) message 可能有多個(gè) frame 組成网杆。
Frame Format
Frame 是 HTTP/2 里面最小的數(shù)據(jù)傳輸單位,一個(gè) Frame 定義如下:
- Length:也就是 Frame 的長度,默認(rèn)最大長度是 16KB跛璧,如果要發(fā)送更大的 Frame严里,需要顯式的設(shè)置 max frame size。
- Type:Frame 的類型追城,譬如有 DATA刹碾,HEADERS,PRIORITY 等座柱。
- Flag 和 R:保留位迷帜,可以先不管。
- Stream Identifier:標(biāo)識(shí)所屬的 stream色洞,如果為 0戏锹,則表示這個(gè) frame 屬于整條連接。
- Frame Payload:根據(jù)不同 Type 有不同的格式火诸。
可以看到锦针,F(xiàn)rame 的格式定義還是非常的簡單,按照官方協(xié)議置蜀,可以非常方便的寫一個(gè)出來奈搜。
Multiplexing
HTTP/2 通過 stream 支持了連接的多路復(fù)用,提高了連接的利用率盯荤。Stream 有很多重要特性:
- 一條連接可以包含多個(gè) streams馋吗,多個(gè) streams 發(fā)送的數(shù)據(jù)互相不影響。
- Stream 可以被 client 和 server 單方面或者共享使用秋秤。
- Stream 可以被任意一段關(guān)閉宏粤。
- Stream 會(huì)確定好發(fā)送 frame 的順序,另一端會(huì)按照接受到的順序來處理灼卢。
- Stream 用一個(gè)唯一 ID 來標(biāo)識(shí)绍哎。
這里在說一下 Stream ID,如果是 client 創(chuàng)建的 stream芥玉,ID 就是奇數(shù)蛇摸,如果是 server 創(chuàng)建的备图,ID 就是偶數(shù)灿巧。ID 0x00 和 0x01 都有特定的使用場景。
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 里面,我們就遇到過這個(gè)值比較小零院,整體吞吐上不去的問題溉跃。
這里還需要注意,雖然一條連接上面能夠處理更多的請求了告抄,但一條連接遠(yuǎn)遠(yuǎn)是不夠的撰茎。一條連接通常只有一個(gè)線程來處理,所以并不能充分利用服務(wù)器多核的優(yōu)勢打洼。同時(shí)龄糊,每個(gè)請求編解碼還是有開銷的,所以用一條連接還是會(huì)出現(xiàn)瓶頸募疮。
在 TiKV 有一個(gè)版本中炫惩,我們就過分相信一條連接跑多 streams 這種方式?jīng)]有問題,就讓 client 只用一條連接跟 TiKV 交互阿浓,結(jié)果發(fā)現(xiàn)性能完全沒法用他嚷,不光處理連接的線程 CPU 跑滿,整體的性能也上不去芭毙,后來我們換成了多條連接爸舒,情況才好轉(zhuǎn)。
Priority
因?yàn)橐粭l連接允許多個(gè) streams 在上面發(fā)送 frame稿蹲,那么在一些場景下面扭勉,我們還是希望 stream 有優(yōu)先級(jí),方便對端為不同的請求分配不同的資源苛聘。譬如對于一個(gè) Web 站點(diǎn)來說涂炎,優(yōu)先加載重要的資源,而對于一些不那么重要的圖片啥的设哗,則使用低的優(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)樘κ秩ぃ蛘邏毫μ笊胃茫蛘咧幌虢o特定的 stream 分配資源,receiver 端就可能不想處理這些數(shù)據(jù)。譬如朝群,如果 client 給 server 請求了一個(gè)視頻燕耿,但這時(shí)候用戶暫停觀看了,client 就可能告訴 server 別在發(fā)送數(shù)據(jù)了姜胖。
雖然 TCP 也有 flow control缸棵,但它僅僅只對一個(gè)連接有效果。HTTP/2 在一條連接上面會(huì)有多個(gè) streams谭期,有時(shí)候堵第,我們僅僅只想對一些 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 連接建立起來之后凄诞,client 和 server 會(huì)交換 SETTINGS frames圆雁,用來設(shè)置 flow control window size。
- Flow control 是 hop-by-hop帆谍,并不是 end-to-end 的伪朽,也就是我們可以用一個(gè)中間人來進(jìn)行 flow control。
這里需要注意汛蝙,HTTP/2 默認(rèn)的 window size 是 64 KB烈涮,實(shí)際這個(gè)值太小了,在 TiKV 里面我們直接設(shè)置成 1 GB窖剑。
HPACK
在一個(gè) HTTP 請求里面坚洽,我們通常在 header 上面攜帶很多該請求的元信息,用來描述要傳輸?shù)馁Y源以及它的相關(guān)屬性西土。在 HTTP/1.x 時(shí)代讶舰,我們采用純文本協(xié)議,并且使用\r\n
來分隔需了,如果我們要傳輸?shù)脑獢?shù)據(jù)很多跳昼,就會(huì)導(dǎo)致 header 非常的龐大。另外援所,多數(shù)時(shí)候庐舟,在一條連接上面的多數(shù)請求欣除,其實(shí) header 差不了多少住拭,譬如我們第一個(gè)請求可能GET /a.txt
后面緊接著是GET /b.txt
兩個(gè)請求唯一的區(qū)別就是 URL path 不一樣,但我們?nèi)匀灰獙⑵渌械?fields 完全發(fā)一遍。
HTTP/2 為了結(jié)果這個(gè)問題滔岳,使用了 HPACK杠娱。雖然 HPACK 的 RFC 文檔看起來比較恐怖,但其實(shí)原理非常的簡單易懂谱煤。
HPACK 提供了一個(gè)靜態(tài)和動(dòng)態(tài)的 table摊求,靜態(tài) table 定義了通用的 HTTP header fields,譬如 method刘离,path 等室叉。發(fā)送請求的時(shí)候,只要指定 field 在靜態(tài) table 里面的索引硫惕,雙方就知道要發(fā)送的 field 是什么了茧痕。
對于動(dòng)態(tài) table,初始化為空恼除,如果兩邊交互之后踪旷,發(fā)現(xiàn)有新的 field,就添加到動(dòng)態(tài) table 上面豁辉,這樣后面的請求就可以跟靜態(tài) table 一樣令野,只需要帶上相關(guān)的 index 就可以了。
同時(shí)徽级,為了減少數(shù)據(jù)傳輸?shù)拇笮∑疲褂?Huffman 進(jìn)行編碼。這里就不再詳細(xì)說明 HPACK 和 Huffman 如何編碼了餐抢。
小結(jié)
上面只是大概列舉了一些 HTTP/2 的特性堵幽,還有一些,譬如 push弹澎,以及不同的 frame 定義等都沒有提及朴下,大家感興趣,可以自行參考 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报强,對于底層 HTTP/2 來說灸姊,它們都是 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-
開頭尝苇,因?yàn)檫@是為 gRPC 后續(xù)自己保留的。
Length-Prefixed-Message 主要在 DATA frame 里面派發(fā)埠胖,它有一個(gè) Compressed flag 用來表示該 message 是否壓縮糠溜,如果為 1,表示該 message 采用了壓縮直撤,而壓縮算啊定義在 header 里面的 Message-Encoding 里面诵冒。然后后面跟著四字節(jié)的 message length 以及實(shí)際的 message。
EOS(end-of-stream) 會(huì)在最后的 DATA frame 里面帶上了 END_STREAM
這個(gè) flag谊惭。用來表示 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)起來浪规。
- 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 協(xié)議的簡單理解或听,可以看到,gRPC 的基石就是 HTTP/2罗丰,然后在上面使用 protobuf 協(xié)議定義好 service RPC神帅。雖然看起來很簡單再姑,但如果一門語言沒有 HTTP/2萌抵,protobuf 等支持,要支持 gRPC 就是一件非常困難的事情了元镀。
轉(zhuǎn)載至公眾號(hào):pingcap2015