背景
早在2.7.5,dubbo就已经支持应用级别注册发现了,这部分我们在之前提到过。
但是估计大部分人都没启用这个特性,而是在升级3.x之后才用起来。
本文基于dubbo3.1.8,分析2.xRPC服务注册发现升级到3.x应用注册发现。
目的是不想基于黑盒升级,要了解一下升级的底层实现。
根据官网提供的升级步骤,我们需要通过配置中心控制整体迁移行为:
简单的修改 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)provider开启双注册,配置项dubbo.application.register-mode:
- all-双注册(默认)
- interface-接口注册(老逻辑)
- instance-应用注册(新逻辑)
2)consumer开启双订阅,配置项dubbo.application.service-discovery.migration:
- APPLICATION_FIRST-应用优先其次接口(默认)
- FORCE_INTERFACE(强制走接口)
- FORCE_APPLICATION(强制走应用)
3)provider切换为应用注册,即dubbo.application.register-mode=instance
需要注意的是,最终consumer以双订阅形式存在,是否会有性能损耗?
根据官网描述,如果上述步骤全部完成,接口级别注册实例不会存在,实际上不会有什么消耗。
这里我们需要关注一下在APPLICATION_FIRST状态下,consumer实际内部运作形式。
双订阅不可避免的会增加消费端的内存消耗,但由于应用级地址发现在地址总量方面的优势,这个过程通常是可接受的,我们从两个方面进行分析:
- 双订阅带来的地址推送数据量增长。这点我们在 ”双注册资源消耗“ 一节中做过介绍,应用级服务发现带来的注册中心数据量增长非常有限。
- 双订阅带来的消费端内存增长。要注意双订阅只存在于启动瞬态,在ClusterInvoker选址决策之后其中一份地址就会被完全销毁;对单个服务来说,启动阶段双订阅带来的内存增长大概能控制在原内存量的 30% ~ 40%,随后就会下降到单订阅水平,如果切到应用级地址,能实现内存 50% 的下降。
除了上述快速升级流程外,对于consumer还支持更丰富的切换流程。
dubbo3还支持更丰富的迁移策略,支持决策阈值、灰度比例、接口粒度、应用粒度。
题外话:接口和应用粒度切换还是比较实用的,所有业务线都在一个时间点配合做基础框架升级,在笔者公司就比较难。
官网提供的yaml模板:
key: 消费者应用名(必填)
step: 状态名(必填)
threshold: 决策阈值(默认1.0)
proportion: 灰度比例(默认100)
delay: 延迟决策时间(默认0)
force: 强制切换(默认 false)
interfaces: 接口粒度配置(可选)
- serviceKey: 接口名(接口 + : + 版本号)(必填)
threshold: 决策阈值
proportion: 灰度比例
delay: 延迟决策时间
force: 强制切换
step: 状态名(必填)
applications: 应用粒度配置(可选)
- serviceKey: 应用名(消费的上游应用名)(必填)
threshold: 决策阈值
proportion: 灰度比例
delay: 延迟决策时间
force: 强制切换
step: 状态名(必填)
如果配置中心用nacos,group=DUBBO_SERVICEDISCOVERY_MIGRATION,dataId=应用名.migration。
决策阈值和灰度比例对于日常系统迁移是否有指导作用?可以针对不止是MigrationStep迁移。
public enum MigrationStep {
FORCE_INTERFACE,
APPLICATION_FIRST,
FORCE_APPLICATION
}
案例
这里按照官方建议的三步切换,我们直接走二三两步,provider直接切换为应用级别注册。
注:这里不同于前面用zk,这边用nacos做注册配置中心。
服务提供者:
注:dataId=dubbo.properties,group=upgrade-provider。
// nacos发布配置 切换为应用级别注册
Properties properties = new Properties();
properties.put("serverAddr", "127.0.0.1");
NacosConfigService nacosConfigService = new NacosConfigService(properties);
nacosConfigService.publishConfig("dubbo.properties", "upgrade-provider",
"dubbo.application.register-mode=instance");
nacosConfigService.shutDown();
// 启动provider
ApplicationConfig providerApp = new ApplicationConfig("upgrade-provider");
ServiceConfig<DemoServiceImpl> service = new ServiceConfig<>();
service.setInterface(DemoService.class);
service.setRef(new DemoServiceImpl());
DubboBootstrap.newInstance().application(providerApp)
.registry(new RegistryConfig("nacos://127.0.0.1:8848"))
.protocol(new ProtocolConfig(CommonConstants.DUBBO, -1))
.service(service)
.start();
服务消费者:
这里不用dubbo.application.service-discovery.migration=APPLICATION_FIRST这种全局配置方式。
用yaml配置支持更多高级特性。
注:dataId=upgrade-consumer.migration,group=DUBBO_SERVICEDISCOVERY_MIGRATION。
static String yaml = "key: upgrade-consumer\n" +
"step: APPLICATION_FIRST\n" +
"threshold: 1.0\n" +
"proportion: 100\n" +
"delay: 0\n" +
"force: false\n";
public static void main(String[] args) throws InterruptedException, NacosException {
Properties properties = new Properties();
properties.put("serverAddr", "127.0.0.1");
NacosConfigService nacosConfigService = new NacosConfigService(properties);
// 发布yaml高级迁移规则
nacosConfigService.publishConfig("upgrade-consumer.migration", "DUBBO_SERVICEDISCOVERY_MIGRATION", yaml);
// 这种方式配置,不支持动态更新
// nacosConfigService.publishConfig("dubbo.properties", "upgrade-consumer",
// "dubbo.application.service-discovery.migration=APPLICATION_FIRST");
nacosConfigService.shutDown();
ReferenceConfig<DemoService> reference = new ReferenceConfig<>();
reference.setInterface(DemoService.class);
reference.setCheck(false);
ApplicationConfig applicationConfig = new ApplicationConfig("upgrade-consumer");
DubboBootstrap.newInstance().application(applicationConfig)
.registry(new RegistryConfig("nacos://127.0.0.1:8848"))
.protocol(new ProtocolConfig(CommonConstants.DUBBO, -1))
.reference(reference)
.start();
}
Provider双注册
本章关注点主要在consumer侧,因为provider侧双注册逻辑较为简单。
ServiceConfig#export,
配置registerMode=all,会生成两个注册url(registry协议和service-discovery-registry协议),
而配置registerMode=instance,只会有一个注册url(service-discovery-registry协议)。
ConfigValidationUtils#loadRegistries:
所以本质上双注册是属于多注册中心的一种特例。
Consumer引入
回顾Cluster/Directory/Invoker
在Dubbo2源码阅读中提到过这三者的关系。
Directory可以理解为内存注册表,包含多个底层DubboInvoker;
而Cluster#join将Directory转换为一个特殊的ClusterInvoker,暴露给上层;
在运行时,ClusterInvoker包含路由、集群容错、负载均衡等逻辑,从Directory中选择一个底层DubboInvoker执行。
如果理解了这三者之间的关系,那么3.0的迁移策略就很容易理解了。
APPLICATION_FIRST的含义是:优先走应用降级走接口。
那么底层对于一个rpc服务一定要有两套Directory内存注册表,一个管理应用级别所有providerUrl,另一个管理接口级别。
如果有两套Directory,在不影响上层业务逻辑的情况下,ClusterInvoker要包含代理逻辑,即什么时候走应用Directory,什么时候走接口Directory。
回顾RegistryProtocol
回顾2.7.6版本RegistryProtocol#doRefer:
1)构造RegistryDirectory,执行RegistryDirectory#subscribe方法订阅rpc服务;
从注册中心捞了providerUrl列表,转换为invokers存储到Directory
2)Cluster#join将RegistryDirectory转换为一个ClusterInvoker;
ClusterInvoker持有Directory,负责从Directory中选Invoker执行
两种RegistryProtocol
RegistryProtocol在3.x中有两种。
RegistryProtocol对应2.7.x中开启应用注册发现的情况。
即dubbo源码分析第四章中描述的,配置registry-type=service。
而默认情况下,2.x都是接口级别注册发现,都会走InterfaceCompatibleRegistryProtocol,这也是本章分析的重点。
InterfaceCompatibleRegistryProtocol没有重写refer方法,只是重写了一些getInvoker方法,说明主流程和RegistryProtocol一致。
这里有三种invoker,先简单介绍一下:
1)getInvoker:接口级别ClusterInvoker,后文简称接口invoker;
2)getServiceDiscoveryInvoker:应用级别ClusterInvoker,后文简称应用invoker;
3)getMigrationInvoker:上述两个ClusterInvoker前面的Proxy,后文简称代理invoker;
主流程
相较于2.7.6,主流程在RegistryProtocol#doRefer中并没有大改动,这也是各种扩展点带来的优势。
这里分为两步,第一步创建ClusterInvoker。
InterfaceCompatibleRegistryProtocol#getMigrationInvoker返回MigrationInvoker。
按照2.7.6的逻辑,在这一步需要执行subscribe订阅,将注册中心的providerUrl转换为invoker存储到Directory,然后调用Cluster#join转换为ClusterInvoker返回。
第二步执行 RegistryProtocolListener#onRefer。
这一步在之前的分析中从来没有提过,因为以前没有关键逻辑。
这里MigrationRuleListener#onRefer执行了很关键的逻辑,稍候分析。
MigrationInvoker
MigrationInvoker实现MigrationClusterInvoker,支持根据迁移规则Step迁移Invoker。
成员变量如下:
其中关键属性有几个:
- migrationRuleListener:迁移规则监听器;
- invoker:接口ClusterInvoker(2.x);
- serviceDiscoveryInvoker:应用ClusterInvoker(3.x);
- currentAvailableInvoker:根据迁移规则决策当前使用的Invoker;
- MigrationRule:yaml配置的迁移规则;
- MigrationStep:迁移步骤,关注APPLICATION_FIRST;
构造阶段只是普通成员变量赋值,没有特殊操作,所以subscribe订阅并不在这一步触发。
注:这里两个invoker入参为null,构造阶段没有一个invoker是可用的。
MigrationRuleListener
MigrationRuleListener实现两个接口:
- RegistryProtocolListener:监听RegistryProtocol#refer构造ClusterInvoker完成,触发onRefer,
- ConfigurationListener:监听DynamicConfiguration配置中心配置变化,触发process;
核心成员变量:
SPI加载触发MigrationRuleListener构造。
构造阶段MigrationRuleListener监听MigrationRule并缓存到本地。
注:这也是为什么dubbo.application.service-discovery.migration这类配置无法动态生效的原因,底层只支持group=DUBBO_SERVICEDISCOVERY_MIGRATION这种yaml配置的MigrationRule动态生效。
回到主流程,在构建完MigrationInvoker后,进入MigrationRuleListener#onRefer:
1)构造MigrationRuleHandler,缓存MigrationInvoker和MigrationRuleHandler的映射关系;
MigrationRuleListener是业务代码中的controller,接收onRefer和config变更请求;
MigrationRuleHandler是业务代码中的service,负责实际执行MigrationRule变更;
MigrationInvoker的行为由MigrationRuleHandler根据MigrationRule决策;
2)执行MigrationRuleHandler#doMigrate,初始化所有底层ClusterInvoker;
MigrationRuleHandler
MigrationRuleHandler#doMigrate:无论是onRefer还是配置中心配置变更,都会走这里。
特别提一下MigrationRule#getStep这个方法,这里是获取迁移Step的核心逻辑。
优先级从高到低如下,只有MigrationRule是可以动态感知变更的:
- MigrationRule下interface级别step
- MigrationRule下application级别step
- MigrationRule的step(this.step)
- dubbo.application.migration.step
- dubbo.application.service-discovery.migration
- 默认APPLICATION_FIRST
注释也说了,这边初始化全局Step,实在很难follow,晕厥。
MigrationRuleHandler#refreshInvoker:根据Step不同,调用MigrationInvoker刷新底层Invoker。
注意:只有step和threshold发生变更,或初始化时,才会刷新invoker。
APPLICATION_FIRST原理
接下来关注APPLICATION_FIRST的原理。
MigrationInvoker#migrateToApplicationFirstInvoker:
1)刷新接口ClusterInvoker,注册接口Directory监听;
2)刷新应用ClusterInvoker,注册应用Directory监听;
3)计算用哪个Invoker;
接口ClusterInvoker
MigrationInvoker#refreshInterfaceInvoker:刷新接口ClusterInvoker。
1)如果接口ClusterInvoker之前有监听,先清除;
2)创建接口ClusterInvoker
InterfaceCompatibleRegistryProtocol#getInvoker:
核心在RegistryProtocol#doCreateInvoker:
这里终于看到了2.x订阅和Cluster#join等操作,至此Directory内存注册表也构建完成了。
3)注册Directory监听
后续如果内存注册表发生变更(比如实例上下线),会重新触发calcPreferredInvoker计算。
应用ClusterInvoker
MigrationInvoker#refreshServiceDiscoveryInvoker:刷新应用ClusterInvoker。
流程和上面接口ClusterInvoker一致。
区别在于InterfaceCompatibleRegistryProtocol#getServiceDiscoveryInvoker:
1)getRegistry:返回应用级别ServiceDiscoveryRegistry,而不是用原始NacosRegistry(接口注册中心);
2)DynamicDirectory:使用应用级别ServiceDiscoveryRegistryDirectory;
和接口ClusterInvoker一样,RegistryProtocol#doCreaetInvoker创建ClusterInvoker,只不过Directory注册表和Registry不同。
阈值决策
在内存注册表变更时,根据阈值MigrationRule配置的threshold,决策底层ClusterInvoker。
MigrationInvoker#calcPreferredInvoker:
当APPLICATION_FIRST,注册表变更都会走这里决策当前invoker指向接口invoker还是应用invoker。
需要所有MigrationAddressComparator都认为可以切换为应用级别invoker,当前invoker才能指向应用级别invoker。
DefaultMigrationAddressComparator是仅有的实现。
根据两个ClusterInvoker目前持有的底层invoker数量来决策是否可以切换。
case1:应用invoker的内存注册表中没有invokers。
不允许切换,使用接口invoker。
case2:应用invoker有invokers,但是接口invoker没有invokers。
直接切换,使用应用invoker。
case3:应用ClusterInvoker和接口ClusterInvoker都有invokers。
根据MigrationRule的threshold来决策是否切换。
如果应用Invokers数量/接口Invokers数量>=threshold,就允许使用应用invoker,否则使用接口invoker。
注:我这里看到threshold的默认阈值是0.0,并不是官网的1.0。
也就是说,默认情况下,应用级别注册provider数量大于0,就使用应用ClusterInvoker。
并非1.0情况下,应用invokers数量超过接口invokers数量才使用应用ClusterInvoker。
可以想像,当provider上下线过程中,注册表的变更会影响APPLICATION_FIRST的决策:
- 如果没有应用级别provider,将会自动切换为接口级别provider;
- 如果有应用级别provider且超过一定数量,根据threshold自动切换为应用级别provider;
灰度决策
在APPLICATION_FIRST的情况下,运行时执行哪个ClusterInvoker还可以通过灰度值来干预。
灰度值默认100,代表禁用灰度逻辑,如果灰度值小于100,每次生成随机数,超出灰度值则走接口ClusterInvoker。
比如配置灰度值60,
随机数为80,则走接口ClusterInvoker,即使当前接口注册表里没有invokers;
随机数为50,则走decideInvoker决策以后,选择ClusterInvoker执行。
MigrationInvoker#invoke:代理方法,使用底层某个ClusterInvoker执行。
兜底决策
MigrationInvoker#decideInvoker:即使没有灰度策略,也需要二次判断应用级别ClusterInvoker是否可用。
如果应用ClusterInvoker不可用,还是要走接口ClusterInvoker。
DynamicDirectory#isAvailable:判断是否存在一个DubboInvoker可用。
DubboInvoker#isAvailable:如果有一个长连接是存活的,就返回true。(以dubbo协议为例)
FORCE_APPLICATION原理
虽然双订阅消耗不大,但是最终还是切换为FORCE_APPLICATION更合适吧。
MigrationInvoker#migrateToForceApplicationInvoker:切换为强制应用ClusterInvoker。
1)刷新应用ClusterInvoker(同APPLICATION_FIRST,只不过传入CountDownLatch是1);
2)如果接口invoker不存在(比如启动时就用的FORCE_APPLICATION),直接切换成功;
3)如果delay,可以等待一会,尽量等ClusterInvoker里的Directory刷新完毕;
4)如果force,强制切换,销毁接口invoker;
5)如果threshold通过,切换并销毁接口invoker;
5)否则不切换,且如果之前step是force_interface需要回滚销毁应用invoker;
MigrationInvoker#invoke:运行期间,FORCE切换后不会做任何灰度或兜底决策,直接执行应用ClusterInvoker。
总结
本章我们分析了2.x升级到3.0应用级别注册发现的相关源码,
避免在不理解原理的情况下升级3.0造成一些麻烦。
provider
provider默认双注册,即register-mode=all,底层会生成两个注册url,可以认为是多注册中心的特例。
如果consumer升级完毕,provider可以切换register-mode=application,这样底层就只有一个注册url了。
consumer
重点在于consumer侧的升级,consumer默认双注册,即step=APPLICATION_FIRST。
从配置角度来说,有两类配置项:
- 全局kv配置:不能动态更新,只能控制step迁移步骤,默认APPLICATION_FIRST;
- 高级MigrationRule配置:支持配置step、阈值、灰度,支持应用粒度和接口粒度控制;
consumer侧有三个核心类。
MigrationRuleListener,负责监听服务引用和配置变更,解析MigrationRule交给MigrationRuleHandler处理。
MigrationRuleHandler,判断MigrationRule是否变更(step/threshold),根据step不同切换MigrationInvoker行为。
MigrationInvoker,ClusterInvoker代理,根据一系列条件决策实际执行哪个ClusterInvoker。
APPLICATIONF_FIRST
MigrationInvoker持有接口ClusterInvoker和应用ClusterInvoker。
threshold阈值,决策当前currentInvoker(指针),如果内存注册表(Directory)中应用invokers数量/接口invokers数量>=threshold,则使用应用Invoker,否则使用接口Invoker。如果注册表发生变更,这里指针也可能发生变更。
proportion灰度值,运行时决策是否执行应用invoker(假设current指向应用invoker),默认100。
假设灰度值=80,如果随机数大于80则走接口invoker;
如果随机数小于等于80,需要判断底层是否存在一个能够正常通讯的应用invoker,如果不存在,还是走接口invoker,如果存在,才会走应用invoker。
FORCE_APPLICATION
虽然消费者双订阅可能消耗不大,但是切换为FORCE_APPLICATION可能更好。
切换FORCE_APPLICATION成功,底层只会存在一个应用ClusterInvoker。
支持force强制切换,不经过threshold阈值决策,强制使用应用ClusterInvoker。
支持threshold阈值决策,同APPLICATION_FIRST。
运行期间不会存在灰度和兜底决策逻辑,直接执行应用ClusterInvoker。