学点儿gRPC-从入门到放弃
微服务如火如荼的当下,各种服务框架层出不穷。Dubbo、SpringCloud在国内Java后端微服务领域目前占据大部分份额。
但是随着云原生愈发普及,具备跨语言、高性能特性的RPC通信框架横空出世,其中gRPC与Thrift是其中的佼佼者。
本文我们将视角集中在gRPC这RPC框架。
gRPC 是Google开源的高性能、通用的RPC框架。客户端与服务端约定接口调用, 可以在各种环境中运行,具有跨语言特性, 适合构建分布式、微服务应用。
个人认为,gRPC最为杀手锏的特性就是“跨语言”,其次才是高性能。
它的跨语言特性体现在,通过定义IDL(接口定义语言),隔离了不同编程语言之间的差异,对IDL进行编译后,生成对应编程语言的nativeCode,让开发者能够集中注意在实现业务需求上,而不需要花费额外的精力在语言层面上。
官网的一张图能够很好的体现这个特点
gRPC特性介绍
gRPC具备以下特性
性能优异:
它采用Proto Buffer作序列化传输媒介, 对比JSON与XML有数倍提升。
采用HTTP2协议, 对头部信息(header)压缩, 对连接进行复用,能够减少TCP连接次数。
针对Java语言,gRPC底层采用Netty作为NIO处理框架, 性能强劲。
- 多语言支持,多客户端接入, 支持C++/GO/Ruby等语言。
- 支持负载均衡、跟踪、健康检查和认证。
gRPC的线程模型是怎样的?
笔者主力语言为Java,因此我们讲解也集中在Java的实现上。
gRPC的Java实现,服务端底层采用了Netty作为核心处理框架,因此其线程模型核心也是遵循了 Netty 的线程分工原则。
协议层消息的接收和编解码由 Netty 的 I/O(NioEventLoop) 线程负责, 应用层的处理由应用线程负责,防止由于应用处理耗时而阻塞 Netty 的 I/O 线程。
Netty线程模型是基于NIO的Reactor模式。
Netty是基于NIO构建的通信框架。
在 Java NIO 中最重要的概念就是多路复用器 Selector,它是 Java NIO 编程的基础。Selector提供了选择已经就绪的任务的能力。
简单来讲,Selector 会不断地轮询注册在其上的 Channel,如果某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。
一般来说,一个 I/O 线程会聚合一个 Selector,一个 Selector 可以同时注册 N 个 Channel, 这样单个
I/O 线程就可以同时并发处理多个客户端连接。
又由于 I/O 操作是非阻塞的,因此也不会受限于网络速度和对方端点的处理时延,可靠性和效率都得到了很大提升。
gRPC客户端如何请求服务端?
作为RPC框架,至少有客户端和服务端两个角色,对于gRPC而言,客户端请求服务端的调用过程如图所示。
具体过程:
- 【Stub生成】客户端生成Stub ,通过Stub发起 RPC远程服务调用 ;
- 【负载均衡】客户端获取服务端的地址信息(列表),使用默认的 LoadBalancer 策略,选择一个具体的 gRPC 服务端进行调用;
- 【建立链接】如果客户端与服务端之间没有可用的连接,则创建 NettyClientTransport 和 NettyClientHandler,建立 HTTP/2 连接;
- 【客户端请求序列化】对请求使用 PB(Protobuf)序列化,并通过 HTTP/2 Stream 发送给 gRPC 服务端;
- 【服务端反序列化】服务端接收到响应之后,使用 PB(Protobuf)做反序列化。
- 【请求响应】回调 GrpcFuture 的 set(Response) 方法,唤醒阻塞的客户端调用线程,获取 RPC 响应数据。
gRPC性能到底有多强?
没有对比就没有发言权。
在不同的操作系统,不同请求数量下,对gRPC与Rest请求进行对比的结论如下:
官网也给出了权威性的比对,具体比对gRPC+ProtoBuf与Http+JSON方式请求的差异。
实测结果显示GRpc的通讯方案, 性能有32%的提升, 资源占用降低30%左右。
gRPC-Java 服务调用实战
按照惯例,我们提供一个简单的订单案例展示gRPC在实际开发中如何使用。
该案例在实际中的意义为:提供一个报价服务,客户端发送下单请求到服务端进行报价,服务端对用户报价单进行汇总计算,并提供查询接口供客户端查询。
主要提供批量下单及查询用户订单能力。
流程图大致如下:
工程结构如下:
|
|
grpc-demo父工程
父工程相对比较简单,管理了子工程及依赖版本。
|
|
grpc-demo-sdk
grpc-demo-sdk是较为关键的公共依赖,主要基于proto对服务进行定义,生成java代码并打包供服务提供方与消费方使用。
pom.xml
sdk的pom文件如下:
|
|
重点关注一下plugin,我们使用protobuf-maven-plugin作为protobuf的编译工具,有了该插件,我们在执行mvn clean compile命令时便可以实现将proto编译为java代码的目的。
同样,执行mvn clean package命令可以实现将proto编译为java代码并打包为jar包的目的。
可以说是极为方便了。
编写proto文件,定义服务接口
编写OrderService.proto,定义服务接口,主要定义了查询用户订单,批量下单接口,及对应的各种实体和枚举。
|
|
如下为protobuf与java、c++对应关系,
更多protobuf的使用,请参考官网文档:https://developers.google.com/protocol-buffers/docs/javatutorial
protobuf属性 | C++属性 | java属性 | 备注 |
double | double | double | 固定8个字节 |
float | float | float | 固定4个字节 |
int32 | int32 | int32 | 使用变长编码,对于负数编码效率较低,如果经常使用负数,建议使用sint32 |
int64 | int64 | int64 | 使用变长编码,对于负数编码效率较低,如果经常使用负数,建议使用sint64 |
uint32 | uint32 | int | 使用变长编码 |
uint64 | uint64 | long | 使用变长编码 |
sint32 | int32 | int | 采用zigzag压缩,对负数编码效率比int32高 |
sint64 | int64 | long | 采用zigzag压缩,对负数编码效率比int64高 |
fixed32 | uint32 | int | 总是4字节,如果数据>2^28,编码效率高于unit32 |
fixed64 | uint64 | long | 总是8字节,如果数据>2^56,编码效率高于unit32 |
sfixed32 | int32 | int | 总是4字节 |
sfixed64 | int64 | long | 总是8字节 |
bool | bool | boolean |
|
string | string | String | 一个字符串必须是utf-8编码或者7-bit的ascii编码的文本 |
bytes | string | ByteString | 可能包含任意顺序的字节数据 |
编译打包grpc-demo-sdk工程
编写完proto文件后,对grpc-demo-sdk工程执行打包编译
|
|
编写服务端grpc-server-demo
接着编写服务端
pom.xml
服务端pom内容如下
|
|
除了lombok外,其余的依赖由grpc-demo-sdk间接引入。
编写OrderServiceImpl实现核心业务逻辑
首先编写OrderServiceImpl,实现核心的下单与查订单业务逻辑。
|
|
这里的代码是完整的代码,读者可以自行复制并直接使用,简单解释下代码:
- placeOrder为下单服务,核心逻辑就是解析用户下单请求PlaceOrderRequest,将用户订单增量添加到内存订单簿USER_MEMORY_ORDER_BOOK中。
- 核心的数据结构为:Map
> ,在实战中,通用会持久化订单到redis、MySQL、RocksDB等存储设施中;
- 核心的数据结构为:Map
- queryUserOrders为查询订单服务,核心逻辑为解析用户查询订单请求QueryUserOrderRequest,取出用户id(userId),并在订单簿中匹配当前用户的订单列表。
服务端启动类OrderServerBoot
有了服务端业务代码之后,重点关注一下服务端启动类的编写。
|
|
解释下代码:
- 核心逻辑为main方法,首先定义OrderServerBoot,通过startServer()启动服务,并通过blockUntilShutdown()让主线程等待终止。
- startServer() 方法核心逻辑,启动一个服务端进程并绑定到对应的端口,这里使用10880,并添加优雅停机钩子;
- stop() 逻辑为服务关闭逻辑;
- blockUntilShutdown() :由于grpc使用守护线程,因此需要在主线程上等待终止。
编写客户端grpc-client-demo
有了服务端,我们接着看下客户端工程的编写。
pom.xml
客户端pom如下
|
|
与服务端相同,除了lombok外,其余的依赖由grpc-demo-sdk间接引入。
编写客户端服务调用代理OrderClientAgent
客户端调用远程服务,需要借助proto生成的stub桩,作为客户端而言,常常会对该stub进行包装,这里我们通过一个OrderClientAgent作为stub的包装类。
|
|
简单解释下代码:
- 通过构造方法传入主机名,服务端端口,构造客户端与服务端间的链接通过ManagedChannel
- 通过OrderServiceGrpc.newBlockingStub(channel)生成客户端访问的stub实例,这里使用的是阻塞型Stub,即同步等待服务端返回所有结果;
- placeOrder方法通过stub访问服务端的下单服务;
- queryOrders方法通过stub访问服务端的查询订单服务。
编写客户端启动类
|
|
重点关注main方法:
- 声明服务端端口,这里注意务必与服务端暴露服务端口保持一致;
- 通过构造方法创建客户端访问服务端的agent实例,即上面提到的OrderClientAgent;
- 通过实例化的OrderClientAgent执行下单、查询订单操作
- 调用完成后,关闭OrderClientAgent,关闭客户端与服务端之间的链接。
- 实际生产中,客户端往往会与服务端保持链接开启,而不会频繁创建、关闭服务。
测试
sdk、客户端、服务端均编写完毕,我们启动服务进行测试。
首先编译打包sdk
在grpc-demo-sdk根目录下执行:
|
|
启动服务端
运行OrderServerBoot的main方法,日志打印如下:
|
|
启动客户端
运行OrderClientBoot的main方法,启动客户端并发起服务调用
首先进行下单:
|
|
下单成功,接着发起查询订单操作:
|
|
可以看到,下单成功,且通过查询订单调用,将用户10086下的两个订单获取到了。
观察服务端日志
服务端日志打印如下
|
|
服务端完成下单之后,对用户订单总价值进行计算
totalPrice = 12.515 + 10 2 = 207.5
小结
本文我们对gRPC进行了如下介绍:
- gRPC特性介绍
- gRPC-java线程模型
- gRPC客户端请求服务端方式
- gRPC与REST性能比对
并通过一个完整的demo展示了基于gRPC实现的报价服务,全景展示了gRPC在实战中如何进行使用。
到此我们对gRPC应当有了大致的了解和认知,后续我们将继续从入门到放弃的学习之路。
预告:接下来将对gRPC的底层机制进行讲解,并会为我们的报价服务添加服务发现能力,整合Nacos提供服务注册与发现,降低客户端与服务端之间的耦合,敬请期待。
版权声明:
原创不易,洗文可耻。除非注明,本博文章均为原创,转载请以链接形式标明本文地址。