gRPC的技术栈
(1)远程服务提供者需要以某种形式提供服务调用相关的信息,包括但不限于服务接口定义、数据结构,以及中间态的服务定义文件,例如gRPC 的 proto 文件、WS-RPC 的WSDL文件定义,甚至也可以是服务端的接口说明文档。服务调用者需要通过一定的途径获取远程服务调用相关信息,例如服务端接口定义Jar包导入、获取服务端IDL文件等。
(2)远程代理对象:服务调用者调用的服务实际是远程服务的本地代理,对于 Java语言,它的实现就是 JDK 的动态代理,通过动态代理的拦截机制,将本地调用封装成远程服务调用。
(3)通信:RPC框架与具体的协议无关,例如Spring的远程调用支持HTTP Invoke、RMI Invoke,MessagePack使用的是私有的二进制压缩协议。
(4)序列化:远程通信需要将对象转换成二进制码流进行网络传输,不同的序列化框架支持的数据类型、数据包大小、异常类型及性能等都不同。不同的 RPC框架的应用场景不同,因此技术选择也存在很大差异。一些做得比较好的 RPC框架,支持多种序列化方式,有的甚至支持用户自定义
相比其他开源的RPC框架,gRPC有如下几个特点
(1)支持多种语言。
(2)基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口及客户端Stub。
(3)通信协议基于标准的HTTP/2设计,支持双向流、消息头压缩、单TCP的多路复用、服务端推送等特性。
(4)序列化支持Protocol buffer和JSON,Protocol buffer是一种语言无关的高性能序列化框架,基于“HTTP/2+Protocol buffer”,保障了RPC调用的高性能。
gRPC 底层的通信框架基于Netty 4.1构建,通过集成Netty的HTTP/2协议栈,支持双向流、消息头压缩、单TCP的多路复用、服务端推送等特性,传统的HTTP/1.0或者HTTP/1.1是无状态的,创建HTTP连接之后,客户端发送请求消息,然后等待服务端响应,接收到服务端响应之后,客户端接着发送后续的请求消息,服务端再返回响应,周而复始。请求和响应消息都是成对出现的,采用的是“一请求对应一响应”模式。在某个时刻,一个HTTP连接上只能单向地处理一个消息,就像单行道,一个消息处理得慢,很容易导致后续消息阻塞。
采用该模式主要存在如下几个缺点。
(1)单个连接的通信效率不高,无法多路复用。
(2)一个请求消息处理得慢,很容易阻塞后续其他请求消息。
(3)如果客户端读取响应超时,由于消息是无状态的,只能关闭连接,重建连接之后再发送请求。如果频繁地发生客户端超时,就会发生大量的HTTP连接断连和重连,如果采用HTTPS,SSL链路的重建成本很高,很容易导致服务端因负载过重而宕机。
(4)为了解决单个HTTP连接性能不足问题,只能创建一个大的连接池,在大规模集群组网场景下,HTTP连接数会非常多,额外占用大量句柄资源。
采用HTTP/2之后,可以实现多路复用,客户端可以连续发送多个请求,服务端也可以返回一个或者多个响应,而且还可以主动推送数据到服务端,实现双向通信,达到TCP协议的通信效果。
gRPC通过对Netty HTTP/2的封装,向用户屏蔽底层RPC通信的协议细节。
Netty HTTP/2服务端创建的主要流程:
1.NettyServer 具体管理 HTTP/2协议栈的启动和停止,以及 HTTP/2协议相关的各种参数。
2.NettyServer 的 start 方法会创建 NettyServerTransport,通过NettyServerTransport来创建gRPC的Netty HTTP/2 ChannelHandler实例,并加入ChannelPipeline。
(NettyServerHandler主要负责HTTP/2消息的处理,例如HTTP/2请求消息体和消息头的读取、Frame消息的发送、Stream状态消息的处理等)
3.创建 ProtocolNegotiator实例,用于 HTTP/2连接创建的协商。gRPC支持三种协商策略,分别是 PlaintextNegotiator、PlaintextUpgradeNegotiator和 TlsNegotiator。其中PlaintextUpgradeNegotiator通过设置Http2ClientUpgradeCodec,用于协议升级。
4.创建ServerBootstrap、bossGroup和workerGroup,启动HTTP/2服务端,在NettyServer的start方法里
gRPC服务端的请求消息由Netty HTTP/2协议栈负责接入,gRPC通过继承Http2FrameAdapter,将自定义的FrameListener添加到Netty的Http2ConnectionDecoder中,在HTTP/2请求消息头和消息体被解析成功之后,回调 gRPC的FrameListener,接收并处理 HTTP/2请求消息,将 Netty的请求消息体 ByteBuf转换成 gRPC内部的NettyReadableBuffer对象,调用deframe方法,完成请求消息体的解码.
服务端发送HTTP/2 响应消息原理:
gRPC 服务端通过将响应消息封装成WriteQueue.AbstractQueuedCommand,异步写入WriteQueue,然后调用 WriteQueue 的 scheduleFlush 操作,将响应消息发送命令放到NioEventLoop 中执行,NioEventLoop调用 channel.write 方法将其发送到ChannelPipeline,由 gRPC 的NettyServerHandler 拦截 write 方法,按照命令的分类进行处理,最后调用 Netty Http2ConnectionEncoder的write方法完成响应消息的发送。
1.通过NettyServerStream的Sink类对需要发送的HTTP/2响应消息进行Task封装,实现消息发送的异步化.
2.WriteQueue 将任务投递到对应 Channel 的 NioEventLoop 线程异步执行消息发送操作,注意WriteQueue的scheduleFlush操作是在业务线程中的,会有多个业务线程,所以这里要加锁,保证多线程的同步性,这里采用了无锁化操作CAS,代码如下
3.NioEventLoop线程循环处理待发送的响应消息,调用 Channel的write方法,将消息发送到ChannelPipeline,由gRPC ChannelHandler拦截处理
4.gRPC的NettyServerHandler拦截write方法,按照命令的类型进行分类处理,如果是 SendGrpcFrameCommand 和SendResponseHeadersCommand,则调用 Netty 的Http2ConnectionEncoder完成HTTP/2消息的发送。
即HTTP/2服务端创建、HTTP/2请求消息的接入和响应发送都由Netty NioEventLoop线程负责,gRPC消息的序列化和反序列化业务服务接口的调用由 gRPC的SerializingExecutor线程池负责。
Netty NIO线程和gRPC的SerializingExecutor之间没有映射关系(M:N),当线程数量比较多时,锁竞争会非常激烈,可以采用 I/O线程和 gRPC服务调用线程绑定的方式,降低出现锁竞争的概率,提升并发性能,通过线程绑定技术降低锁竞争的概率(1:1).