RC3 版本對於 TiKV 來講最重要的功能就是支持了 gRPC,也就意味着後面你們能夠很是方便的使用本身喜歡的語音對接 TiKV 了。html
gRPC 是基於 HTTP/2 協議的,要深入理解 gRPC,理解下 HTTP/2 是必要的,這裏先簡單介紹一下 HTTP/2 相關的知識,而後在介紹下 gRPC 是如何基於 HTTP/2 構建的。node
HTTP 協議能夠算是現階段 Web 上面最通用的協議了,在以前很長一段時間,不少應用都是基於 HTTP/1.x 協議,HTTP/1.x 協議是一個文本協議,可讀性很是好,但其實並不高效,筆者主要碰到過幾個問題:git
若是要解析一個完整的 HTTP 請求,首先咱們須要能正確的讀出 HTTP header。HTTP header 各個 fields 使用 \r\n
分隔,而後跟 body 之間使用 \r\n\r\n
分隔。解析完 header 以後,咱們才能從 header 裏面的 content-length
拿到 body 的 size,從而讀取 body。github
這套流程其實並不高效,由於咱們須要讀取屢次,才能將一個完整的 HTTP 請求給解析出來,雖然在代碼實現上面,有不少優化方式,譬如:web
\r\n
的方式流式解析但上面的方式對於高性能服務來講,終歸仍是會有開銷。其實最主要的問題在於,HTTP/1.x 的協議是 文本協議,是給人看的,對機器不友好,若是要對機器友好,二進制協議纔是更好的選擇。bash
若是你們對解析 HTTP/1.x 很感興趣,能夠研究下 http-parser,一個很是高效小巧的 C library,見過很多框架都是集成了這個庫來處理 HTTP/1.x 的。服務器
HTTP/1.x 另外一個問題就在於它的交互模式,一個鏈接每次只能一問一答,也就是client 發送了 request 以後,必須等到 response,才能繼續發送下一次請求。網絡
這套機制是很是簡單,但會形成網絡鏈接利用率不高。若是須要同時進行大量的交互,client 須要跟 server 創建多條鏈接,但鏈接的創建也是有開銷的,因此爲了性能,一般這些鏈接都是長鏈接一直保活的,雖然對於 server 來講同時處理百萬鏈接也沒啥太大的挑戰,但終歸效率不高。併發
用 HTTP/1.x 作過推送的同窗,大概就知道有多麼的痛苦,由於 HTTP/1.x 並無推送機制。因此一般兩種作法:app
相比 Long polling,筆者仍是更喜歡 web-socket 一點,畢竟更加高效,只是 web-socket 後面的交互並非傳統意義上面的 HTTP 了。
雖然 HTTP/1.x 協議可能仍然是當今互聯網運用最普遍的協議,但隨着 Web 服務規模的不斷擴大,HTTP/1.x 愈加顯得捉襟見肘,咱們急需另外一套更好的協議來構建咱們的服務,因而就有了 HTTP/2。
HTTP/2 是一個二進制協議,這也就意味着它的可讀性幾乎爲 0,但幸運的是,咱們仍是有不少工具,譬如 Wireshark, 可以將其解析出來。
在瞭解 HTTP/2 以前,須要知道一些通用術語:
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 的格式定義仍是很是的簡單,按照官方協議,同意能夠很是方便的寫一個出來。
HTTP/2 經過 stream 支持了鏈接的多路複用,提升了鏈接的利用率。Stream 有不少重要特性:
這裏在說一下 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 跑滿,總體的性能也上不去,後來咱們換成了多條鏈接,狀況纔好轉。
由於一條鏈接容許多個 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。
HTTP/2 也支持流控,若是 sender 端發送數據太快,receiver 端可能由於太忙,或者壓力太大,或者只想給特定的 stream 分配資源,receiver 端就可能不想處理這些數據。譬如,若是 client 給 server 請求了一個視屏,但這時候用戶暫停觀看了,client 就可能告訴 server 別在發送數據了。
雖然 TCP 也有 flow control,但它僅僅只對一個鏈接有效果。HTTP/2 在一條鏈接上面會有多個 streams,有時候,咱們僅僅只想對一些 stream 進行控制,因此 HTTP/2 單獨提供了流控機制。Flow control 有以下特性:
這裏須要注意,HTTP/2 默認的 window size 是 64 KB,實際這個值過小了,在 TiKV 裏面咱們直接設置成 1 GB。
在一個 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 文檔。
gRPC 是 Google 基於 HTTP/2 以及 protobuf 的,要了解 gRPC 協議,只須要知道 gRPC 是如何在 HTTP/2 上面傳輸就能夠了。
gRPC 一般有四種模式,unary,client streaming,server streaming 以及 bidirectional streaming,對於底層 HTTP/2 來講,它們都是 stream,而且仍然是一套 request + response 模型。
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 關聯起來。
/Service-Name/{method name}
?( {proto package name} "." ) {service name}
{fully qualified proto message name}
上面只是對 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: github.com/pingcap/grp…
做者:唐劉