gRPC-go服务端实现简析
Posted 微保技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gRPC-go服务端实现简析相关的知识,希望对你有一定的参考价值。
gRPC是Google开源的一个RPC(Remote Procedure Calls)网络服务框架,基于HTTP/2协议构建,默认使用PB(Protocol Buffers)进行数据交换。gRPC具有友好的接口定义、可插拔的设计、合理的分层,使用者可以快速的实现健壮、高性能的后台服务。
01
gRPC-go和HTTP/2
gRPC-go是gRPC框架的go语言版本,本文以1.20版本的代码为例,结合HTTP/2协议规范,对其H2 Server的实现细节以及gRPC在是如何在此基础上构建服务模型做一个简要分析。
HTTP/2核心的特性是二进制分帧(Binary Framing)。
二进制分帧重新定义数据在客户端和服务器之间的传输方式,也是HTTP/2性能相比HTTP提升的主要原因。概括如下:
传输数据拆分成更小的粒度:帧(frame)
二进制编码
抽象stream的语义,一次逻辑请求的frame属于同一个streamID
TCP连接上属于不同流的帧可以交错发送,达到多路复用的目的
gRPC-go使用HTTP/2的流来承载RPC调用:
RPC调用与HTTP/2的stream是一一对应的关系(streamID为奇数,超过最大值1<<32 - 1之后,服务器关闭连接)
RPC的请求响应均可由多个HTTP/2的data frame组成
gRPC支持两种形式的RPC调用:
UnaryRPC,即Request-Response模式,适用于大部分请求-响应的业务场景
StreamingRPC,代表的是流式调用(不同于H2的stream),适用于从对端持续读取或向对端持续发送数据的业务场景
type dataFrame struct {
streamID uint32
endStream bool
h []byte
d []byte
// onEachWrite is called every time
// a part of d is written out.
onEachWrite func()
}
gRPC-go的报文内容,存放在dataFrame中。UnaryRPC的报文对应一个dataFrame,StreamingRPC的报文对应一个或多个dataFrame。
一个dataFrame可以被转换成N个HTTP/2的Data帧,所以在HTTP/2的层面并不能有效区分StreamingRPC和UnaryRPC的调用,两者的区别主要是代码实现及使用方式。
ServerTransport是gRPC对服务端网络连接的抽象,handleRawConn会为已连接的客户端实例化http2Server,http2Server与客户端一一对应。
02
初始化
在起始阶段,http2Server会进行HTTP/2传输数据前必要的检查和配置
检查HTTP/2 PREFACE
向客户端发送SETTING帧
处理客户端的SETTING帧并确认
SETTING的主要内容包括:设置并发流个数上限、设置流控的初始窗口大小等。即使使用默认值,也会发送空的SETTING帧,SETTING并不是一个协商的过程。
03
接收数据
HandleStreams在一个协程中运行,通过Framer顺序读取帧并解析。尽管TCP连接上存在交错发送的帧,但是同一stream的帧有严格的顺序定义。新的header frame达到后,帧处理函数会创建一个stream实例并为其分配一个recvbuffer,后续的data frame会写入到对应的recvbuffer中。
type recvBuffer struct {
c chan recvMsg
mu sync.Mutex
backlog []recvMsg
err error
}
每个stream会创建一个goroutinue,阻塞在recv_buffer上等待data frame。得益于goroutinue轻量的创建和销毁,go编写的服务器程序中,这种多协程处理模型较为常见。
04
处理数据
gRPC-go层次清晰,应用层对数据的处理,交由grpcServer处理:
读取HTTP HEADER中的path(helloworld.Greeter/SayHello),并解析服务名和方法名
从注册信息中查找对应服务方法的Handler(注册信息,一般是在程序启动时,执行proto-gen生成的代码初始化)
如果是UnaryRPC,将读取数据交给Handler处理,将返回数据写回
如果是StreamingRPC,将数据读取和写入的操作,交给Handler来执行
05
返回数据
controlBuffer
的命名有些歧义,除了用于控制连接的数据,应用层的数据也存放在该队列。
type controlBuffer struct {
ch chan struct{}
done <-chan struct{}
mu sync.Mutex
consumerWaiting bool
list *itemList
err error
}
loopyWriter同接受请求一样,运行在一个独立的协程里,读取controlBuffer的数据,由Framer组装成帧格式。
非数据帧直接写Framer的buffer
数据帧写入当前流的帧列表后,enqueue activeStreams队列 (FIFO)
每次读取ControlBuf都会触发 dequeue activeStreams队列 ,取出队首的流,并将其一个data帧写入Framer的buffer
如果出队列的流,data帧列表还有数据,将该流重新入队列
Framer的buffer写满会发送数据到对端
activeStreams队列无数据且Framer buffer未写满时,主动让出cpu,等待下次调度时再次尝试读取activeStreams队列,并将数据发送到对端
某一时刻ControlBuf和activeStreams的数据(相同颜色代表同一个流,假设data frame仅对应一个HTTP/2 Data Frame)
数据全部写入Framer Buffer之后
activeStreams主要是为了避免HOL(Head-of-line)Blocking,framer buffer则是为了提升性能,减少系统调用
06
流量控制
流控是一种控制数据发送的机制。HTTP/2是在一个连接实现多路复用,TCP的流控无法精细到stream的数据发送,因此HTTP/2协议定义了流控的语义。
HTTP/2 defines only the format and semantics of the WINDOW_UPDATE frame (Section 6.9). This document does not stipulate how a receiver decides when to send this frame or the value that it sends, nor does it specify how a sender chooses to send packets. Implementations are able to select any algorithm that suits their needs
HTTP/2协议并没有给出具体的流控算法,不同的实现,流控方式都不尽相同。
gRPC-go通过在发送端定义sendQuota(连接配额)、writeQuota(流配额)、bytesOutStanding(流发送计数),在接收端定义trInFlow(连接接收计数)、inFlow(流接收计数)、bdpEstimator(带宽时延积评估)等来实现连接及stream的流量控制。
静态窗口
静态窗口是指,连接和流的窗口大小保持不变。在用户自定义窗口大小时生效(需大于默认值64KB)。下图是发送端的视图,以100KB的静态窗口为例。sendQuota对连接进行流量控制,sendQuota为0时,loopyWriter停止发送数据;stream上的流控则通过窗口与bytesOutStanding的差值来控制,当差值为0时,当前流不再发送数据。gRPC限制了单个HTTP/2数据帧的大小为16KB,一个data frame对应一次调用的完整报文(最大4M),会被拆分成N个数据帧,其对应的首个数据帧会包含报文的长度字段(payload len)。
接收端采用如下策略更新发送端的配额:
1、连接接收数据超过窗口大小四分之一时,向发送端发送窗口更新,增加sendQuota。以上图为例,在接收端收到第二个数据帧后,10KB+16KB > 100/4KB,WINDOW_UPDATE(streamID=0,incr=26KB)
2、使用payload len,计算发送端未发送的数据量,如果大于当前流剩余配额,发送窗口更新,以增加当前流的配额。以上图为例,在接收端收到第二个数据帧后,stream3的estUntransmittedData=200KB-16KB=184KB,estSenderQuota=100KB-16KB=84KB,WINDOW_UPDATE(streamID=3,incr=200KB)
3、当前流累计接收数据超过流窗口大小四分之一时,向发送端发送窗口更新,以增加当前流的配额。以上图为例,在接收端收到第三个数据帧后,stream1 累计接收10KB+16KB > 100/4KB,WINDOW_UPDATE(streamID=1,incr=26KB)
4、如果已经发生 2 中的调整,则不再进行3中的调整
动态窗口
动态窗口是指gRPC-go会在读取数据帧时,通过特殊的PING帧来计算连接的BDP(带宽时延积),不断增加窗口大小。该特性在使用默认窗口大小(64KB)时开启。
更新发送端配额的策略稍有不同:
特殊PING帧发送前发送WINDOW_UPDATE (streamID=当前流ID,incr=本次数据大小)
根据BDP计算出的新窗口大小,通过WINDOW_UPDATE和SETTING帧通知对端
07
结语
gRPC-go代码结构清晰,服务模型易于理解,其中数据结构设计以及性能提升的方式方法,比较有启发。文末附上作者的留的彩蛋:what does the ping message say?
// Adding arbitrary data to ping so that its ack can be identified.
// Easter-egg: what does the ping message say?
var bdpPing = &ping{data: [8]byte{2, 4, 16, 16, 9, 14, 7, 7}}
参考资料:
https://developers.google.com/web/fundamentals/performance/http2/
https://grpc.io/blog/2017-08-22-grpc-go-perf-improvements/
https://grpc.io/blog/grpc_on_http2/
后台留言功能已开启,欢迎留言吐槽哦~
or
长按扫码可关注
以上是关于gRPC-go服务端实现简析的主要内容,如果未能解决你的问题,请参考以下文章