Dubbo2.x迁移3.x过程及原理
- 1.Dubbo2.x迁移3.x
- 1.1 快速升级步骤
- 1.2 Provider 端升级过程详解
- 1.2.1 双注册带来的资源消耗
- 1.3 Consumer 端升级过程
- 1.3.1 APPLICATION_FIRST策略
- 1.3.2 双订阅带来的资源消耗
- 1.3.3 消费端更细粒度的控制
- 1.4 迁移状态的收敛
- 1.4.1 不同的升级策略影响很大
- 2. 迁移规则说明
- 2.1 状态模型
- 2.2 规则体说明
- 2.3 配置方式说明
- 2.3.1. 配置中心配置文件下发(推荐)
- 2.3.2 启动参数配置
- 2.3.3 本地文件配置
- 2.4 决策说明
- 2.4.1 阈值探测
- 2.4.2 灰度比例
- 2.5 切换过程说明
- 2.5.1 切换到应用级优先
- 2.5.2 应用级优先切换到强制
- 2.5.3 强制接口级和强制应用级互相切换
前言
Dubbo3 依旧保持了 2.x 的经典架构,以解决微服务进程间通信为主要职责,通过丰富的服务治理(如地址发现、流量管理等)能力来更好的管控微服务集群;Dubbo3 对原有框架的升级是全面的,体现在核心 Dubbo 特性的几乎每个环节,通过升级实现了稳定性、性能、伸缩性、易用性的全面提升。
1.Dubbo2.x迁移3.x
1.1 快速升级步骤
简单的修改 pom.xml 到最新版本就可以完成升级,如果要迁移到应用级地址,只需要调整开关控制 3.x 版本的默认行为。
- 升级 Provider 应用到最新 3.x 版本依赖,配置双注册开关dubbo.application.register-mode=all(建议通过全局配置中心设置,默认已自动开启),完成应用发布。
- 升级 Consumer 应用到最新 3.x 版本依赖,配置双订阅开关dubbo.application.service-discovery.migration=APPLICATION_FIRST(建议通过全局配置中心设置,默认已自动开启),完成应用发布。
- 在确认 Provider 的上有 Consumer 全部完成应用级地址迁移后,Provider 切到应用级地址单注册。完成升级
1.2 Provider 端升级过程详解
在不改变任何 Dubbo 配置的情况下,可以将一个应用或实例升级到 3.x 版本,升级后的 Dubbo 实例会默保保证与 2.x 版本的兼容性,即会正常注册 2.x 格式的地址到注册中心,因此升级后的实例仍会对整个集群仍保持可见状态。
同时新的地址发现模型(注册应用级别的地址)也将会自动注册。
通过 -D 参数,可以指定 provider 启动时的注册行为
-Ddubbo.application.register-mode=all
# 可选值 interface、instance、all,默认是 all,即接口级地址、应用级地址都注册
另外,可以在配置中心修改全局默认行为,来控制所有 3.x 实例注册行为。其中,全局性开关的优先级低于 -D 参数。
为了保证平滑迁移,即升级到 3.x 的实例能同时被 2.x 与 3.x 的消费者实例发现,3.x 实例需要开启双注册;当所有上游的消费端都迁移到 3.x 的地址模型后,提供端就可以切换到 instance 模式(只注册应用级地址)。对于如何升级消费端到 3.x 请参见下一小节。
1.2.1 双注册带来的资源消耗
双注册不可避免的会带来额外的注册中心存储压力,但考虑到应用级地址发现模型的数据量在存储方面的极大优势,即使对于一些超大规模集群的用户而言,新增的数据量也并不会带来存储问题。总体来说,对于一个普通集群而言,数据增长可控制在之前数据总量的 1/100 ~ 1/1000
以一个中等规模的集群实例来说: 2000 实例、50个应用(500 个 Dubbo 接口,平均每个应用 10 个接口)。
假设每个接口级 URL 地址平均大小为 5kb,每个应用级 URL 平均大小为 0.5kb
老的接口级地址量:2000 * 500 * 5kb ≈ 4.8G
新的应用级地址量:2000 * 50 * 0.5kb ≈ 48M
双注册后仅仅增加了 48M 的数据量。
1.3 Consumer 端升级过程
对于 2.x 的消费者实例,它们看到的自然都是 2.x 版本的提供者地址列表;
对于 3.x 的消费者,它具备同时发现 2.x 与 3.x 提供者地址列表的能力。在默认情况下,如果集群中存在可以消费的 3.x 的地址,将自动消费 3.x 的地址,如果不存在新地址则自动消费 2.x 的地址。Dubbo3 提供了开关来控制这个行为:
dubbo.application.service-discovery.migration=APPLICATION_FIRST
# 可选值
# FORCE_INTERFACE,只消费接口级地址,如无地址则报错,单订阅 2.x 地址
# APPLICATION_FIRST,智能决策接口级/应用级地址,双订阅
# FORCE_APPLICATION,只消费应用级地址,如无地址则报错,单订阅 3.x 地址
dubbo.application.service-discovery.migration
支持通过 -D 以及 全局配置中心 两种方式进行配置。
接下来,我们就具体看一下,如何通过双订阅模式(APPLICATION_FIRST)让升级到 3.x 的消费端迁移到应用级别的地址。在具体展开之前,先明确一条消费端的选址行为:对于双订阅的场景,消费端虽然可同时持有 2.x 地址与 3.x 地址,但选址过程中两份地址是完全隔离的:要么用 2.x 地址,要么用 3.x 地址,不存在两份地址混合调用的情况,这个决策过程是在收到第一次地址通知后就完成了的。
1.3.1 APPLICATION_FIRST策略
首先,提前在全局配置中心 Nacos 配置一条配置项(所有消费端都将默认执行这个选址策略):
紧接着,升级消费端到 3.x 版本并启动,这时消费端读取到APPLICATION_FIRST配置后,执行双订阅逻辑
(订阅 2.x 接口级地址与 3.x 应用级地址)
至此,升级操作就完成了,剩下的就是框架内部的执行了。在调用发生前,框架在消费端会有一个“选址过程”,注意这里的选址和之前 2.x 版本是有区别的,选址过程包含了两层筛选:
- 先进行地址列表(ClusterInvoker)筛选(接口级地址 or 应用级地址)
- 再进行实际的 provider 地址(Invoker)筛选。
ClusterInvoker 筛选的依据,可以通过 MigrationAddressComparator SPI 自行定义,目前官方提供了一个简单的地址数量比对策略,即当 应用级地址数量 == 接口级地址数量 满足时则进行迁移。
其实 FORCE_INTERFACE、APPLICATION_FIRST、FORCE_APPLICATION 控制的都是这里的 ClusterInvoker 类型的筛选策略
1.3.2 双订阅带来的资源消耗
双订阅不可避免的会增加消费端的内存消耗,但由于应用级地址发现在地址总量方面的优势,这个过程通常是可接受的,我们从两个方面进行分析:
- 双订阅带来的地址推送数据量增长。这点我们在 ”2.1 双注册带来的资源消耗“ 中做过介绍,应用级服务发现带来的注册中心数据量增长非常有限。
- 双订阅带来的消费端内存增长。要注意双订阅只存在于启动瞬态,在ClusterInvoker选址决策之后其中一份地址就会被完全销毁;对单个服务来说,启动阶段双订阅带来的内存增长大概能控制在原内存量的 30% ~ 40%,随后就会下降到单订阅水平,如果切到应用级地址,能实现内存 50% 的下降。
1.3.3 消费端更细粒度的控制
除了全局的迁移策略之外,Dubbo 在消费端提供了更细粒度的迁移策略支持。控制单位可以是某一个消费者应用,它消费的服务A、服务B可以有各自独立的迁移策略,具体是用方式是在消费端配置迁移规则:
key: demo-consumer
step: APPLICATION_FIRST
applications:- name: demo-providerstep: FORCE_APPLICATION
services:- serviceKey: org.apache.dubbo.config.api.DemoService:1.0.0step: FORCE_INTERFACE
使用这种方式能做到比较精细迁移控制,但是当下及后续的改造成本会比较高,除了一些特别场景,我们不太推荐启用这种配置方式。 迁移指南官方推荐使用的全局的开关式的迁移策略,让消费端实例在启动阶段自行决策使用哪份可用的地址列表。
1.4 迁移状态的收敛
为了同时兼容 2.x 版本,升级到 3.x 版本的应用在一段时间内要么处在双注册状态,要么处在双订阅状态。
解决这个问题,我们还是从 Provider 视角来看,当所有的 Provider 都切换到应用级地址注册之后,也就不存在双订阅的问题了。
1.4.1 不同的升级策略影响很大
毫无疑问越早越彻底的升级,就能尽快摆脱这个局面。设想,如果可以将组织内所有的应用都升级到 3.x 版本,则版本收敛就变的非常简单:升级过程中 Provider 始终保持双注册,当所有的应用都升级到 3.x 之后,就可以调整全局默认行为,让 Provider 都变成应用级地址单注册了,这个过程并不会给 Consumer 应用带来困扰,因为它们已经是可以识别应用级地址的 3.x 版本了。
如果没有办法做到应用的全量升级,甚至在相当长的时间内只能升级一部分应用,则不可避免的迁移状态要持续比较长的时间。 在这种情况下,我们追求的只能是尽量保持已升级应用的上下游实现版本及功能收敛。推动某些 Provider 的上游消费者都升级到 Dubbo3,这样就可以解除这部分 Provider 的双注册,要做到这一点,可能需要一些辅助统计工具的支持。
- 要能分析出应用间的依赖关系,比如一个 Provdier 应用被哪些消费端应用消费,这可以通过 Dubbo 提供的服务元数据上报能力来实现。
2.要能知道每个应用当前使用的 dubbo 版本,可以通过扫描或者主动上报手段。
2. 迁移规则说明
2.1 状态模型
在 Dubbo 3 之前地址注册模型是以接口级粒度注册到注册中心的,而 Dubbo 3 全新的应用级注册模型注册到注册中心的粒度是应用级的。从注册中心的实现上来说是几乎不一样的,这导致了对于从接口级注册模型获取到的 invokers 是无法与从应用级注册模型获取到的 invokers 进行合并的。为了帮助用户从接口级往应用级迁移,Dubbo 3 设计了 Migration 机制,基于三个状态的切换实现实际调用中地址模型的切换。
当前共存在三种状态,FORCE_INTERFACE(强制接口级),APPLICATION_FIRST(应用级优先)、FORCE_APPLICATION(强制应用级)。
- FORCE_INTERFACE:只启用兼容模式下接口级服务发现的注册中心逻辑,调用流量 100% 走原有流程
- APPLICATION_FIRST:开启接口级、应用级双订阅,运行时根据阈值和灰度流量比例动态决定调用流量走向
- FORCE_APPLICATION:只启用新模式下应用级服务发现的注册中心逻辑,调用流量 100% 走应用级订阅的地址
2.2 规则体说明
规则采用 yaml 格式配置,具体配置下参考如下:
key: 消费者应用名(必填)
step: 状态名(必填)
threshold: 决策阈值(默认1.0)
proportion: 灰度比例(默认100)
delay: 延迟决策时间(默认0)
force: 强制切换(默认 false)
interfaces: 接口粒度配置(可选)- serviceKey: 接口名(接口 + : + 版本号)(必填)threshold: 决策阈值proportion: 灰度比例delay: 延迟决策时间force: 强制切换step: 状态名(必填)- serviceKey: 接口名(接口 + : + 版本号)step: 状态名
applications: 应用粒度配置(可选)- serviceKey: 应用名(消费的上游应用名)(必填)threshold: 决策阈值proportion: 灰度比例delay: 延迟决策时间force: 强制切换step: 状态名(必填)
参数说明:
- key: 消费者应用名
- step: 状态名(FORCE_INTERFACE、APPLICATION_FIRST、FORCE_APPLICATION)
- threshold: 决策阈值(浮点数,具体含义参考后文)
- proportion: 灰度比例(0~100,决定调用次数比例)
- delay: 延迟决策时间(延迟决策的时间,实际等待时间为 1~2 倍 delay 时间,取决于注册中心第一次通知的时间,对于目前 Dubbo 的注册中心实现次配置项保留 0 即可)
- force: 强制切换(对于 FORCE_INTERFACE、FORCE_APPLICATION 是否不考虑决策直接切换,可能导致无地址调用失败问题)
- interfaces: 接口粒度配置
参考配置示例如下:
key: demo-consumer
step: APPLICATION_FIRST
threshold: 1.0
proportion: 60
delay: 0
force: false
interfaces:- serviceKey: DemoService:1.0.0threshold: 0.5proportion: 30delay: 0force: truestep: APPLICATION_FIRST- serviceKey: GreetingService:1.0.0step: FORCE_APPLICATION
2.3 配置方式说明
2.3.1. 配置中心配置文件下发(推荐)
- Key: 消费者应用名 + “.migration”
- Group: DUBBO_SERVICEDISCOVERY_MIGRATION
配置项内容参考上一节
程序启动时会拉取此配置作为最高优先级启动项,当配置项为启动项时不执行检查操作,直接按状态信息达到终态。 程序运行过程中收到新配置项将执行迁移操作,过程中根据配置信息进行检查,如果检查失败将回滚为迁移前状态。迁移是按接口粒度执行的,也即是如果一个应用有 10 个接口,其中 8 个迁移成功,2 个失败,那终态 8 个迁移成功的接口将执行新的行为,2 个失败的仍为旧状态。如果需要重新触发迁移可以通过重新下发规则达到。
注:如果程序在迁移时由于检查失败回滚了,由于程序无回写配置项行为,所以如果此时程序重启了,那么程序会直接按照新的行为不检查直接初始化。
2.3.2 启动参数配置
- 配置项名:dubbo.application.service-discovery.migration
- 允许值范围:FORCE_INTERFACE、APPLICATION_FIRST、FORCE_APPLICATION
此配置项可以通过环境变量或者配置中心传入,启动时优先级比配置文件低,也即是当配置中心的配置文件不存在时读取此配置项作为启动状态。
2.3.3 本地文件配置
配置项名 | 默认值 | 说明 |
---|---|---|
dubbo.migration.file | dubbo-migration.yaml | 本地配置文件路径 |
dubbo.application.migration.delay | 60000 | 配置文件延迟生效时间(毫秒) |
配置文件中格式与前文提到的规则一致
本地文件配置方式本质上是一个延时配置通知的方式,本地文件不会影响默认启动方式,当达到延时时间后触发推送一条内容和本地文件一致的通知。这里的延时时间与规则体中的 delay 字段无关联。 本地文件配置方式可以保证启动以默认行为初始化,当达到延时时触发迁移操作,执行对应的检查,避免启动时就以终态方式启动。
2.4 决策说明
2.4.1 阈值探测
阈值机制旨在进行流量切换前的地址数检查,如果应用级的可使用地址数与接口级的可用地址数对比后没达到阈值将检查失败。
核心代码如下:
if (((float) newAddressSize / (float) oldAddressSize) >= threshold) {return true;
}
return false;
同时 MigrationAddressComparator 也是一个 SPI 拓展点,用户可以自行拓展,所有检查的结果取交集。
2.4.2 灰度比例
灰度比例功能仅在应用级优先状态下生效。此功能可以让用户自行决定调用往新模式应用级注册中心地址的调用数比例。灰度生效的前提是满足了阈值探测,在应用级优先状态下,如果阈值探测通过,currentAvailableInvoker 将被切换为对应应用级地址的 invoker;如果探测失败 currentAvailableInvoker 仍为原有接口级地址的 invoker。
流程图如下:
探测阶段
调用阶段
核心代码如下:
// currentAvailableInvoker is based on MigrationAddressComparator's result
if (currentAvailableInvoker != null) {if (step == APPLICATION_FIRST) {// call ratio calculation based on random valueif (ThreadLocalRandom.current().nextDouble(100) > promotion) {return invoker.invoke(invocation);}}return currentAvailableInvoker.invoke(invocation);
}
2.5 切换过程说明
地址迁移过程中涉及到了三种状态的切换,为了保证平滑迁移,共有 6 条切换路径需要支持,可以总结为从强制接口级、强制应用级往应用级优先切换;应用级优先往强制接口级、强制应用级切换;还有强制接口级和强制应用级互相切换。 对于同一接口切换的过程总是同步的,如果前一个规则还未处理完就收到新规则则回进行等待。
2.5.1 切换到应用级优先
从强制接口级、强制应用级往应用级优先切换本质上是从某一单订阅往双订阅切换,保留原有的订阅并创建另外一种订阅的过程。这个切换模式下规则体中配置的 delay 配置不会生效,也即是创建完订阅后马上进行阈值探测并决策选择某一组订阅进行实际优先调用。由于应用级优先模式是支持运行时动态进行阈值探测,所以对于部分注册中心无法启动时即获取全量地址的场景在全部地址通知完也会重新计算阈值并切换。 应用级优先模式下的动态切换是基于服务目录(Directory)的地址监听器实现的。
2.5.2 应用级优先切换到强制
应用级优先往强制接口级、强制应用级切换的过程是对双订阅的地址进行检查,如果满足则对另外一份订阅进行销毁,如果不满足则回滚保留原来的应用级优先状态。 如果用户希望这个切换过程不经过检查直接切换可以通过配置 force 参数实现。
2.5.3 强制接口级和强制应用级互相切换
强制接口级和强制应用级互相切换需要临时创建一份新的订阅,判断新的订阅(即阈值计算时使用新订阅的地址数去除旧订阅的地址数)是否达标,如果达标则进行切换,如果不达标会销毁这份新的订阅并且回滚到之前的状态。