gRPC Java 服务端实现简析
Posted 微保技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gRPC Java 服务端实现简析相关的知识,希望对你有一定的参考价值。
编辑:edwinzeng 曾鑫鹏
01
背景
gRPC 是 Google 出品的远程调用解决方案,默认使用Protocol Buffers协议,提供高性能保障,使得我们可以快速的搭建起分布式应用。其跨平台的特性,受到了多语言应用环境的青睐。本文来自微保在大规模 gRPC 实践过程中,对 gRPC Java服务端实现原理的一些研究。
02
gRPC Java 源码解读
gRPC网络通信是基于标准的HTTP/2协议,gRPC Java服务端网络通信层的实现上并没有自己造轮子,而是完全基于 Netty 框架。但是 gRPC 对概念进行了多层的抽象,为了深入理解原理,我们需要先了解其对各种概念抽象的方式。
2.1 核心类抽象
我们首先将最核心的概念抽象出来,看看gRPC Java服务端整体的模样。
通过核心类图,可以大体看到gRPC 实现中的一些概念。
ServerBuilder,这是gRPC暴露给业务层的启动入口,通过这个入口设置端口号和对外提供服务的实现类,构造一个gRPC服务并启动。
Server,gRPC 服务中最顶层的服务抽象,有start启动和shutdown关闭两个核心动作。
InternalServer,gRPC真正完成通信动作的内部服务抽象。
ServerTransport,InternalServer 内部依赖的通信窗口。
NettyServerHandler,向Netty注册的处理器,是真正的核心消息接收逻辑的处理者。
ServerStream,信道流,每一个请求会被识别为一个独立的Stream。
TransportState,通信状态标识,用来标识信道流的处理情况,承担实际的请求接收,解码分发工作。
ServerCall,服务调用抽象,在收到Body请求以后真正被触发,发起本地服务调用。
2.2 启动和初始化流程
启动一个gRPC服务是一件非常简单的事情,官方给出了范例:
io.grpc.examples.helloworld.HelloWorldServer#start
启动服务的入口类是ServerBuilder类,通过这个类设置好端口和本地方法就可以将服务运行起来。
为了深入了解框架内部的初始化流程,我们通过一个时序图来展现:
可以看到,初始化的流程最重要的步骤,就是设定好相关参数以后,将自己注册给Netty。
io.grpc.netty.NettyServerTransport#start
Netty是底层的通信框架,gRPC的实现并不直接处理Http/2协议层的细节。
gRPC的线程机制上有很多Netty线程池和业务线程池之间的切换,所以需要有非常多的监听器注册。时序上省略这些注册链路,实际上这部分依赖关系很复杂。后文介绍线程模型时会有所涉及。
2.3 Header消息处理
经过了上一个步骤的初始化,服务已经启动,端口也开始监听消息。由于协议的特性,Header消息的处理和Body消息是分开的,我们先看Header消息的处理流程。
FrameListener是NettyServerHandler在初始化流程中向Netty注册的一个解码器。当Header消息到来时,Netty的处理线程会触发FrameListener的回调。Header消息是一个请求的开始,接收到Header消息后会初始化当前这个流,并且设置好Metadata。
io.grpc.netty.NettyServerHandler#start
2.4 Body消息处理
Header消息处理完毕后,已经设置好路由信息,等到Body 消息传输完毕以后,会触发对本地服务的调用。
请求接收完毕后,会设置标识endOfStream, 关闭解码器,会触发到真正的本地调用。本地调用会切换到业务层的线程池进行。
03
gRPC 线程模型
gRPC的线程模型设计,遵循一个基本原则:除了传输过程中的监听及解包相关流程,其他的逻辑处理都会放在业务线程池中。比如序列化与反序列化,拦截器逻辑,本地方法调用。这个设计符合Netty的线程模型实践规范,最大化的保障传输框架的性能,提高服务资源利用率。gRPC 框架向业务层暴露了两个入口,一个是拦截器,在进入本地方法调用前拦截请求,用于处理一些前置逻辑;另一个就是本地服务。为了更清晰的表达业务线程池和Netty I/O 线程池的分工,我们用一个流程图来示意:
ServerBuilder提供了基础方法让我们能够在启动的时候注册业务线程池,并且自定义我们想要的线程池大小,扩展策略,拒绝策略。
一个自定义线程池的示例
另外一个值得注意的点,由于HTTP/2在协议层将Header和Body做了分帧传输,所以gRPC服务端接收逻辑和后续的异步处理逻辑也是分开的,这造成gRPC向业务开放的拦截器和本地服务极有可能会在不同的线程中运行,这里和我们常规的开发设计思路并不一致。所以尝试使用ThreadLocal进行数据变量存储和传递会在多并发条件下出现偶发性问题,非常不建议在实践中这样使用。
04
流控机制
gRPC在通信层是完全基于HTTP/2的,其流控机制也遵从HTTP/2的协议定义。我们先来了解一下HTTP/2中流控的原则:
HTTP/2 协议层对于流控的设计,是旨在不改变协议实现的基础上支持多样性的流控算法。简单的描述即为:服务端通过WINDOW_UPDATE帧动态的告知每一个信道的发送端窗口大小,如果窗口大小不够大,则客户端应该停止发送,从而达到控制流量的目的。
在gRPC服务端的实现中,有两个关键逻辑来实现流控:
io.grpc.netty.FlowControlPinger#onDataRead
io.grpc.netty.FlowControlPinger#updateWindow
gRPC服务端收到Body消息后会记录下自上一次发送Ping以后的消息长度,当收到Ping以后会触发updateWindow方法更新窗口大小。这是一个逐渐增加窗口过程。
05
总结
gRPC 是一款优秀的RPC框架,使我们能够快速搭建起分布式的远程调用系统。其优秀的性能表现和跨平台的特性是相较其他解决方案的优势,也是在微保大规模投入使用的原因。但gRPC官方仅仅只是想提供一个远程调用的框架,为了在工业化的分布式环境下使用,至少还需要自行集成一套服务注册与发现的方案。这一点相比业内目前流行的解决方案是一个劣势,也是被诟病的地方。
以上是关于gRPC Java 服务端实现简析的主要内容,如果未能解决你的问题,请参考以下文章