Dubbo3.0消费端服务发现智能决策原理

47 阅读12分钟

Dubbo消费端服务发现智能决策原理

智能决策简介

这里要说的内容对Dubbo2迁移到Dubbo3的应用比较有帮助,消费者应用级服务发现做了一些自动决策的逻辑来决定当前消费者是应用级发现还是接口级服务发现,这里与前面说的提供者双注册的原理是对等的,提供者默认同时进行应用级注册和接口级注册,消费者对提供者注册的数据来决定使用应用级发现或者接口级发现

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

同时新的地址发现模型(注册应用级别的地址)也将会自动注册。

可以来看下官网的服务发现模型图

提供者双注册图 image.png 消费者订阅决策的图

image.png

对于双注册不熟悉的可以看博客 双注册原理

地址迁移状态切换

地址迁移过程中涉及到了三种状态的切换,为了保证平滑迁移,共有 6 条切换路径需要支持,可以总结为从强制接口级、强制应用级往应用级优先切换;应用级优先往强制接口级、强制应用级切换;还有强制接口级和强制应用级互相切换。 对于同一接口切换的过程总是同步的,如果前一个规则还未处理完就收到新规则则回进行等待。

1. 切换到应用级优先

从强制接口级、强制应用级往应用级优先切换本质上是从某一单订阅往双订阅切换,保留原有的订阅并创建另外一种订阅的过程。这个切换模式下规则体中配置的 delay 配置不会生效,也即是创建完订阅后马上进行阈值探测并决策选择某一组订阅进行实际优先调用。由于应用级优先模式是支持运行时动态进行阈值探测,所以对于部分注册中心无法启动时即获取全量地址的场景在全部地址通知完也会重新计算阈值并切换。 应用级优先模式下的动态切换是基于服务目录(Directory)的地址监听器实现的。 //imgs/v3/migration/migration-4.png

2. 应用级优先切换到强制

应用级优先往强制接口级、强制应用级切换的过程是对双订阅的地址进行检查,如果满足则对另外一份订阅进行销毁,如果不满足则回滚保留原来的应用级优先状态。 如果用户希望这个切换过程不经过检查直接切换可以通过配置 force 参数实现。 //imgs/v3/migration/migration-5.png

3. 强制接口级和强制应用级互相切换

强制接口级和强制应用级互相切换需要临时创建一份新的订阅,判断新的订阅(即阈值计算时使用新订阅的地址数去除旧订阅的地址数)是否达标,如果达标则进行切换,如果不达标会销毁这份新的订阅并且回滚到之前的状态。 //imgs/v3/migration/migration-6.png

消费者智能决策原理

对于 2.x 的消费者实例,它们看到的自然都是 2.x 版本的提供者地址列表;

对于 3.x 的消费者,它具备同时发现 2.x 与 3.x 提供者地址列表的能力。在默认情况下,如果集群中存在可以消费的 3.x 的地址,将自动消费 3.x 的地址,如果不存在新地址则自动消费 2.x 的地址。Dubbo3 提供了开关来控制这个行为:

dubbo.application.service-discovery.migration=APPLICATION_FIRST

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

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

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

这里再引入一张官方的图:

image.png

接下来就围绕着这一张图来看下框架内代码是如何实现的

入口代码位于RegistryProtocol中的interceptInvoker方法

protected <T> Invoker<T> interceptInvoker(ClusterInvoker<T> invoker, URL url, URL consumerUrl) {
    //目前存在的扩展类型为RegistryProtocolListener监听器的实现类型MigrationRuleListener
    List<RegistryProtocolListener> listeners = findRegistryProtocolListeners(url);
    if (CollectionUtils.isEmpty(listeners)) {
        return invoker;
    }

    for (RegistryProtocolListener listener : listeners) {
        listener.onRefer(this, invoker, consumerUrl, url);
    }
    return invoker;
}

迁移监听器MigrationRuleListener类型的onRefer方法

public void onRefer(RegistryProtocol registryProtocol, ClusterInvoker<?> invoker, URL consumerUrl, URL registryURL) {
    //创建一个对应invoker对象的MigrationRuleHandler类型对象 然后将其存放在缓存Map<MigrationInvoker, MigrationRuleHandler>类型对象handles中
    MigrationRuleHandler<?> migrationRuleHandler = handlers.computeIfAbsent((MigrationInvoker<?>) invoker, _key -> {
        ((MigrationInvoker<?>) invoker).setMigrationRuleListener(this);
        return new MigrationRuleHandler<>((MigrationInvoker<?>) invoker, consumerUrl);
    });
    //迁移规则执行 rule是封装了迁移的配置规则的信息对应类型MigrationRule类型,在初始化对象的时候进行了配置初始化
    migrationRuleHandler.doMigrate(rule);
}

规则采用 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: 状态名(必填)

详情参考官方 应用级服务发现地址迁移规则

迁移规则执行器MigrationRuleHandler执行迁移doMigrate方法

在 Dubbo 3 之前地址注册模型是以接口级粒度注册到注册中心的,而 Dubbo 3 全新的应用级注册模型注册到注册中心的粒度是应用级的。从注册中心的实现上来说是几乎不一样的,这导致了对于从接口级注册模型获取到的 invokers 是无法与从应用级注册模型获取到的 invokers 进行合并的。为了帮助用户从接口级往应用级迁移,Dubbo 3 设计了 Migration 机制,基于三个状态的切换实现实际调用中地址模型的切换。

状态模型图 image.png

整个迁移的执行的过程会依据配置的migrate状态来执行

状态分为:

  • APPLICATION_FIRST
  • FORCE_APPLICATION
  • FORCE_INTERFACE

来看下MigrationRuleHandler的doMigrate方法

public synchronized void doMigrate(MigrationRule rule) {
    if (migrationInvoker instanceof ServiceDiscoveryMigrationInvoker) {
        refreshInvoker(MigrationStep.FORCE_APPLICATION, 1.0f, rule);
        return;
    }

    // 默认的迁移步骤为APPLICATION_FIRST
    MigrationStep step = MigrationStep.APPLICATION_FIRST;
    // 定义默认阈值
    float threshold = -1f;

    try {
        // 获取迁移步骤,默认设置为APPLICATION_FIRST
        step = rule.getStep(consumerURL);
        // 获取阈值,默认是-1L
        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);
    }
}

来看下getStep方法,getThreshold方法逻辑与getStep类似

public MigrationStep getStep(URL consumerURL) {
    // migrate规则中有接口级粒度配置(interfaces)
    if (interfaceRules != null) {
        //根据服务的显示名获取规则,与interfaces:serviceKey对应
        //显示名称为接口名称或接口名称+版本号,例如org.apache.dubbo.sample.provider.DubboService
        SubMigrationRule rule = interfaceRules.get(consumerURL.getDisplayServiceKey());
        if (rule != null) {
            if (rule.getStep() != null) {
                return rule.getStep();
            }
        }
    }
    // migrate规则中有应用级粒度配置(interfaces)
    if (applications != null) {
        ServiceNameMapping serviceNameMapping = ServiceNameMapping.getDefaultExtension(consumerURL.getScopeModel());
        //根据url获取应用名称
        Set<String> services = serviceNameMapping.getRemoteMapping(consumerURL);
        //应用名称对应applications:serviceKey
        if (CollectionUtils.isNotEmpty(services)) {
            for (String service : services) {
                SubMigrationRule rule = applicationRules.get(service);
                if (rule != null) {
                    if (rule.getStep() != null) {
                        return rule.getStep();
                    }
                }
            }
        }
    }

    /**
     * FIXME, it's really hard to follow setting default values here.
     */
     // 设置默认值
    if (step == null) {
        // initial step : APPLICATION_FIRST
        step = MigrationStep.APPLICATION_FIRST;
        step = Enum.valueOf(MigrationStep.class,
            consumerURL.getParameter(MIGRATION_STEP_KEY, getDefaultStep(consumerURL, step.name())));
    }

    return step;
}

刷新调用器对象refreshInvoker方法

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;
}

应用级优先迁移发现规则逻辑migrateToApplicationFirstInvoker

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);
}

先来看下接口级invoker刷新

protected void refreshInterfaceInvoker(CountDownLatch latch) {
    // 清除监听器
    clearListener(invoker);
    // 检查接口级invoker是否需要刷新
    // 需要刷新的条件是 1. 空对象 2. 已被销毁 3. 没有代理invoker对象
    if (needRefresh(invoker)) {
        if (logger.isDebugEnabled()) {
            logger.debug("Re-subscribing interface addresses for interface " + type.getName());
        }
        // 销毁对象,下方会重新创建
        if (invoker != null) {
            invoker.destroy();
        }
        // 创建invoker
        //registryProtocol实例为InterfaceCompatibleRegistryProtocol
        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);
        }
    });
}

应用级invoker刷新

protected void refreshServiceDiscoveryInvoker(CountDownLatch latch) {
    // 清除监听器
    clearListener(serviceDiscoveryInvoker);
    // 检查接口级invoker是否需要刷新
    // 需要刷新的条件是 1. 空对象 2. 已被销毁 3. 没有代理invoker对象
    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);
        }
    });
}

计算使用接口级服务或应用级服务calcPreferredInvoker

private synchronized void calcPreferredInvoker(MigrationRule migrationRule) {
    // 保证接口级invoker和应用级invoker都存在
    if (serviceDiscoveryInvoker == null || invoker == null) {
        return;
    }
    // MigrationAddressComparator类型的实现为DefaultMigrationAddressComparator
    Set<MigrationAddressComparator> detectors = ScopeModelUtil.getApplicationModel(consumerUrl == null ? null : consumerUrl.getScopeModel())
        .getExtensionLoader(MigrationAddressComparator.class).getSupportedExtensionInstances();
    if (CollectionUtils.isNotEmpty(detectors)) {
        //选择最优的invoker
        //由于是应用级优先,以应用级服务invoker作为new,接口级服务作为old来检查是否使用应用级服务发现
        if (detectors.stream().allMatch(comparator -> comparator.shouldMigrate(serviceDiscoveryInvoker, invoker, migrationRule))) {
            this.currentAvailableInvoker = serviceDiscoveryInvoker;
        } else {
            this.currentAvailableInvoker = invoker;
        }
    }
}

强制应用级服务发现规则逻辑migrateToForceApplicationInvoker

来看下基本逻辑图:

强制应用级服务规则图 (1).png

public boolean migrateToForceApplicationInvoker(MigrationRule newRule) {
    CountDownLatch latch = new CountDownLatch(1);
    // 刷新应用级发现invoker
    refreshServiceDiscoveryInvoker(latch);
    // 第一次迁移服务如果就是APPLICATION_FORCE,接口级服务invoker为null,直接设置应用级服务invoker为可用invoker
    if (invoker == null) {
        // invoker is absent, ignore threshold check
        this.currentAvailableInvoker = serviceDiscoveryInvoker;
        return true;
    }
    // 走到这里证明是从 应用级优先或强制接口级 切换过来
    
    // 等地址刷新通知完成
    waitAddressNotify(newRule, latch);
    // 检查规则是否强制切换为应用级服务
    if (newRule.getForce(consumerUrl)) {
        // 强制迁移为应用级服务
        this.currentAvailableInvoker = serviceDiscoveryInvoker;
        // 销毁接口级服务
        this.destroyInterfaceInvoker();
        return true;
    }
    // 不是强制切换,需要检查阈值
    Set<MigrationAddressComparator> detectors = ScopeModelUtil.getApplicationModel(consumerUrl == null ? null : consumerUrl.getScopeModel())
        .getExtensionLoader(MigrationAddressComparator.class).getSupportedExtensionInstances();
    if (CollectionUtils.isNotEmpty(detectors)) {
        // 阈值探测,检查成功后切换为应用级服务
        if (detectors.stream().allMatch(comparator -> comparator.shouldMigrate(serviceDiscoveryInvoker, invoker, newRule))) {
            this.currentAvailableInvoker = serviceDiscoveryInvoker;
            this.destroyInterfaceInvoker();
            return true;
        }
    }

    // 检查失败,证明无法切换为应用级服务,销毁应用级服务
    if (step == MigrationStep.FORCE_INTERFACE) {
        destroyServiceDiscoveryInvoker();
    }
    return false;
}

强制接口级服务发现规则逻辑migrateToForceInterfaceInvoker

来看下基本逻辑图:

强制接口级服务规则图.png

强制接口级服务发现与强制应用级服务发现逻辑一致

区别在于从设置应用级服务改为设置接口级服务

public boolean migrateToForceInterfaceInvoker(MigrationRule newRule) {
    CountDownLatch latch = new CountDownLatch(1);
    refreshInterfaceInvoker(latch);

    if (serviceDiscoveryInvoker == null) {
        // serviceDiscoveryInvoker is absent, ignore threshold check
        this.currentAvailableInvoker = invoker;
        return true;
    }
    
    // 走到这里证明是从 应用级优先或强制应用级 切换过来

    // wait and compare threshold
    waitAddressNotify(newRule, latch);

    if (newRule.getForce(consumerUrl)) {
        // force migrate, ignore threshold check
        this.currentAvailableInvoker = invoker;
        this.destroyServiceDiscoveryInvoker();
        return true;
    }

    Set<MigrationAddressComparator> detectors = ScopeModelUtil.getApplicationModel(consumerUrl == null ? null : consumerUrl.getScopeModel())
        .getExtensionLoader(MigrationAddressComparator.class).getSupportedExtensionInstances();
    if (CollectionUtils.isNotEmpty(detectors)) {
        // 阈值探测,检查成功后切换为接口级服务
        if (detectors.stream().allMatch(comparator -> comparator.shouldMigrate(invoker, serviceDiscoveryInvoker, newRule))) {
            this.currentAvailableInvoker = invoker;
            this.destroyServiceDiscoveryInvoker();
            return true;
        }
    }
    
    // 检查失败切换为强制应用级服务发现,并销毁接口级invoker
    if (step == MigrationStep.FORCE_APPLICATION) {
        destroyInterfaceInvoker();
    }
    return false;
}

智能决策默认实现DefaultMigrationAddressComparator

MigrationAddressComparator 也是一个 SPI 拓展点,用户可以自行拓展,所有检查的结果取交集。

这里看的是框架内默认的实现DefaultMigrationAddressComparator

来看下决策方法shouldMigrate

public <T> boolean shouldMigrate(ClusterInvoker<T> newInvoker, ClusterInvoker<T> oldInvoker, MigrationRule rule) {
    // 创建迁移数据缓存
    Map<String, Integer> migrationData = serviceMigrationData.computeIfAbsent(oldInvoker.getUrl().getDisplayServiceKey(), _k -> new ConcurrentHashMap<>());
    // newinvoker代表需要迁移过去的invoker,如果invoker无任何代理invoker,直接返回false,标识不允许迁移
    if (!newInvoker.hasProxyInvokers()) {
        migrationData.put(OLD_ADDRESS_SIZE, getAddressSize(oldInvoker));
        migrationData.put(NEW_ADDRESS_SIZE, -1);
        logger.info("No " + getInvokerType(newInvoker) + " address available, stop compare.");
        return false;
    }
    // oldinvoker代表被替代的invoker,如果无任何代理invoker,直接使用newinvoker即可,返回true,标识允许迁移
    if (!oldInvoker.hasProxyInvokers()) {
        migrationData.put(OLD_ADDRESS_SIZE, -1);
        migrationData.put(NEW_ADDRESS_SIZE, getAddressSize(newInvoker));
        logger.info("No " + getInvokerType(oldInvoker) + " address available, stop compare.");
        return true;
    }
    // 两者都存在代理invoker,需要进行阈值探测检查来判定是否允许迁移
    // 获取newinvoker的服务地址数量
    int newAddressSize = getAddressSize(newInvoker);
    // 获取oldinvoker的服务地址数量
    int oldAddressSize = getAddressSize(oldInvoker);

    migrationData.put(OLD_ADDRESS_SIZE, oldAddressSize);
    migrationData.put(NEW_ADDRESS_SIZE, newAddressSize);

    String rawThreshold = null;
    // 获取配置的阈值,默认为 1.0L
    Float configuredThreshold = rule == null ? null : rule.getThreshold(oldInvoker.getUrl());
    if (configuredThreshold != null && configuredThreshold >= 0) {
        rawThreshold = String.valueOf(configuredThreshold);
    }
    rawThreshold = StringUtils.isNotEmpty(rawThreshold) ? rawThreshold : ConfigurationUtils.getCachedDynamicProperty(newInvoker.getUrl().getScopeModel(), MIGRATION_THRESHOLD, DEFAULT_THRESHOLD_STRING);
    float threshold;
    try {
        threshold = Float.parseFloat(rawThreshold);
    } catch (Exception e) {
        logger.error("Invalid migration threshold " + rawThreshold);
        threshold = DEFAULT_THREAD;
    }

    logger.info("serviceKey:" + oldInvoker.getUrl().getServiceKey() + " Instance address size " + newAddressSize + ", interface address size " + oldAddressSize + ", threshold " + threshold);
    // 代表迁移过去的invoker是有效的,旧的invoker无效,允许迁移
    if (newAddressSize != 0 && oldAddressSize == 0) {
        return true;
    }
    // 新旧invoker都无效,不允许迁移
    if (newAddressSize == 0 && oldAddressSize == 0) {
        return false;
    }
    // 比对迁移的invoker与旧invoker,大于阈值代表允许迁移
    if (((float) newAddressSize / (float) oldAddressSize) >= threshold) {
        return true;
    }
    return false;
}

是否具有代理invoker的检查实质上检查的是Directory中是否有对应服务发现invoker

来看下代码:

default boolean hasProxyInvokers() {
    // 应用级服务发现实现类为ServiceDiscoveryRegistryDirectory,接口级服务发现实现类为RegistryDirectory
    Directory<T> directory = getDirectory();
    if (directory == null) {
        return false;
    }
    // 这里检查的就是AbstractDirectory中的invokers是否为空
    return !directory.isEmpty();
}

Dierctory用于消费者存储所有服务调用invoker,以及可用的服务调用invoker

阈值机制旨在进行流量切换前的地址数检查,如果应用级的可使用地址数与接口级的可用地址数对比后没达到阈值将检查失败。

核心代码如下

if (((float) newAddressSize / (float) oldAddressSize) >= threshold) {
   return true;
}
return false;

迁移规则校验的流程如下:

迁移阈值规则.png