dubbo 服务消费原理分析之应用级服务发现

文章目录


前言

文章基于3.1.0版本进行分析

java 复制代码
		<dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>3.1.0</version>
        </dependency>

一、MigrationRuleListener

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、Provider 端升级

在不改变任何 Dubbo 配置的情况下,可以将一个应用或实例升级到 3.x 版本,升级后的 Dubbo 实例会默保保证与 2.x 版本的兼容性,即会正常注册 2.x 格式的地址到注册中心,因此升级后的实例仍会对整个集群仍保持可见状态。

3、Consumer 端升级

对于双订阅的场景,消费端虽然可同时持有 2.x 地址与 3.x 地址,但选址过程中两份地址是完全隔离的:要么用 2.x 地址,要么用 3.x 地址,不存在两份地址混合调用的情况,这个决策过程是在收到第一次地址通知后就完成了的。

4、服务消费选址

调用发生前,框架在消费端会有一个"选址过程",注意这里的选址和之前 2.x 版本是有区别的,选址过程包含了两层筛选

  • 先进行地址列表(ClusterInvoker)筛选(接口级地址 or 应用级地址)
  • 再进行实际的 provider 地址(Invoker)筛选

双注册带来的资源消耗

双注册不可避免的会带来额外的注册中心存储压力,但考虑到应用级地址发现模型的数据量在存储方面的极大优势,即使对于一些超大规模集群的用户而言,新增的数据量也并不会带来存储问题。总体来说,对于一个普通集群而言,数据增长可控制在之前数据总量的 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 的数据量。

接下来分析MigrationRuleListener的实际处理逻辑

5、MigrationRuleListener.onRefer

java 复制代码
    @Override
    public void onRefer(RegistryProtocol registryProtocol, ClusterInvoker<?> invoker, URL consumerUrl, URL registryURL) {
		MigrationRuleHandler<?> migrationRuleHandler = handlers.computeIfAbsent((MigrationInvoker<?>) invoker, _key -> {
            ((MigrationInvoker<?>) invoker).setMigrationRuleListener(this);
            return new MigrationRuleHandler<>((MigrationInvoker<?>) invoker, consumerUrl);
        });

        // 迁移规则执行 rule是封装了迁移的配置规则的信息对应类型MigrationRule类型,在初始化对象的时候进行了配置初始化
		migrationRuleHandler.doMigrate(rule);
    }

6、MigrationRuleHandler.doMigrate

从url中读取迁移方式和阈值,用于进行迁移的调用决策

  • 获取迁移方式
  • 获取决策阈值(默认-1.0)
java 复制代码
	public synchronized void doMigrate(MigrationRule rule) {
        if (migrationInvoker instanceof ServiceDiscoveryMigrationInvoker) {
            refreshInvoker(MigrationStep.FORCE_APPLICATION, 1.0f, rule);
            return;
        }

        // initial step : APPLICATION_FIRST
		// 迁移方式,MigrationStep 一共有3种枚举情况:FORCE_INTERFACE, APPLICATION_FIRST, FORCE_APPLICATION
		// 默认 APPLICATION_FIRST 开启接口级、应用级双订阅
        MigrationStep step = MigrationStep.APPLICATION_FIRST;
        float threshold = -1f;

        try {	
			// URL中获取迁移方式
			step = rule.getStep(consumerURL);
			// threshold: 决策阈值(默认-1.0)计算与获取
            threshold = rule.getThreshold(consumerURL);
        } catch (Exception e) {
            logger.error("Failed to get step and threshold info from rule: " + rule, e);
        }

        // 刷新调用器对象 来进行决策服务发现模式
		if (refreshInvoker(step, threshold, rule)) {
            // refresh success, update rule
            setMigrationRule(rule);
        }
    }

6、MigrationRuleHandler.refreshInvoker

通过迁移配置和当前提供者注册信息来决定创建什么类型的调用器对象(Invoker)来为后续服务调用做准备

java 复制代码
	private boolean refreshInvoker(MigrationStep step, Float threshold, MigrationRule newRule) {
        if (step == null || threshold == null) {
            throw new IllegalStateException("Step or threshold of migration rule cannot be null");
        }
        MigrationStep originStep = currentStep;

        if ((currentStep == null || currentStep != step) || !currentThreshold.equals(threshold)) {
            boolean success = true;
            switch (step) {
                // 接口和应用双订阅,默认
				case APPLICATION_FIRST:
                    migrationInvoker.migrateToApplicationFirstInvoker(newRule);
                    break;
                // 仅应用
				case FORCE_APPLICATION:
                    success = migrationInvoker.migrateToForceApplicationInvoker(newRule);
                    break;
                // 仅接口
				case FORCE_INTERFACE:
                default:
                    success = migrationInvoker.migrateToForceInterfaceInvoker(newRule);
            }

            if (success) {
				// 设置迁移模式和阈值
                setCurrentStepAndThreshold(step, threshold);
                logger.info("Succeed Migrated to " + step + " mode. Service Name: " + consumerURL.getDisplayServiceKey());
                report(step, originStep, "true");
            } else {
                // migrate failed, do not save new step and rule
                logger.warn("Migrate to " + step + " mode failed. Probably not satisfy the threshold you set "
                        + threshold + ". Please try re-publish configuration if you still after check.");
                report(step, originStep, "false");
            }

            return success;
        }
        // ignore if step is same with previous, will continue override rule for MigrationInvoker
        return true;
    }

7、MigrationClusterInvoker.migrateToApplicationFirstInvoker

默认使用接口级和应用级双订阅来兼容迁移的服务的

对应实现类 MigrationInvoker.migrateToApplicationFirstInvoker

java 复制代码
    @Override
    public void migrateToApplicationFirstInvoker(MigrationRule newRule) {
        CountDownLatch latch = new CountDownLatch(0);
        // 刷新接口级Invoker
		refreshInterfaceInvoker(latch);
		// 刷新应用级Invoker
        refreshServiceDiscoveryInvoker(latch);

        // directly calculate preferred invoker, will not wait until address notify
        // calculation will re-occurred when address notify later
		//  计算当前使用应用级还是接口级服务发现的Invoker对象
        calcPreferredInvoker(newRule);
    }

8、MigrationInvoker.refreshInterfaceInvoker

刷新接口级Invoker

java 复制代码
	protected void refreshInterfaceInvoker(CountDownLatch latch) {
        // 去除旧的监听
		clearListener(invoker);
		// 需要更新
        if (needRefresh(invoker)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Re-subscribing interface addresses for interface " + type.getName());
            }

            if (invoker != null) {
                invoker.destroy();
            }
			// 创建invoker
            invoker = registryProtocol.getInvoker(cluster, registry, type, url);
        }
        setListener(invoker, () -> {
            latch.countDown();
            if (reportService.hasReporter()) {
                reportService.reportConsumptionStatus(
                    reportService.createConsumptionReport(consumerUrl.getServiceInterface(), consumerUrl.getVersion(), consumerUrl.getGroup(), "interface"));
            }
            if (step == APPLICATION_FIRST) {
                calcPreferredInvoker(rule);
            }
        });
    }

9、MigrationInvoker.refreshServiceDiscoveryInvoker

刷新应用级Invoker

java 复制代码
	protected void refreshServiceDiscoveryInvoker(CountDownLatch latch) {
        clearListener(serviceDiscoveryInvoker);
        if (needRefresh(serviceDiscoveryInvoker)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Re-subscribing instance addresses, current interface " + type.getName());
            }

            if (serviceDiscoveryInvoker != null) {
                serviceDiscoveryInvoker.destroy();
            }
			// 获取应用级invoker
            serviceDiscoveryInvoker = registryProtocol.getServiceDiscoveryInvoker(cluster, registry, type, url);
        }
        setListener(serviceDiscoveryInvoker, () -> {
            latch.countDown();
            if (reportService.hasReporter()) {
                reportService.reportConsumptionStatus(
                    reportService.createConsumptionReport(consumerUrl.getServiceInterface(), consumerUrl.getVersion(), consumerUrl.getGroup(), "app"));
            }
            if (step == APPLICATION_FIRST) {
                calcPreferredInvoker(rule);
            }
        });
    }

10、MigrationInvoker.calcPreferredInvoker

计算当前使用应用级还是接口级服务发现的Invoker对象

java 复制代码
	private synchronized void calcPreferredInvoker(MigrationRule migrationRule) {
        if (serviceDiscoveryInvoker == null || invoker == null) {
            return;
        }
        // 通过阈值指定invoker
		Set<MigrationAddressComparator> detectors = ScopeModelUtil.getApplicationModel(consumerUrl == null ? null : consumerUrl.getScopeModel())
            .getExtensionLoader(MigrationAddressComparator.class).getSupportedExtensionInstances();
        if (CollectionUtils.isNotEmpty(detectors)) {
            // pick preferred invoker
            // the real invoker choice in invocation will be affected by promotion
            if (detectors.stream().allMatch(comparator -> comparator.shouldMigrate(serviceDiscoveryInvoker, invoker, migrationRule))) {
                this.currentAvailableInvoker = serviceDiscoveryInvoker;
            } else {
                this.currentAvailableInvoker = invoker;
            }
        }
    }

11、RegistryProtocol.getServiceDiscoveryInvoker

这里主要做了两件事

  • 创建注册中心目录
  • 通过代理对象的invoker
java 复制代码
    public <T> ClusterInvoker<T> getServiceDiscoveryInvoker(Cluster cluster, Registry registry, Class<T> type, URL url) {
        // 创建注册中心目录
		DynamicDirectory<T> directory = new ServiceDiscoveryRegistryDirectory<>(type, url);
		// 创建invoker
        return doCreateInvoker(directory, cluster, registry, type);
    }

服务目录(RegistryDirectory),负责框架与注册中心的订阅,并动态更新本地Invoker列表、路由列表、配置信息的逻辑

对服务目录的分析在 dubbo 服务消费原理分析之服务目录 讲解

相关推荐
一勺菠萝丶6 分钟前
Spring Boot + MyBatis/MyBatis Plus:XML中循环处理List参数的终极指南
xml·spring boot·mybatis
RainbowSea2 小时前
问题:后端由于字符内容过长,前端展示精度丢失修复
java·spring boot·后端
风象南2 小时前
SpringBoot 控制器的动态注册与卸载
java·spring boot·后端
我是一只代码狗2 小时前
springboot中使用线程池
java·spring boot·后端
hello早上好2 小时前
JDK 代理原理
java·spring boot·spring
PanZonghui3 小时前
Centos项目部署之运行SpringBoot打包后的jar文件
linux·spring boot
沉着的码农3 小时前
【设计模式】基于责任链模式的参数校验
java·spring boot·分布式
zyxzyx6663 小时前
Flyway 介绍以及与 Spring Boot 集成指南
spring boot·笔记
文火冰糖的硅基工坊4 小时前
[创业之路-458]:企业经营层 - 蓝海战略 - 重构价值曲线、整合产业要素、创造新需求
科技·重构·架构·创业·业务
一头生产的驴5 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf