6
本文来自OPPO互联网基础技术团队,转载请注名作者。同时欢迎关注我们的公众号:OPPO_tech,与你分享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的动态。


OPPO数智技术
612 声望952 粉丝