Dubbo协议解析与ESA RPC实践
点击关注“OPPO互联网技术”,阅读更多技术干货
1. 背景
Dubbo是一款高性能、轻量级的开源Java RPC框架,诞生于2012年,2015年停止研发,后来重启并发布了2.7及连续多个版本。Dubbo自开源以来,许多大公司都以此为微服务架构基石,甚至在官方停止维护的几年中,热度依然不减。
但最近几年云原生技术开始成为主流,与Dubbo框架的核心设计理念有不相容之处,再加上公司安全治理的需求,OPPO互联网技术团队开发了面向云原生、 Mesh友好的ESA RPC框架。
2.Dubbo协议解析
协议是两个网络实体进行通信的基础,数据在网络上从一个实体传输到另一个实体,以字节流的形式传递到对端。Dubbo协议由服务提供者与消费者双端约定,需要确定的是一次有意义的传输内容在读到何时结束,因为一个一个byte传输过来,需要有一个结束。而且数据在网络上的传输,存在粘包和半包的情况,能够应对这个问题的办法就是协议能够准确的识别,当粘包发生时不会多读,当半包发生时会继续读取。
2.1 Dubbo Header内容
Dubbo header的长度总共16字节,128位,如下图所示:
Magic(16 bits) : 协议魔数,标识Dubbo 数据包。
Req/Res(1 bit) : 标识请求或者相应。请求:1,相应:0。
Two Way(1 bit) : 仅在 Req/Res 为1(请求)时才有用,标记是否期望从服务器返回值。如果需要来自服务器的返回值,则设置为1。
Event(1 bit) : 标识是否是事件消息,例如,心跳事件。如果这是一个事件,则设置为1。
SerializationId(5 bits) : 序列化id。
Status(8 bits) : 仅在 Req/Res 为0(响应)时有用,用于标识响应的状态。
RequstId(64 bits) : 标识唯一请求。类型为long。
Data length(32 bits) : 序列化后的内容长度(变长部分,即不包含header),按字节计数。通过payload参数指定,默认为8M。
2.2 Dubbo body内容
Dubbo 数据包的body 部分内容,分为请求包与响应包。
如果是请求包,则包含的部分有:
dubbo协议版本号(2.0.2);
接口名;
接口版本号;
方法名;
方法参数类型;
方法参数;
附件(Attachment):
接口分组(group);
接口版本号(version);
接口名;
自定义附件参数;
如果是响应包,则包含的内容有:
返回值类型(byte):
返回空值(2);
正常返回值(1);
异常(0);
返回值;
通过对dubbo协议的解析,我们可以知道,dubbo协议是一个Header定长的变长协议。这也在我们ESA RPC实践过程中提供了一些思路。
2.3 Dubbo协议优缺点
2.3.1 优点
Dubbo协议的设计非常紧凑、简单,尽可能的减少传输包大小,能用一个bit表示的字段,不会用一个byte。
2.3.2 不足
请求body中某些字段重复传递(如接口名,接口版本号),即body内容与附件attachment 中存在重复字段,增大传输数据包大小;
对于ServiceMesh 场景很不友好。在ServiceMesh 场景中,会将原sdk中的大部分功能迁移至SideCar 中实现,这里以服务发现为例。Dubbo 中的服务发现,是通过接口名(interfaceName)、接口分组(group)、接口版本号(version)三者定位一个唯一服务,也是服务发现的关键要素,但是我们从dubbo body内容可知,必须要将完整的数据包全部解析(attachment位于body末),才能获取到这三个要素,这是完全没必要的。
没有预留字段,扩展性不足。
3. Dubbo的现状
Dubbo自开源以来,在业内造成了巨大的影响,许多公司甚至大厂都以此为微服务架构基石,甚至在Dubbo官方停止维护的几年中,热度依然不减,足以证明其本身的优秀。
在这过程中,Dubbo协议的内容一直没有太大变化,主要是为了兼容性考虑,但其他内容,随着Dubbo的发展变化却是很大。这里我们主要聊一聊dubbo从2.7.0版本以后的情况。
3.1 Dubbo 2.7.x版本总览
这是dubbo自2.7.0版本以来,各个版本的简要功能说明,以及升级建议。可以看到dubbo官方推荐生产使用的只有2.7.3 和2.7.4.1两个版本。但这两个推荐版本,也有不能满足需求的地方。
由于dubbo在2.7.3 和2.7.4.1 这两个版本中改动巨大,使得这两个版本无法向下兼容,这让基于其他版本做的一些dubbo扩展几乎无法使用。升级dubbo的同时,还需要将以前的扩展全部检查修改一遍,这带来很大工作量。而且除了我们自身团队的一些公共扩展外,全公司其他业务团队很可能还有自己的一些扩展,这无疑增大了我们升级dubbo的成本。
4. ESA RPC最佳实践
最近几年云原生技术开始成为主流,与Dubbo框架的核心设计理念也有不相容之处,再加上公司安全治理的需求,我们需要一款面向云原生、 Mesh友好的RPC框架。
在这个背景下,OPPO互联网技术团队从2019年下半年开始动手设计开发ESA RPC,到2020年一季度,ESA RPC 第一版成功发布。下面我们简单介绍下ESA RPC的一些主要功能。
4.1 实例级服务注册与发现
ESA RPC通过深度整合发布平台,实现实例级服务注册与发现,如图所示:
应用发布时,相应的发布平台会将实例信息注册到OPPO自研的注册中心ESA Registry(应用本身则不再进行注册),注册信息包含应用名、ip、端口、实例编号等等,消费者启动时只需通过应用编号订阅相关提供者即可。
既然服务注册部分是由发布平台完成,开发者在发布应用时,就需要填写相关信息,即相关的暴露协议以及对应的端口,这样发布平台才可以正确注册提供者信息。
4.2 客户端线程模型优化
ESA RPC全面拥抱java8的CompletableFuture ,我们将同步和异步的请求统一处理,认为同步是特殊的异步。而Dubbo,由于历史原因,最初dubbo使用的jdk版本还是1.7,所以在客户端的线程模型中,为了不阻塞IO线程,dubbo增加了一个Cached线程池,所有的IO消息统一都通知到这个Cached线程池中,然后再切换回相应的业务线程,这样可能会造成当请求并发较高时,客户端线程暴涨问题,进而导致客户端性能低下。
所以我们在ESA RPC客户端优化了线程模型,将原有的dubbo客户端cached线程池取消,改为如下图模型:
具体做法:
当前业务线程发出远程调用请求后,生成CompletableFuture 对象,并传递至IO线程,等待返回;
IO线程收到返回内容后,找到与之对应的CompletableFuture 对象,直接赋予其返回内容;
业务线程通过自己生成的CompletableFuture 对象获取返回值;
4.3 智能Failover
对于一些高并发的服务,可能会因传统Failover 中的重试而导致服务雪崩。ESA RPC对此进行优化,采用基于请求失败率的Failover ,即当请求失败率低于相应阈值时,执行正常的failover重试策略,而当失败率超过阈值时,则停止进行重试,直到失败率低于阈值再恢复重试功能。
ESA RPC采用RingBuffer 的数据结构记录请求状态,成功为0,失败为1。用户可通过配置的方式指定该RingBuffer 的长度,以及请求失败率阈值。
4.4 ServiceKeeper
ESA ServiceKeeper (以下简称ServiceKeeper ),属于OPPO自研的基础框架技术栈ESA Stack系列的一员。ServiceKeeper 是一款轻量级的服务治理框架,通过拦截并代理原始方法的方式织入限流、并发数限制、熔断、降级等功能。
ServiceKeeper 支持方法和参数级的服务治理以及动态动态更新配置等功能,包括:
方法隔离
方法限流
方法熔断
方法降级
参数级隔离、限流、熔断
方法重试
接口分组
动态更新配置,实时生效
ESA RPC中默认使用ServiceKeeper 来实现相关服务治理内容,使用起来也相对简单。
Step 1
application.properties 文件中开启ServiceKeeper 功能。
# 开启服务端
esa.rpc.provider.parameter.enable-service-keeper=true
# 开启客户端
esa.rpc.consumer.parameter.enable-service-keeper=true
Step 2
新增service-keeper.properties 配置文件,并按照如下规则进行配置:
# 接口级配置规则:{interfaceName}/{version}/{group}.{serviceKeeper params},示例:
com.oppo.dubbo.demo.DemoService/0.0.1/group1.maxConcurrentLimit=20
com.oppo.dubbo.demo.DemoService/0.0.1/group1.failureRateThreshold=55.5
com.oppo.dubbo.demo.DemoService/0.0.1/group1.forcedOpen=55.5
...
#方法级动态配置规则:{interfaceName}/{version}/{group}.{methodName}.{serviceKeeper params},示例:
com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.maxConcurrentLimit=20
com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.maxConcurrentLimit=20
com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.failureRateThreshold=55.5
com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.forcedOpen=false
com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.limitForPeriod=600
...
#参数级动态配置规则:{interfaceName}/{version}/{group}.{methodName}.参数别名.配置名称=配置值列表,示例:
com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.arg0.limitForPeriod={LiSi:20,ZhangSan:50}
...
4.5 连接管理
ESA RPC中,一个消费者与一个提供者,默认只会创建一个连接,但是允许用户通过配置创建多个,配置项为connections (与dubbo保持一致)。ESA RPC的连接池通过公司内部一个全异步对象池管理库commons pool来达到对连接的管理,其中连接的创建、销毁等操作均为异步执行,避免阻塞线程,提升框架整体性能。
需要注意的是,这里的建连过程,有一个并发问题要解决:当客户端在高并发的调用建连方法时,如何保证建立的连接刚好是所设定的个数呢?为了配合 Netty 的无锁理念,我们也采用一个无锁化的建连过程来实现,利用 ConcurrentHashMap 的putIfAbsent 方法:
AcquireTask acquireTask = this.pool.get(idx);
if (acquireTask == null) {
acquireTask = new AcquireTask();
AcquireTask tmpTask = this.pool.putIfAbsent(idx, acquireTask);
if (tmpTask == null) {
acquireTask.create(); //执行真正的建连操作
}
}
4.6 gRPC协议支持
由于ESA RPC默认使用ESA Regsitry 作为注册中心,由上述实例注册部分可知,服务注册通过发布平台来完成,所以ESA RPC对于gRPC协议的支持具有天然的优势,即服务的提供者可以不接入任何sdk,甚至可以是其他非java语言,只需要通过公司发布平台发布应用后,就可以注册至注册中心,消费者也就可以进行订阅消费。
这里我们以消费端为例,来介绍ESA RPC客户端如何请求gRPC服务端。
proto文件定义:
syntax = "proto3";
option java_multiple_files = false;
option java_outer_classname = "HelloWorld";
option objc_class_prefix = "HLW";
package esa.rpc.grpc.test.service;
// The greeting service definition.
service GreeterService {
// Sends a greeting
rpc sayHello (HelloRequest) returns (HelloReply) {
}
}
service DemoService {
// Sends a greeting
rpc sayHello (HelloRequest) returns (HelloReply) {
}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
然后maven中添加proto代码生成插件:
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.11.0:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>esa.rpc:protoc-gen-grpc-java:1.0.0-
SNAPSHOT:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
如上proto定义文件,通过protobuf:compile和protobuf:compile-custom则会生成如下代码:
可以看到,自动生成的代码中我们额外生成了相应的java接口。
在dubbo客户端我们就可以直接使用这个接口进行远程调用,使用方式:
@Reference(...,protocol="grpc")
private DemoService demoService;
4.7 ESA RPC性能
这里仅举一例,展示ESA RPC性能。
5. ESA RPC未来规划
5.1 ESA RPC如何进行平滑迁移?
由于历史原因,现公司内部大量使用的是Dubbo作为RPC框架,以及zookeeper注册中心,如何能够保证业务的平滑迁移,一直是我们在思考的问题。这个问题想要解答,主要分为以下两点。
5.1.1 代码层面
在代码层面,ESA RPC考虑到这个历史原因,尽可能的兼容dubbo,尽可能降低迁移成本。但ESA RPC毕竟作为一款新的RPC框架,想要零成本零改动迁移是不可能的,但在没有dubbo扩展的情况下,改动很小。
5.1.2 整体架构
这一点我们举例说明,当业务方迁移某一应用至ESA RPC框架时,该应用中消费ABCD四个接口,但这些接口的服务提供者应用并未升级至ESA RPC,接口元数据信息均保存至zookeeper注册中心当中,而ESA RPC推荐使用的ESA Registry注册中心中没有这些提供者信息,这就导致了消费者无法消费这些老的提供者信息。
针对这一问题,后续我们ESA Stack系列会提供相应的数据同步工具,将原zookeeper注册中心中的服务元数据信息同步到我们ESA Registry中,而zookeeper中的这些信息暂时不删除(以便老的接口消费者能够消费),等待均升级完成后,即可停用zookeeper注册中心。
5.2 自研RPC协议
在上面Dubbo协议解析过程中,我们分析了Dubbo协议的优缺点,了解了Dubbo协议的不足。所以后续的版本升级过程中,自研RPC协议是一个不可忽视的内容。自研RPC协议需要充分考虑安全、性能、Mesh支持、可扩展、兼容性等因素,相信通过自研RPC协议可以使我们的ESA RPC更上一层楼。
5.3 其他
多协议暴露
同机房优先路由
类隔离
...
在这篇文章中,我们主要分享了Dubbo协议的分析以及ESA RPC的实践内容,后续OPPO互联网技术团队会继续分享更多ESA RPC的动态。
☆ END ☆
OPPO互联网基础技术团队招聘一大波岗位,涵盖C++、Go、OpenJDK、Java、DevOps、Android、ElasticSearch等多个方向,请点击这里查看详细信息及JD。
更多技术干货
扫码关注
OPPO互联网技术