作者:
严浩:同程艺龙高级开发,负责服务治理相关工作, Apache Dubbo Committer。
胥皓:同程艺龙高级开发,负责服务治理相关工作。
背景
在微服务发展初期,市场上还没有成熟和流行的 RPC 框架,我们公司内部自研开发了一套名为 DSF (Distributed Service Framework) 的 RPC 框架,支撑起了公司业务的高速发展。但是随着技术的快速迭代和人员的不断变更,开发者既要修复之前的 BUG 又要跟上技术的更新,开发维护成本越来越高。另一方面,现在应用程序都在往云原生方向发展与设计,公司也在这方面做出探索。因此公司微服务框架的演进已经到了岔路口,是全新升级原有的 SDK,还是选择拥抱开源?
考虑到升级现有的 SDK 在一段时间之后可能依然会面临现在的问题,最后我们选择了拥抱开源。在一番调研之后我们选择了 Dubbo3 作为公司的下一代 RPC 框架,担任微服务治理体系的数据面。
目前 Dubbo3 在公司的落地开发工作已经完成,通过本文我们对公司内部 Dubbo3 的实践及收益做了深入总结。
Dubbo 3 核心功能介绍
Dubbo 社区关于 Dubbo 3 的文档和资料越来越完善,以下是我们从社区引用的一些内容。
Dubbo 3 被社区寄予厚望,将其视为下一代云原生服务框架打造,Dubbo3 提供的核心特性列表,主要包括四部分。
- 全新服务发现模型。应用粒度服务发现,面向云原生设计,适配基础设施与异构系统;性能与集群伸缩性大幅提升。
- 下一代 RPC 协议 Triple。基于 HTTP/2 的 Triple 协议,兼容 gRPC;网关穿透性强、多语言友好、支持 Reactive Stream。
- 统一流量治理模型。面向云原生流量治理,SDK、Mesh、VM、Container 等统一治理规则;能够支持更丰富的流量治理场景。
- Service Mesh。在最新的3.1.0的版本中支持Sidecar Mesh 与 Proxyless Mesh,提供更多架构选择,降低迁移、落地成本。
Dubbo 3 的核心功能点(如应用级服务发现以 ip、port 为区分实例)和公司内部的服务模型一致,极大地减少了我们的适配工作。还有在 Service Mesh 中对 Sidecar Mesh 与 Proxyless Mesh 的支持也将减少后续公司对 Mesh 方案的探索成本,包括 Dubbo3 在多语言体系的发展也为异构架构提供了支撑。
总的来说,Dubbo 3 非常契合公司的技术体系和后续的发展方向。此外,Dubbo 在开发者中的熟悉度、社区的高活跃度和完善的文档建设也都能为推动 Dubbo3 的使用带来不少的帮助。
方案调研
在了解了 Dubbo3 的核心功能和基本工作原理之后我们开始前期工作阶段。
公司内部存在微服务体系 RPC 框架 DSF 和承担服务发现、路由、上下负载等功能的控制中心,如果让用户直接切换到 Dubbo3 使用完全隔离的一套微服务体系会对用户带来高额的升级和切换成本。所以我们选择用 Dubbo3 替换之前的 DSF 框架作为数据面,将 Dubbo3 接入当前的微服务控制中心。同时要求 Dubbo3 支持原有 DSF 框架的私有协议,与 DSF 框架能够相互发现和调用,进一步降低用户升级成本。
这样用户在编程习惯上和 Dubbo3 的使用完全保持一致,在服务治理上(如上下负载、同中心路由、实例标签等功能)的使用与 DSF 保持一致。由于协议兼容,新的 Dubbo3 应用和原有 DSF 应用之间也能实现互相发现和调用。
要完成这个目标,需要去拓展 Dubbo3 SDK 的注册模块支持从现有的控制中心进行服务注册与发现、扩展自定义协议与 DSF 服务相互调用。借助于Dubbo 强大的插件机制,我们在没有修改 Dubbo 框架任何代码的基础上轻松地完成了这个目标,用户只需要引入 Dubbo 3.0 以上版本的 SDK 和我们开发的插件包即可。
整体的架构流程如下:
Dubbo3 落地的方案需要满足以下三点要求:
- Dubbo3 要接入现有的控制中心,由控制中心完成服务注册发现和服务治理功能
- Dubbo3 能够和 DSF 能够相互调用,满足此要求需要两个框架能够互相服务发现并且协议能够兼容
- 通过插件机制完成所有功能,不能修改 Dubbo 源码,用户可以自由地升级 Dubbo3 SDK 的版本
服务注册发现兼容
既然需要将 Dubbo3 的应用级注册接入到控制中心,而且需要与 DSF 服务进行服务发现,就需要了解 Dubbo3 应用级发现的流程才能对其进行更好的拓展。
应用级服务发现核心原理
我们从 Dubbo 最经典的工作原理图说起。Dubbo 从设计之初就内置了服务地址发现的能力,Provider 注册地址到注册中心,Consumer 通过订阅实时获取注册中心的地址更新,在收到地址列表后,Consumer 基于特定的负载均衡策略发起对 Provider 的 RPC 调用。
在这个过程中:
- 每个 Provider 通过特定的 key 向注册中心注册本机可访问地址
- 注册中心通过这个 key 对 Provider 实例地址进行聚合
- Consumer 通过同样的 key 从注册中心订阅,以便及时收到聚合后的地址列表
可以看到接口级服务发现是以接口为维度进行服务注册的,并在注册数据上携带了服务的配置和元数据。这种方式简单易用而且可以轻松实现应用、接口、方法粒度的服务治理。但由此带来的注册数据的放大问题会给注册中心造成较大压力,还有就是与现在云原生的服务模型并不兼容,不能与 Kubernetes 兼容。
面对这些不足,在 Dubbo3 架构下社区认真思考了两个问题:
- 如何在保留易用性、功能性的同时,重新组织 URL 地址数据,避免冗余数据的出现,让 Dubbo 3 能支撑更大规模集群水平扩容?
- 如何在地址发现层面与其他的微服务体系如 Kubernetes、Spring Cloud 打通?
最终,社区给出的方案也是非常巧妙和经典,将之前接口级服务的数据拆成两部分。属于实例模型的 ip 和 port 注册到注册中心,而属于业务属性的 RPC 元数据和 RPC 服务配置统一由 Dubbo Provider 的 MetadataService RPC 服务提供或者由元数据中心提供。在服务消费端和提供端之间建立了一条内置的 RPC 服务信息协商机制,也称为"服务自省"。全新的应用级服务发现模型,相比之前接口级别单机内存下降 50% 且极大的减少了注册中心的压力。
还有一个问题是换成了应用级服务发现之后,Consumer 是如何知道订阅的接口是属于哪一个服务的?因为 Dubbo 的编程模型是以接口为维度的。Dubbo3 提供了两种解决方案:一是从元数据中心存储接口和应用名的映射关系,二是通过 Consumer 在接口上通过 providerBy 配置手动指定服务提供方的名称。
兼容方案
如果是 Dubbo Consumer 调用 Dubbo Provider,我们只需要按部就班参考其他应用级别服务发现的插件比如 Zookeeper、Nacos 开发就可以完成此功能。如果 DSF Client 要调用 Dubbo Provider 我们是将兼容逻辑放在了控制中心,避免用户 SDK 的升级成本。剩下的兼容流程只有 Dubbo Consumer 调用 DSF Server 了,因为要求尽量不要修改 Dubbo 框架的源码,所以这里的兼容逻辑我们只能在注册中心的插件中完成。
上面介绍应用级服务发现的核心原理的时候提到应用级服务发现有 3 个关键的步骤:
- 通过元数据的 mapping 获取接口对应的服务名或者通过接口配置中的 providerBy 指定
- 通过服务名获取实例列表,实例以 ip、port 为维度,并且在实例信息中携带元数据的 revision
- 调用 Dubbo Provider 的 MetaService 获取实例的元数据信息,组装接口数据
Dubbo 服务调用 DSF 服务兼容流程的第一步非常简单,因为 DSF 并没有接口与服务名的 mapping 数据,所以通过 providerBy 指定接口所属的 DSF 服务名。第二步因为 DSF 服务的注册模型也是应用级的,实例的数据完全可以兼容这一部分也很简单。最关键的一步在于如何获取 DSF 服务的元数据,很显然现有的 DSF 服务并不具有 MetaService 的接口。
上面提到 Dubbo3 支持两种方式获取实例的元数据。默认就是从 Dubbo Provider 的 MetaService 获取实例的元数据信息,也支持从元数据中心获取实例的元数据信息,只需要将实例的 dubbo.metadata.storage-type 属性设置为 remote 即可。而 DSF 服务正好发布了 API 的契约数据到控制中心用作服务测试和寻址兼容,完全可以将 DSF 的契约数据转换为 Dubbo 的元数据的格式,满足服务发现的流程。
以下为 Dubbo Consumer 发现 DSF 服务的流程:
完成服务发现的兼容之后,用户在调用 DSF 服务的时候仅需要在接口上通过 providerBy 指定接口对应的服务即可,使用成本极低。
协议兼容与服务治理
协议兼容
完成服务发现的相互兼容之后,离 Dubbo 与 DSF 服务的相互调用的目标只剩最后一块拼图,在插件中实现 DSF 协议即可。
相比服务发现的各种数据兼容,协议的兼容比较清晰,只需要根据 Dubbo 协议扩展说明进行自定义协议扩展完成 DSF 数据格式兼容即可。
Dubbo 协议扩展需要实现以下接口:
- org.apache.dubbo.rpc.Exporter
- org.apache.dubbo.rpc.Invoker
- org.apache.dubbo.rpc.Protocol
当用户调用 refer() 所返回的 Invoker 对象的 invoke() 方法时,协议需相应执行同 URL 远端 export() 传入的 Invoker 对象的 invoke() 方法。其中,refer() 返回的 Invoker 由协议实现,协议通常需要在此 Invoker 中发送远程请求,export() 传入的 Invoker 由框架实现并传入,协议不需要关心。也就是说服务提供方在容器启动的时候就进行服务的暴露,而服务调用方需要通过协议进行Invoker的调用。我们的扩展如下:
最后完成的效果如下,用户只需要在 pom 中引入 Dubbo3 以上的任意版本和开发的插件,配置上指定注册中心地址和协议为 dsf 即可,其他使用方式和 Dubbo3 保持一致。
<properties> <dubbo.version>3.0.11</dubbo.version> <dubbo-dsf.version>1.0.0</dubbo-dsf.version> </properties> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>${dubbo.version}</version> </dependency> <dependency> <groupId>com.ly.dsf</groupId> <artifactId>dubbo-dsf-extensions-all</artifactId> <version>${dubbo-dsf.version}</version> </dependency>
配置文件:
# 注册地址为控制中心 dubbo.registry.address=dsf://{address} # 协议指定 dsf dubbo.protocol.name=dsf dubbo.consumer.protocol=dsf
服务治理
Dubbo3 有非常强大的流量治理的功能,同时我们内部的控制中心也有服务治理的功能,部分功能也有重合。
对于这部分的取舍,我们打算控制中心原有的功能对 Dubbo3 服务依然支持,如服务发现、同中心寻址、上下负载、服务测试等操作和之前保持一致。而 Dubbo 特有的服务治理功能如动态配置、Mesh 路由,我们将新增功能对其支持,保证 Dubbo3 功能的完整。
总结
Dubbo 3 是一个优秀的微服务框架,提供的 SPI 以及 Extension 机制能够非常方便的让用户去扩展实现想要功能。而且 Dubbo3 也更适应目前云原生的架构,Dubbo 3.1.x 版本支持 Sidecar 和 Proxyless 的 Mesh 方案,而且社区也在准备开源 Java Agent 方式的 Proxyless,这样就能较好的将微服务架框的 Framework 与数据面解耦,降低微服务框架的维护成本和升级成本。我们也会和社区一起探索,共建 Dubbo 社区的繁荣。