因为项目中使用grpc作为分布式框架,因此最近花点时间来学习grpc的原理,对于grpc的使用本文不进行赘述,不了解的同学可以去官网或者博客了解一下使用方法,此次只是针对自己的盲点进行梳理
标位红色的是还未确认,后续会更新或补充文档
后续会进行源码级别的学习和探讨
grpc本质
grpc本质上就是request-response模式,大致上跟一个http请求,虽然表面上很复杂,如:远程调用,序列化等,但本质上就是client通过封装参数向server端发起请求,server端返回数据,client端再解析的过程
client-server交互模式
有四种交互模式
- Simple RPC 最常见的一种,一个reqeust,一个response
- Server-Streaming RPC, client一次请求,server端分多次返回
- Client-Streaming RPC client多条请求数据,server端一次返回
- Bidirectional-Streamin RPC client多条请求,server端对应多条返回
一般情况下用最简单的Simple RPC模式就行
client to server的请求响应流程
流程图入上图说是,其实流程很简单,就是client将要调用的信息encode一下,将要调用的方法放在http post的header中,然后发送http请求到server端,server端docode,并且解码后调用具体的方法,再返回给client
其中编码解码,用的是protobuf
参数是放在Body中还是header中呢
protobuf编码
grpc使用protocol buffer将整个消息进行编码,编码后的整个格式如下
- 编码之后,第一个字节表示是否是压缩的标志位,这个作用在下文http2中再讲
- compressed flag之后是4个字节表示后面的消息的总共的大小,4个字节可以表示的数字最大为4GB,也就是表示grpc最大能处理4GB的数据
- 再之后就是接口定义的message的编码
message编码的结构
接口message的编码如下所示,tag,value作为一个整体表示我们在proto定义的以序号排列的元素信息,比如int, string型,以及嵌套的message等
tag
tag可以看做是index和wiretype的结合,index就是指在proto定义的成员的序号,wireType是指根据规则,将成员的数据类型映射到一个值
最终,tag的计算公式为:
Tag value = (field_index << 3) | wire_type, 也就是后3bit表示wire_type
value
针对不同的wireType对应的数据类型,数据的编码方式也有所差异,具体可以看https://blog.csdn.net/weikangc/article/details/51027818以及proto官网,值得一提的是,如果是string结构,由于长度不固定,那么value中刚开始一个字节表示的是string的长度
所以说,其实真正的message的结构是
Tag | Length(可选)| value的格式
protobuf高性能的原因
上面一小节说到value根据不同的类型会有不同的编码方式,正是这些编码方式,让protof编码比正常的编码更节省空间,另外其编码解码方式效率也高,protobuf性能比json和xml要高,但是json使用更广泛,并且可读性更高
空间占用小
-
字段占用方面
-
Variant
Variant在protobuf中用来表示数字,int32,int64等都是用variant来进行编码,在一般的变成语言中,int32占用32位,也就是4个字节,但是Variant可以动态地减少字节数,对于Variant表示的数字,每个字节的最高位表示该字节是否是该数字的最后一个字节,那么比如数字很小,小于等于一个字节,那么这个数字就可以只占用一个字节,最左边的bit用0表示,如果是多个字节,那么最后一个字节的最高位是0,其余字节的最高位是1,解析时通过移动每个字节的位数来重新计算这些字节表示的真正的大小就行
当然,如果数字比较大,那么可能占用的字节数要比平常要大,但一般情况下,数字不会很大
因为负数的最高位总是1,因此,protobuf使用Zigzag编码将有符号数转化为无符号数,再使用Variant
-
string, 用一个length表示value的长度
-
-
排列更加紧凑,用tag|length|value形式紧密排列,而不需要像xml那样用一些负责的结构来包裹
编码解码
编码解码只需要进行简单的计算,比如位移等,而xml这种,由于其要转成文档结构模型,那么消耗的CPU计算更加复杂
http2
htt2相当于http1有了4个重要的改进
- 采用二进制分帧进行传输,原来是文本传输,但是文本比二进制浪费空间(表面上看,所有文件最终的底层都是二进制,但是不要混淆了概念,就拿http传输中的一个数字来说,65536, 这个文本的asciii码是36 35 35 33 36,或者说用二进制表示是00000110 00000101 00000101 00000011 00000110,要占用8个字节,但是如果直接用二进制的话,直接用10000000000000000,也就是2个字节就能表示), 一个请求如果数据量太大,可能会被拆成多帧
二进制还是有点疑问,上面举的例子未必就是对的,这个数字未必就是int型?
- 多路复用,同域名下所有通信都在单个连接上进行,只需要占用一个tcp连接,单个连接上交错的请求和响应互不干扰
- 服务器推送,服务器可以在发送html时主动推送其它资源
- 头部压缩,客户端和服务器端的多次请求,每次请求会发送与之前不一样的header的信息,如果一样,不再冗余的发送,在http2中用一个首部表来进行维护
wireshark抓包grpc请求
打开wireshark后,进行一次grpc请求操作,然后发现,只有tcp协议,没有http2协议
这时候需要进行如下操作:
1、设置解析proto文件,可以尽可能看到明文
2、鼠标停在一个下面内容有data的tcp中,右键data,current选中http2
3、然后就会看到解析除了http2
如下所示,标红的就是context中的metadata,果然出现在了header中
grpc中的request/response
request
grpc请求中request信息包含三个部分如上图所示,request headers, length-prefixed-message, end of stream flag
三个部分的请求是按照顺序的
1、首先,发送request headers, 初始化一个请求,内容如下:
2、length-prefixed-message,也就是上文中所说的编码的数据信息,如果一帧传不完,会分多帧进行传说
3、end of stream flag,标识request数据已经传输完毕
response
response信息入上图所示,也是分成三个部分,前两个部分类似,最后一个部分trailers,不仅标识数据传输结束,还返回了grpc status
有些场景下,比如快速失败,或者业务失败,那么只需要返回trailer就行,trailer信息中加上response headers中的一些信息,比如http status, content-type等
交互模式对应的数据流
前面提到过,client-server有多种交互模式,simple, client-server stream等,它们的request-response消息流是什么样的呢?
前三种模式基本差不多,当client端发送完,end of stream flag之后,client端会half-close connection,也就是client端无法再向server端发送数据,server端开始传输数据
只有最后一个bi-directional-stream模式不同,client端会传输多个请求,而server端也会同时返回数据,而不会等到client端完毕后才开是response
grpc整体架构
grcp整体架构如上图所示,最底层是封装了http/ssh网络层的处理逻辑,中间层是核心层,抽象了通用的方法,比如序列化,网络请求等,那么最上层可以选择不同的语言,c/c++/java等生成不同的上层接口调用方式
grpc特性
Interceptors
grpc有client端和server端的interceptor接口供开发者实现进行动态扩展
client端的interceptor接口如:
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
apc项目中monitor_interceptor.go中就实现了该接口,用于计算调用其他服务的接口耗时以及上报prometheus
server端的interceptor接口如:
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
Deadline
通过调用context.WithDeadline方法或者context.WithTimeout方法可以设置超时时间,并且这个context具有传递性,比如在server端续用了这个context调用了另一个服务,那么如果这个context超时了调用该服务也会失效
根据我的理解,context就是上下文的意思,也携带一些信息,grpc获取这个context时会解析超时时间,最终进行http请求的超时设置
Cancel
grpc提供cancel功能,终止grpc调用
用法如下:
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
这个cancel是个方法,通过调用Cancel可以取消
需要了解下context的机制,为什么调用context的cancel能够取消
错误码
grpc已经有默认的错误码,开发者可以自定用,并通过调用status.Err等方法返回给client端
Metadata
matadata可以看做是一个map,存放键值对,并通过调用metadata.NewOutgoingContext或者AppendToOutgoingContext将md放在context中,context中key为mdOutgoingKey struct,value为md,通过调用metadata.FromIncomingContext可以取出
在grpc的请求中,metadata是存放在http请求中的header中
Load Balancing
微服务中,一个服务一般会有多个实例部署,那么其他服务(或中间件)会请求到不同的实例中,这时候就需要进行负载均衡,让client端能够通过不同的算法进行请求到不同实例中,因此grpc提供了两种负载均衡模式
Load Balancer Proxy
负载均衡代理,有一个专门的负载均衡进程,所有微服务不需要自己维护其他服务地址,只需要向proxy注册自己的信息,那么由proxy来维护服务地址列表,当client向微服务发起请求时,实则是向proxy发起请求,由proxy根据自己的负责均衡算法转发请求到指定的服务实例。
这种方式下,client端不用开发和维护其他微服务地址列表,但是如果调用量很大,proxy就是瓶颈,LB发生故障,将影响整个系统
Client-Side Load Balancing
这种模式下,负载均衡算法被内置在每个微服务中,每个微服务启动时向注册中心注册自己的信息,同时监听/定时获取注册中心中其他服务的地址列表,当请求其他微服务时通过内置的负载均衡算法从内存中的服务地址列表中选取合适的地址进行请求
grpc生态
有很多grpc的插件能够满足日常的需求,虽然这些插件不属于grpc,但是属于grpc的生态,可以看做是一个个插件。比较有用的有:
grpc gateway, http/json transcoding, grpc server reflection, grpc middleware, healthcheck等,具体的功能一看名字就一目了然,具体可以自行按需查询下