gRPC是google开源的高性能跨语言的RPC方案。gRPC的设计目标是在任何环境下运行,支持可插拔的负载均衡,跟踪,运行状况检查和身份验证。它不仅支持数据中心内部和跨数据中心的服务调用,它也适用于分布式计算的最后一公里,将设备,移动应用程序和浏览器连接到后端服务。
https://grpc.io/
https://github.com/grpc/grpc
GRPC设计的动机和原则
https://grpc.io/blog/principles
个人觉得官方的文章令人印象深刻的点:
内部有Stubby的框架,但是它不是基于任何一个标准的
支持任意环境使用,支持物联网、手机、浏览器
支持stream和流控
HTTP/1里的header对应HTTP/2里的 HEADERS frame
HTTP/1里的payload对应HTTP/2里的 DATA frame
在Chrome浏览器里,打开chrome://net-internals/#http2,可以看到http2链接的信息。
gRPC over HTTP/2
准确来说gRPC设计上是分层的,底层支持不同的协议,目前gRPC支持:
gRPC over HTTP2
gRPC Web
但是大多数情况下,讨论都是基于gRPC over HTTP2。
简而言之,gGRPC把元数据放到HTTP/2 Headers里,请求参数序列化之后放到 DATA frame里。
基于HTTP/2 协议的优点
HTTP/2 是一个公开的标准
Google本身把这个事情想清楚了,它并没有把内部的Stubby开源,而是选择重新做。现在技术越来越开放,私有协议的空间越来越小。
HTTP/2 是一个经过实践检验的标准
HTTP/2是先有实践再有标准,这个很重要。很多不成功的标准都是先有一大堆厂商讨论出标准后有实现,导致混乱而不可用,比如CORBA。HTTP/2的前身是Google的SPDY,没有Google的实践和推动,可能都不会有HTTP/2。
HTTP/2 天然支持物联网、手机、浏览器
实际上先用上HTTP/2的也是手机和手机浏览器。移动互联网推动了HTTP/2的发展和普及。
基于HTTP/2 多语言客户端实现容易
只讨论协议本身的实现,不考虑序列化。
每个流行的编程语言都会有成熟的HTTP/2 Client
HTTP/2 Client是经过充分测试,可靠的
用Client发送HTTP/2请求的难度远低于用socket发送数据包/解析数据包
HTTP/2支持Stream和流控
在业界,有很多支持stream的方案,比如基于websocket的,或者rsocket。但是这些方案都不是通用的。
HTTP/2里的Stream还可以设置优先级,尽管在rpc里可能用的比较少,但是一些复杂的场景可能会用到。
基于HTTP/2 在Gateway/Proxy很容易支持
nginx对gRPC的支持:https://www.nginx.com/blog/nginx-1-13-10-grpc/
envoy对gRPC的支持:https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/grpc#
HTTP/2 安全性有保证
HTTP/2 天然支持SSL,当然gRPC可以跑在clear text协议(即不加密)上。
很多私有协议的rpc可能自己包装了一层TLS支持,使用起来也非常复杂。开发者是否有足够的安全知识?使用者是否配置对了?运维者是否能正确理解?
HTTP/2 在公有网络上的传输上有保证。比如这个CRIME攻击,私有协议很难保证没有这样子的漏洞。
HTTP/2 鉴权成熟
从HTTP/1发展起来的鉴权系统已经很成熟了,可以无缝用在HTTP/2上
可以从前端到后端完全打通的鉴权,不需要做任何转换适配
比如传统的rpc dubbo,需要写一个dubbo filter,还要考虑把鉴权相关的信息通过thread local传递进去。rpc协议本身也需要支持。总之,非常复杂。实际上绝大部分公司里的rpc都是没有鉴权的,可以随便调。
基于HTTP/2 的缺点
rpc的元数据的传输不够高效
尽管HPAC可以压缩HTTP Header,但是对于rpc来说,确定一个函数调用,可以简化为一个int,只要两端去协商过一次,后面直接查表就可以了,不需要像HPAC那样编码解码。
可以考虑专门对gRPC做一个优化过的HTTP/2解析器,减少一些通用的处理,感觉可以提升性能。
HTTP/2 里一次gRPC调用需要解码两次
一次是HEADERS frame,一次是DATA frame。
HTTP/2 标准本身是只有一个TCP连接,但是实际在gRPC里是会有多个TCP连接,使用时需要注意。
gRPC选择基于HTTP/2,那么它的性能肯定不会是最顶尖的。但是对于rpc来说中庸的qps可以接受,通用和兼容性才是最重要的事情。
官方的benchmark:https://grpc.io/docs/guides/benchmarking.html
https://github.com/hank-whu/rpc-benchmark
Google制定标准的能力
近10年来,Google制定标准的能力越来越强。下面列举一些标准:
HTTP/2
WebP图片格式
WebRTC 网页即时通信
VP9/AV1 视频编码标准
Service Worker/PWA
当然google也并不都会成功,很多事情它想推也失败了,比如Chrome的Native Client。
gRPC目前是k8s生态里的事实标准。 gRPC是否会成为更多地方,更大领域的RPC标准?
为什么会出现gRPC
准确来说为什么会出现基于HTTP/2的RPC?
个人认为一个重要的原因是,在Cloud Native的潮流下,开放互通的需求必然会产生基于HTTP/2的RPC。即使没有gRPC,也会有其它基于HTTP/2的RPC。
gRPC在Google的内部也是先用在Google Cloud Platform和公开的API上:https://opensource.google.com/projects/grpc
尽管gRPC它可能替换不了内部的RPC实现,但是在开放互通的时代,不止在k8s上,gRPC会有越来越多的舞台可以施展。
链接
https://hpbn.co/
https://grpc.io/blog/loadbalancing
https://http2.github.io/faq
https://grpc.io/blog/principles/
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
HTTP/1 协议回顾
HTTP (Hypertext transfer protocol) 超文本传输协议
HTTP 协议在 7 层传输架构中属于应用层协议,其依赖 TCP 协议
HTTP 协议由请求和响应两部分组成,是一个标准的客户端服务器模型
HTTP 默认端口号 80,https 默认端口号 443
HTTP 协议通过 URL(统一资源定位符 - Uniform-Resource-Locator)来定位互联网上的资源地址
HTTP 是一个无状态的面向连接的协议,无状态不代表 HTTP 不能保持长连接,无状态指的是 HTTP 不具备事务记忆能力,也就是下一次的请求不会记住上一次的请求信息。
HTTP 协议 0.9 和 1.0 版本使用非持续性连接,也就是一次请求一次响应,连接就会关闭,而从 HTTP 协议 1.1 开始,默认开启了 Keep-Alive,保持长连接,就是说一次请求结束后,连接不会马上关闭,下一次请求会继续使用这个连接,但长连接不代表一直不关闭,一段时间后这个连接就会关闭。
HTTP 的请求和响应分别都有请求行 / 响应行,首部,实体组成的,
HTTP2 协议分析
什么是 HTTP2
HTTP / 2 并不是对 HTTP 协议的重写,相对于 HTTP / 1,HTTP / 2 的侧重点主要在性能。请求方法,状态码和语义和 HTTP / 1 都是相同的,可以使用与 HTTP / 1.x 相同的 API(可能有一些小的添加)来表示协议。
HTTP / 2 主要有两个规范组成
Hypertext Transfer Protocol version 2 (超文本传输协议版本 2)
HPACK - HTTP / 2 的头压缩 (HPACK 是一种头部压缩算法)
这两个规范的连接如下
https://httpwg.org/specs/rfc7540.html (第一个)
https://httpwg.github.io/specs/rfc7541.htm… (第二个)
HTTP2 中的一些概念
帧:数据通信的最小信息单位
流:存在与 TCP 连接中的一个虚拟通道(双向的,能往过流,也能往回流)
HTTP2 的特性
HTTP / 2 支持 HTTP / 1.1 的所有核心功能,但旨在通过多种方式提高效率
HTTP/2 采用二进制传输数据,而非 HTTP/1 的文本格式传输
HTTP / 2 基本协议单元是帧,比如 head(头部信息)帧,data(传输数据细信息)帧
HTTP / 2 使用流技术支持多路复用,也就是说提供了在单个连接上复用 HTTP 请求和响应的能力, 多个请求或响应可以同时在一个连接上使用流.
HTTP / 2 支持压缩头部帧,允许将多个请求压缩成成一个分组,而且在客户端和服务器端分别头部信息建立索引,相同的表头只需要传输索引就可以。
HTTP / 2 支持对请求划分优先级(就是流的优先级)
HTTP / 2 支持 Server Push 技术
发送 3 个请求,在 HTTP/1 中是按照顺序,一起请求,一次响应,而 HTTP/2 协议可以做到在一个 TCP 连接中并行执行,而不用按照顺序一对一。
HTTP2 的原理
多路复用
HTTP/2 将每一个请求变成流,每一个流都有自己的 ID,有自己的优先级,这些流可以由客户端发送到服务端,也可以由服务端发送到客户端,将数据划分为帧,头部信息为 head 帧,实体信息为 data 帧,最后将这些流乱序发送到一个 TCP 连接中
HTTP/2 中,在一个浏览器同域名下的所有请求都是在单个连接中完成,这个连接可以承载任意数量的双向数据流,每个数据流都以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,根据帧首部的流标识可以将多个帧重新组装成一个流。
在 HTTP/1 中,如果想并发发送多个请求,必须创建多个 TCP 连接,而且浏览器为了减少负载,会对同一域名下的 TCP 连接做限制,这样当请求量比较大时,会引起阻塞
HTTP /1 中客户端往服务端发送请求严格遵守一个请求,一个响应,比如客户端请求展示网页时,服务端发挥 HTML 内容,浏览器解析时发送 css,js 请求,服务端又返回 css,js 文件,那么服务端为什么不能在返回网页时就推送 css,js 内容给客户端呢,在 HTTP /2 中这已功能已经支持,
服务端主动推送也会遵守同源策略,不会随便推送第三方的资源到客户端
如果服务端推送资源是呗客户端缓存过的,客户端是有权力拒绝服务端的推送的,浏览器可以通过发送 RST_STREAM 帧来拒收。
每一个服务端推送的资源都是一个流
头部压缩
HTTP /1 的请求头较大,而且是以纯文本发送,HTTP/2 对消息头进行了压缩,采用的是 HACK 算法,能够节省消息头占用的网络流量,其主要是在两端建立了索引表,消息头在传输时可以采用索引,而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。
HACK 算法可以参考: https://www.zcfy.cc/article/1969
帧的描述
所有帧都以固定的 9 字节头开头,后跟可变长度的有效载荷,组成如下:
长度:帧有效负载的长度表示为无符号的 24 位整数
类型:8 位类型的帧,帧类型确定帧的格式和语义
标志:为特定于帧类型的布尔标志保留的 8 位字段
R:保留的 1 位字段。该位的语义未定义
流标识符:流标识符,表示为无符号 31 位整数,客户端发起流标识符必须时奇数,服务端发起的流标识符必须是偶数
9 字节总共是:9 * 8 = 72 位
上面的描述 24 + 8 + 8 +1 + 31 = 72 位
gRPC 和 HTTP2 的关系
gRPC 设计时的初衷:gRPC 的设计目标是在任何环境下运行,支持可插拔的负载均衡,跟踪,运行状况检查和身份验证。它不仅支持数据中心内部和跨数据中心的服务调用,它也适用于分布式计算的最后一公里,将设备,移动应用程序和浏览器连接到后端服务,同时,它也是高性能的,而 HTTP /2 恰好支持这些。
HTTP /2 天然的通用性满足各种设备,场景
HTTP /2 的性能相对来说也是很好的,除非你需要极致的性能
HTTP /2 的安全性非常好,天然支持 SSL
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”