Dubbo是一款高性能、轻量级的开源Java RPC框架,诞生于2012年,2015年停止研发,后来重启并发布了2.7及连续多个版本。Dubbo自开源以来,许多大公司都以此为微服务架构基石,甚至在官方停止维护的几年中,热度依然不减。
但最近几年云原生技术开始成为主流,与Dubbo框架的核心设计理念有不相容之处,再加上公司安全治理的需求,OPPO互联网技术团队开发了面向云原生、 Mesh友好的ESA RPC框架。
协议是两个网络实体进行通信的基础,数据在网络上从一个实体传输到另一个实体,以字节流的形式传递到对端。Dubbo协议由服务提供者与消费者双端约定,需要确定的是一次有意义的传输内容在读到何时结束,因为一个一个byte传输过来,需要有一个结束。而且数据在网络上的传输,存在粘包和半包的情况,能够应对这个问题的办法就是协议能够准确的识别,当粘包发生时不会多读,当半包发生时会继续读取。
Dubbo header的长度总共16字节,128位,如下图所示:
Dubbo 数据包的body 部分内容,分为请求包与响应包。
如果是请求包,则包含的部分有:
如果是响应包,则包含的内容有:
返回值类型(byte):
返回值;
通过对dubbo协议的解析,我们可以知道,dubbo协议是一个Header定长的变长协议。这也在我们ESA RPC实践过程中提供了一些思路。
Dubbo协议的设计非常紧凑、简单,尽可能的减少传输包大小,能用一个bit表示的字段,不会用一个byte。
Dubbo自开源以来,在业内造成了巨大的影响,许多公司甚至大厂都以此为微服务架构基石,甚至在Dubbo官方停止维护的几年中,热度依然不减,足以证明其本身的优秀。
在这过程中,Dubbo协议的内容一直没有太大变化,主要是为了兼容性考虑,但其他内容,随着Dubbo的发展变化却是很大。这里我们主要聊一聊dubbo从2.7.0版本以后的情况。
这是dubbo自2.7.0版本以来,各个版本的简要功能说明,以及升级建议。可以看到dubbo官方推荐生产使用的只有2.7.3 和2.7.4.1两个版本。但这两个推荐版本,也有不能满足需求的地方。
由于dubbo在2.7.3 和2.7.4.1 这两个版本中改动巨大,使得这两个版本无法向下兼容,这让基于其他版本做的一些dubbo扩展几乎无法使用。升级dubbo的同时,还需要将以前的扩展全部检查修改一遍,这带来很大工作量。而且除了我们自身团队的一些公共扩展外,全公司其他业务团队很可能还有自己的一些扩展,这无疑增大了我们升级dubbo的成本。
最近几年云原生技术开始成为主流,与Dubbo框架的核心设计理念也有不相容之处,再加上公司安全治理的需求,我们需要一款面向云原生、 Mesh友好的RPC框架。
在这个背景下,OPPO互联网技术团队从2019年下半年开始动手设计开发ESA RPC,到2020年一季度,ESA RPC 第一版成功发布。下面我们简单介绍下ESA RPC的一些主要功能。
ESA RPC通过深度整合发布平台,实现实例级服务注册与发现,如图所示:
应用发布时,相应的发布平台会将实例信息注册到OPPO自研的注册中心ESA Registry(应用本身则不再进行注册),注册信息包含应用名、ip、端口、实例编号等等,消费者启动时只需通过应用编号订阅相关提供者即可。
既然服务注册部分是由发布平台完成,开发者在发布应用时,就需要填写相关信息,即相关的暴露协议以及对应的端口,这样发布平台才可以正确注册提供者信息。
ESA RPC全面拥抱java8的CompletableFuture ,我们将同步和异步的请求统一处理,认为同步是特殊的异步。而Dubbo,由于历史原因,最初dubbo使用的jdk版本还是1.7,所以在客户端的线程模型中,为了不阻塞IO线程,dubbo增加了一个Cached线程池,所有的IO消息统一都通知到这个Cached线程池中,然后再切换回相应的业务线程,这样可能会造成当请求并发较高时,客户端线程暴涨问题,进而导致客户端性能低下。
所以我们在ESA RPC客户端优化了线程模型,将原有的dubbo客户端cached线程池取消,改为如下图模型:
具体做法:
对于一些高并发的服务,可能会因传统Failover 中的重试而导致服务雪崩。ESA RPC对此进行优化,采用基于请求失败率的Failover ,即当请求失败率低于相应阈值时,执行正常的failover重试策略,而当失败率超过阈值时,则停止进行重试,直到失败率低于阈值再恢复重试功能。
ESA RPC采用RingBuffer 的数据结构记录请求状态,成功为0,失败为1。用户可通过配置的方式指定该RingBuffer 的长度,以及请求失败率阈值。
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}
...
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(); //执行真正的建连操作
}
}
由于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代码生成插件:
kr.motd.maven
os-maven-plugin
1.5.0.Final
org.xolstice.maven.plugins
protobuf-maven-plugin
0.5.0
com.google.protobuf:protoc:3.11.0:exe:${os.detected.classifier}
grpc-java
esa.rpc:protoc-gen-grpc-java:1.0.0-
SNAPSHOT:exe:${os.detected.classifier}
compile
compile-custom
如上proto定义文件,通过protobuf:compile和protobuf:compile-custom则会生成如下代码:
可以看到,自动生成的代码中我们额外生成了相应的java接口。
在dubbo客户端我们就可以直接使用这个接口进行远程调用,使用方式:
@Reference(...,protocol="grpc")
private DemoService demoService;
这里仅举一例,展示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注册中心。
在上面Dubbo协议解析过程中,我们分析了Dubbo协议的优缺点,了解了Dubbo协议的不足。所以后续的版本升级过程中,自研RPC协议是一个不可忽视的内容。自研RPC协议需要充分考虑安全、性能、Mesh支持、可扩展、兼容性等因素,相信通过自研RPC协议可以使我们的ESA RPC更上一层楼。