vlambda博客
学习文章列表

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

gRPC Java 服务端实现简析


启动服务的入口类是ServerBuilder类,通过这个类设置好端口和本地方法就可以将服务运行起来。

为了深入了解框架内部的初始化流程,我们通过一个时序图来展现:

gRPC Java 服务端实现简析

可以看到,初始化的流程最重要的步骤,就是设定好相关参数以后,将自己注册给Netty。

io.grpc.netty.NettyServerTransport#start

gRPC Java 服务端实现简析


Netty是底层的通信框架,gRPC的实现并不直接处理Http/2协议层的细节。

gRPC的线程机制上有很多Netty线程池和业务线程池之间的切换,所以需要有非常多的监听器注册。时序上省略这些注册链路,实际上这部分依赖关系很复杂。后文介绍线程模型时会有所涉及。


2.3 Header消息处理

经过了上一个步骤的初始化,服务已经启动,端口也开始监听消息。由于协议的特性,Header消息的处理和Body消息是分开的,我们先看Header消息的处理流程。

gRPC Java 服务端实现简析

FrameListener是NettyServerHandler在初始化流程中向Netty注册的一个解码器。当Header消息到来时,Netty的处理线程会触发FrameListener的回调。Header消息是一个请求的开始,接收到Header消息后会初始化当前这个流,并且设置好Metadata。

io.grpc.netty.NettyServerHandler#start

gRPC Java 服务端实现简析


2.4 Body消息处理

Header消息处理完毕后,已经设置好路由信息,等到Body 消息传输完毕以后,会触发对本地服务的调用。

gRPC Java 服务端实现简析

请求接收完毕后,会设置标识endOfStream, 关闭解码器,会触发到真正的本地调用。本地调用会切换到业务层的线程池进行。

03

gRPC 线程模型

gRPC的线程模型设计,遵循一个基本原则:除了传输过程中的监听及解包相关流程,其他的逻辑处理都会放在业务线程池中。比如序列化与反序列化,拦截器逻辑,本地方法调用。这个设计符合Netty的线程模型实践规范,最大化的保障传输框架的性能,提高服务资源利用率。gRPC 框架向业务层暴露了两个入口,一个是拦截器,在进入本地方法调用前拦截请求,用于处理一些前置逻辑;另一个就是本地服务。为了更清晰的表达业务线程池和Netty I/O 线程池的分工,我们用一个流程图来示意:

gRPC Java 服务端实现简析

ServerBuilder提供了基础方法让我们能够在启动的时候注册业务线程池,并且自定义我们想要的线程池大小,扩展策略,拒绝策略。

一个自定义线程池的示例

gRPC Java 服务端实现简析


另外一个值得注意的点,由于HTTP/2在协议层将Header和Body做了分帧传输,所以gRPC服务端接收逻辑和后续的异步处理逻辑也是分开的,这造成gRPC向业务开放的拦截器和本地服务极有可能会在不同的线程中运行,这里和我们常规的开发设计思路并不一致。所以尝试使用ThreadLocal进行数据变量存储和传递会在多并发条件下出现偶发性问题,非常不建议在实践中这样使用。

04

流控机制

gRPC在通信层是完全基于HTTP/2的,其流控机制也遵从HTTP/2的协议定义。我们先来了解一下HTTP/2中流控的原则:

RFC7540#5.2.1 Flow-Control Principles
HTTP/2 stream flow control aims to allow a variety of flow-control  algorithms to be used without requiring protocol changes. Flow  control in HTTP/2 has the following characteristics:
1. Flow control is specific to a connection. Both types of flow  control are between the endpoints of a single hop and not over  the entire end-to-end path.
2. Flow control is based on WINDOW_UPDATE frames. Receivers  advertise how many octets they are prepared to receive on a  stream and for the entire connection. This is a credit-based  scheme.
3. Flow control is directional with overall control provided by the  receiver. A receiver MAY choose to set any window size that it  desires for each stream and for the entire connection. A sender  MUST respect flow-control limits imposed by a receiver. Clients,  servers, and intermediaries all independently advertise their  flow-control window as a receiver and abide by the flow-control  limits set by their peer when sending.
4. The initial value for the flow-control window is 65,535 octets  for both new streams and the overall connection.
5. The frame type determines whether flow control applies to a  frame. Of the frames specified in this document, only DATA  frames are subject to flow control; all other frame types do not  consume space in the advertised flow-control window. This  ensures that important control frames are not blocked by flow  control.
6. Flow control cannot be disabled.
7. 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 协议层对于流控的设计,是旨在不改变协议实现的基础上支持多样性的流控算法。简单的描述即为:服务端通过WINDOW_UPDATE帧动态的告知每一个信道的发送端窗口大小,如果窗口大小不够大,则客户端应该停止发送,从而达到控制流量的目的。


在gRPC服务端的实现中,有两个关键逻辑来实现流控:

io.grpc.netty.FlowControlPinger#onDataRead

gRPC Java 服务端实现简析


io.grpc.netty.FlowControlPinger#updateWindow


gRPC服务端收到Body消息后会记录下自上一次发送Ping以后的消息长度,当收到Ping以后会触发updateWindow方法更新窗口大小。这是一个逐渐增加窗口过程。

05

总结

gRPC 是一款优秀的RPC框架,使我们能够快速搭建起分布式的远程调用系统。其优秀的性能表现和跨平台的特性是相较其他解决方案的优势,也是在微保大规模投入使用的原因。但gRPC官方仅仅只是想提供一个远程调用的框架,为了在工业化的分布式环境下使用,至少还需要自行集成一套服务注册与发现的方案。这一点相比业内目前流行的解决方案是一个劣势,也是被诟病的地方。