Dubbo3源码(二)升级到应用注册发现

1,388 阅读7分钟

背景

早在2.7.5,dubbo就已经支持应用级别注册发现了,这部分我们在之前提到过。

但是估计大部分人都没启用这个特性,而是在升级3.x之后才用起来。

本文基于dubbo3.1.8,分析2.xRPC服务注册发现升级到3.x应用注册发现。

目的是不想基于黑盒升级,要了解一下升级的底层实现

根据官网提供的升级步骤,我们需要通过配置中心控制整体迁移行为:

简单的修改 pom.xml 到最新版本就可以完成升级,如果要迁移到应用级地址,只需要调整开关控制 3.x 版本的默认行为。

  1. 升级 Provider 应用到最新 3.x 版本依赖,配置双注册开关dubbo.application.register-mode=all(建议通过全局配置中心设置,默认已自动开启),完成应用发布。
  2. 升级 Consumer 应用到最新 3.x 版本依赖,配置双订阅开关dubbo.application.service-discovery.migration=APPLICATION_FIRST(建议通过全局配置中心设置,默认已自动开启),完成应用发布。
  3. 在确认 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实际内部运作形式

双订阅不可避免的会增加消费端的内存消耗,但由于应用级地址发现在地址总量方面的优势,这个过程通常是可接受的,我们从两个方面进行分析:

  1. 双订阅带来的地址推送数据量增长。这点我们在 ”双注册资源消耗“ 一节中做过介绍,应用级服务发现带来的注册中心数据量增长非常有限。
  2. 双订阅带来的消费端内存增长。要注意双订阅只存在于启动瞬态,在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行为

MigrationInvokerClusterInvoker代理,根据一系列条件决策实际执行哪个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。

参考文献