点击关注“OPPO互联网技术”,阅读更多技术干货css
1. 背景
Dubbo是一款高性能、轻量级的开源Java RPC框架,诞生于2012年,2015年中止研发,后来重启并发布了2.7及连续多个版本。Dubbo自开源以来,许多大公司都以此为微服务架构基石,甚至在官方中止维护的几年中,热度依然不减。java
但最近几年云原生技术开始成为主流,与Dubbo框架的核心设计理念有不相容之处,再加上公司安全治理的需求,OPPO互联网技术团队开发了面向云原生、 Mesh友好的ESA RPC框架。web
2.Dubbo协议解析
协议是两个网络实体进行通讯的基础,数据在网络上从一个实体传输到另外一个实体,以字节流的形式传递到对端。Dubbo协议由服务提供者与消费者双端约定,须要肯定的是一次有意义的传输内容在读到什么时候结束,由于一个一个byte传输过来,须要有一个结束。并且数据在网络上的传输,存在粘包和半包的状况,可以应对这个问题的办法就是协议可以准确的识别,当粘包发生时不会多读,当半包发生时会继续读取。缓存
2.1 Dubbo Header内容
Dubbo header的长度总共16字节,128位,以下图所示:安全
Magic(16 bits) : 协议魔数,标识Dubbo 数据包。bash
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=20com.oppo.dubbo.demo.DemoService/0.0.1/group1.failureRateThreshold=55.5com.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=20com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.maxConcurrentLimit=20com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.failureRateThreshold=55.5com.oppo.dubbo.demo.DemoService/0.0.1/group1.sayHello.forcedOpen=falsecom.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 greetingsmessage 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互联网技术


本文分享自微信公众号 - OPPO互联网技术(OPPO_tech)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。