gRPC & netty 初探
gRPC & netty
背景
- 开发MQ测试环境多版本引流功能,复用RocketMQ的remoting模块netty通信模块,client以近原生的方式请求元信息服务控制消息引流
- 开发故障检测过程中使用gRPC通信能力,java-gRPC底层默认由netty实现
- SkyWalking多版本链路传递功能,学习SkyWalking通信部分设计
gRPC简介
一个高性能、开源通用RPC框架
亮点
- protobuf二进制序列化协议,易于定义;
- 启动快,易于拓展;
- 跨平台、跨语言,你甚至可以在web端把他当websocket去用;
- 基于HTTP/2的双向流式传输,高度集成可插拔的鉴权机制;
CNCF孵化中
gRPC使用protobuf作为
从protobuf开始
protubuf是gRPC中的核心概念之一,gRPC使用protobuf同时作为接口定义语言(IDL)和底层消息交换的序列化结构(实际上也可以替换为json等),protobuf中即可以定义rpc(端点),也可以定义数据结构,也支持目录包引用、oneof、enum等特性,protobuf的定义和常用的编程语言数据结构还是有些区别的,例如没有继承关系(protobuf3不再支持extend,实际上可以通过oneof关键字曲线救国)
以下面故障检测服务的其中一个protobuf定义为例,import可以从其他包下引入定义,option则是代码生成的一些选项,service下定义了一个rpc:StreamChannel,入参和出参都是stream类型,表示是双向流;enum和message的数据结构中都由Field var=number的格式组成,其中number的值会在序列化时转成二进制,用来表示字段code。protobuf序列化时number在1~15时占用1个字节,16~2047会占用2个字节,所以最佳实践是把前15的序号保留给最常使用的字段。
syntax = "proto3";
import "command/server/register.proto";
import "command/client/thread_snapshot.proto";
import "command/client/profile.proto";
import "command/server/hot_thread.proto";
package com.enmonster.platform.hts.grpc;
option java_multiple_files = true;
option java_package = "com.enmonster.platform.hts.grpc";
option java_outer_classname = "CommandDispatcherRPC";
service CommandDispatcher {
// 双向流,server和client可互相通讯
rpc StreamChannel (stream StreamDataPackage) returns (stream StreamDataPackage) {}
}
// 保证向前兼容,添加命令请勿修改已有命令的field_value
enum Command {
REGISTER = 0; // client注册到server
RECORD_HOT_THREAD = 1; // 记录热点线程
// 以上为server端支持的命令,以下为client端支持的命令
THREAD_SNAPSHOT = 11; // 采集线程快照
PROFILE = 12; // 开始async-profiler采样
}
// 双向流的逻辑数据包,response_body为空表示是请求,不为空表示是响应
message StreamDataPackage {
string job_id = 1; // 命令所属的job id
Command command = 2; // 命令
string client_ip = 3; // 客户端ip, aka node ip
oneof request { // 命令请求payload
RegisterRequest register_request = 10;
ThreadSnapshotRequest thread_snapshot_request = 11;
HotThreadRequest hot_thread_request = 12;
ProfileRequest profile_request = 13;
}
BaseResponse response = 20; // 命令响应payload,不为null表示是命令结果,否则是命令请求
}
message BaseResponse {
bool ok = 1; // 是否成功
string message = 2; // 消息
// 命令结果body
oneof body {
RegisterResponse register_response = 3;
ThreadSnapshotResponse thread_snapshot_response = 4;
HotThreadResponse hot_thread_response = 5;
ProfileResponse profile_response = 6;
ProfileResultResponse profile_result_response = 7;
}
}
大部分字段类型都和常用编程语言类型相似,包括支持map<string, string>表示map、repeated表示数组等,同时也支持复用包里的数据结构,例如google.protobuf.Timestamp
编译protobuf
protobuf的优势在于其是跨平台、跨语言的DSL,比高级编程语言更抽象一级,所以写起来非常简洁(有点像写java接口哈)。这就意味着protobuf+grpc编译后的产物是高级编程语言(java、python、go、c/cpp……),有点像前端的.vue编译成css、js,不过感觉抽象级别更高一些。
这里以java编译为例,引入protobuf-maven-plugin插件执行compile就可以得到编译后的java文件(也可以直接用protoc二进制编译器,实际上这个maven插件也是去执行protoc编译的)
填充自己的业务逻辑吧
编译后的代码中可以看见对应的java类中已经有了完整的gRPC底层通讯逻辑,包括定义的RPC端点,几种同步异步的stub可以直接调用,各种数据结构的builder等,使用时可以非常方便地继承生成的ImplBase类,填充对应handler的逻辑。
可以把protobuf定义单独打到一个maven模块,server和client去引相同的依赖包,以保证版本的一致性和复用。因为是BI_DI类型的rpc,client的逻辑和server就很类似了,初始由client去主动连接server,我这里因为需要上报client的信息做了心跳保活,实际上因为gRPC基于HTTP/2的特性,如果没有显式地设置deadline/timeout,流式的rpc是可以一直传输的,而不用使用HTTP/1.X去轮询或者定时hold长链接。
@see HTC的client代码
SkyWalking是怎么玩的
SkyWalking作为tracing组件,每时每刻都会上报大量的数据到其后端server,除了对SW本身的处理能力、吞吐性有要求外,稳固可靠的传输层/RPC框架也很重要,SkyWalking使用的就是gRPC。社区在8.X后也有了走kafka消息队列的方案,但是至少就我们公司而言,gRPC在链路传输和rpc性能方面已经表现得相当稳固了。
TraceSegmentServiceClient.java
Tracing.proto
RemoteServiceHandler.java
TraceSegmentReportServiceHandler.java
我看的也比较浅,如果感兴趣可以去看SW官方的这篇博客,SkyWalking创始人吴晟写的,介绍了一些通讯、路由、整个系统架构层面的一些内容,比较干货。
有趣的是,RocketMQ在最新发布的5.0版本中也在原来的纯netty通信基础上,选用了gRPC作为默认通讯及rpc方案,由此可见gRPC的高性能和可靠性也越来越被开源社区认可。
站在巨人的肩膀上——netty
既然上面说到gRPC这么强大,除了protobuf作为二进制序列化框架、rpc DSL,以及HTTP/2的底层协议,其他都是自己实现的吗?从grpc-java的角度看,底层还少不了一位重量级角色netty,作为网络层框架。(当然这么说也不是很严谨,实际上除了序列化协议可以由protobuf替换为json等,底层网络传输层也可以由ok-http等替换,这和语言也有关系)
netty简介
netty是一个异步的、事件驱动的网络应用框架,netty的性能众所周知非常强大,资料也非常多,我并没有写过原生的netty,最近在写虚拟环境MQ相关内容时发现RocketMQ的通讯模块对netty封装的也非常好,所以拿过来看下内部是怎么实现的,以及分享下如何基于rocket-remoting模块去拓展MQ client的逻辑,得到和原生consumer<->broker一致的rpc能力。
org.apache.rocketmq.remoting.netty.NettyRemotingAbstract#processRequestCommand