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…
做者:唐刘