Dubbo笔记 ⑮ :Dubbo集群组件 之 Router

一、前言

本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。


集群组件相关文章:

  1. Dubbo笔记⑫ :Dubbo 集群组件概述
  2. Dubbo笔记⑬ :Dubbo 集群组件 之 Cluster & ClusterInvoker
  3. Dubbo笔记⑭ :Dubbo集群组件 之 Directory
  4. Dubbo笔记⑮ :Dubbo集群组件 之 Router
  5. Dubbo笔记⑯ :Dubbo集群组件 之 LoadBalance

1. dubbo-admin

本文通过 dubbo-admin 来设置 router。所以在文章开始之前需要先启动 dubbo-admin。本文使用 dubbo-admin-0.2.0。
下载地址:https://github.com/apache/dubbo-admin/tags
安装过程:https://dubbo.apache.org/zh/docs/v2.7/admin/ops/introduction/
需要注意,dubbo-admin-0.2.0 使用了前后端分离。前端启动需要安装node.js。安装node.js的过程请自行百度。


在启动前端服务过程中,遇到以下问题:
在这里插入图片描述
个人解决方案:删除 .babelrc 文件中的如下插件即可。
在这里插入图片描述

二、Router

服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。在详细分析服务路由的源码之前,先来介绍一下服务路由是什么。服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。

路由功能依赖于 Router 接口实现:

public interface Router extends Comparable<Router> {
    /**
     * Get the router url.
     *
     * @return url
     */
     // 获取当前路由的url,即消费者的URl
    URL getUrl();

    // 完成请求路由的实际实现方法,根据一定规则将入参中的invokers 过滤,并返回
    <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
    
    // 通知路由器调用者列表。 调用者列表可能会不时更改。 此方法使路由器有机会在route(List, URL, Invocation)之前进行准备
    // 即当服务提供者的列表进行变化时会触发该方法。触发时机在  RegistryDirectory#refreshInvoker -》RouterChain#setInvokers
    default <T> void notify(List<Invoker<T>> invokers) {

    }

	// 决定此路由器是否需要在每次RPC到来时执行,还是仅在地址或规则更改时才执行。
    boolean isRuntime();

   // 要确定当任何调用者都无法匹配路由器规则时该路由器是否应生效,这意味着route(List, URL, Invocation)将为空。 大多数情况下,大多数路由器实现都会将此值默认设置为false。
    boolean isForce();

    // 路由优先级,由于存在多个路由,所以需要通过该参数决定路由执行优先级。越大优先级越高
    int getPriority();

    @Override
    default int compareTo(Router o) {
        if (o == null) {
            throw new IllegalArgumentException();
        }
        if (this.getPriority() == o.getPriority()) {
            if (o.getUrl() == null) {
                return 1;
            }
            if (getUrl() == null) {
                return -1;
            }
            return getUrl().toFullString().compareTo(o.getUrl().toFullString());
        } else {
            return getPriority() > o.getPriority() ? 1 : -1;
        }
    }
}

1. 调用时机

Router 中有两个关键方法

  • Router#notify : 完成了 路由信息的更新。当服务启动时或者标签路由更新时会通过此方法通知到当前服务。 调用时机在消费者端刷新本地的服务提供者列表时。即在 RegistryDirectory#refreshInvoker -》RouterChain#setInvokers 中。如下:

        public void setInvokers(List<Invoker<T>> invokers) {
            this.invokers = (invokers == null ? Collections.emptyList() : invokers);
            routers.forEach(router -> router.notify(this.invokers));
        }
    
  • Router#route : 完成了路由规则的实现。这里通过 RouterChain#routers 遍历来进行路由。

        public List<Invoker<T>> route(URL url, Invocation invocation) {
            List<Invoker<T>> finalInvokers = invokers;
            for (Router router : routers) {
                finalInvokers = router.route(finalInvokers, url, invocation);
            }
            return finalInvokers;
        }
    

Dubbo 提供了下面六种 Router 的实现类,由对应的 RouterFactory 加载而来。

实现类功能
ScriptRouter脚本路由,脚本路由规则 4 支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript
ConditionRouter条件路由,可以通过管理端设置一些匹配条件
ServiceRouter服务级别的路由,依赖于ConditionRouter 实现
AppRouter应用级路由器,依赖于ConditionRouter 实现
TagRouter标签路由,通过标签进行路由
MockInvokersSelector由 MockRouterFactory 加载而来,完成了 本地 mock 的功能

我们在Dubbo笔记⑨ : 消费者启动流程 - RegistryProtocol#refer 1. RegistryDirectory#buildRouterChain 章节 中讲到了默认流程下,RouterChain#routers 并不会加载所有的 Router。默认加载的是下面四个 :

	// 其调用顺序如下:
	MockInvokersSelector =TagRouter =AppRouter =ServiceRouter

由于官方文档比较详细,本文并不会介绍所有的 router(因为懒 )。而是介绍了 MockInvokersSelector 和 TagRouter 的实现过程。

各个路由的用法:
https://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule-deprecated/
https://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/

ConditionRouter 路由的源码分析 :
https://dubbo.apache.org/zh/docs/v2.7/dev/source/router/

三、MockInvokersSelector

MockInvokersSelector 完成了本地mock 的功能,这个在之前的文章中已经详细介绍,这里不再赘述。

四、TagRouter

1. 标签路由

标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。

标签主要是指对Provider端应用实例的分组,目前有两种方式可以完成实例分组,分别是动态规则打标和静态规则打标,其中动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。

1.1 标签格式

  • Key 明确规则体作用到哪个应用。必填。
  • enabled=true 当前路由规则是否生效,可不填,缺省生效。
  • force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false。
  • runtime=false 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false。
  • priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。
    tags 定义具体的标签分组内容,可定义任意n(n>=1)个标签并为每个标签指定实例列表。必填。
  • name, 标签名称
  • addresses, 当前标签包含的实例列表

1.2 路由降级约定

  1. request.tag=tag1 时优先选择 标记了tag=tag1 的 provider。若集群中不存在与请求标记对应的服务,默认将降级请求 tag为空的provider;如果要改变这种默认行为,即找不到匹配tag1的provider返回异常,需设置request.tag.force=true。

  2. request.tag未设置时,只会匹配tag为空的provider。即使集群中存在可用的服务,若 tag 不匹配也就无法调用,这与约定1不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。


1.3 简单演示

启动如下两个服务,在 dubbo-admin 中看到如下:
在这里插入图片描述

两个服务的实现分别为:

// 20880 端口
@Service(version = "1.0.0", group = "dubbo")
public class DemoServiceImpl implements DemoService {
    @Override
    public String sayHello(String name) {
        return "Spring Dubbo DemoServiceImpl name = " + name;
    }
}
// 9999 端口
@Service(version = "1.0.0", group = "dubbo")
public class DemoServiceImpl implements DemoService {
    @Override
    public String sayHello(String name) {
        return "MainDubbo DemoServiceImpl name = " + name;
    }
}

在 dubbo-admin 选中服务,创建标签路由。
在这里插入图片描述
在ZK 中的节点结构:
在这里插入图片描述

即代表,tag 为 spring 的访问端口为9999服务,tag 为main 的请求访问端口为 20880的服务。这里需要注意,addresses 中的 ip 需要和 服务列表中ip相同,不能写localhost、127.0.0.1 等。这里后面会分析。

请求访问,这里直接通过main 方法的方式访问

	public static void main(String[] args) throws InterruptedException {
		// 自定义的方法 获取 ReferenceConfig
        ReferenceConfig<DemoService> referenceConfig = DubboUtil.referenceConfig("spring-dubbo-provider");
        referenceConfig.setCheck(false);
        DemoService demoService = referenceConfig.get();

        referenceConfig.setMonitor("http://localhost:8080");
 // 	 也可以通过这种方式设置全局的 tag,但是优先级基于 上下文设置的tag
 //      Map<String, String> map = Maps.newHashMap();
 //      map.put(Constants.TAG_KEY, "main");
 //      referenceConfig.setParameters(map);
        // 上下文设置tag
        RpcContext.getContext().setAttachment(Constants.TAG_KEY,"main");
        String result = demoService.sayHello("demo");
        // 输出 main result = Main Dubbo DemoServiceImpl name = demo
        System.out.println("main result = " + result);

        RpcContext.getContext().setAttachment(Constants.TAG_KEY,"spring");
        result = demoService.sayHello("demo");
		// 输出 spring result = Spring Dubbo DemoServiceImpl name = demo
        System.out.println("spring result = " + result);
    }

这里可以看到,对于 tag 配置为 main 的请求访问到了20880端口的服务上,对于 tag 为 spring的请求访问到了 9999 端口的服务上。

2. 代码实现

下面我们来看看这两个方法的实现:

2.1 TagRouter#notify

    @Override
    public <T> void notify(List<Invoker<T>> invokers) {
        if (invokers == null || invokers.isEmpty()) {
            return;
        }

        Invoker<T> invoker = invokers.get(0);
        URL url = invoker.getUrl();
        // 获取服务提供者的 dubbo.application.name
        String providerApplication = url.getParameter(Constants.REMOTE_APPLICATION_KEY);

        if (StringUtils.isEmpty(providerApplication)) {
            return;
        }

        synchronized (this) {
        	// 判断是否是当前的服务提供者服务发生改变
            if (!providerApplication.equals(application)) {
                if (!StringUtils.isEmpty(application)) {
                    configuration.removeListener(application + RULE_SUFFIX, this);
                }
                // 更新配置中心 /dubbo/config 的监听。设置自身为监听,当节点更新时会调用process方法
              	// 我们设置的路由规则会保存到 /dubbo/config/applicationname 节点。
                String key = providerApplication + RULE_SUFFIX;
                configuration.addListener(key, this);
                application = providerApplication;
                // 获取最新的规则,并进行同步
                String rawRule = configuration.getConfig(key);
                if (rawRule != null) {
                    this.process(new ConfigChangeEvent(key, rawRule));
                }
            }
        }
    }

这样完成了,消费者启动后会触发TagRouter#notify 方法,而在 TagRouter#notify 方法中, TagRouter 完成了 /dubbo/config 的监听。当有路由设置进来时,会触发 TagRouter 的监听方法,即TagRouter#process,TagRouter#process 的 实现如下:

 @Override
    public synchronized void process(ConfigChangeEvent event) {
        try {	
        	// 如果事件类型为 delete,则移除本地的路由规则
            if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
                this.tagRouterRule = null;
            } else {
            	// 解析路由规则。这里的 event.getValue() 即我们在注册中心设置的yaml格式的值
                this.tagRouterRule = TagRuleParser.parse(event.getValue());
            }
        } catch (Exception e) {
        }
    }

这里的逻辑比较简单,如果是删除时间,则清空本地的路由规则,否则重新解析赋值。

  1. event.getValue() 值即为我们在注册中心设置的值 :

    enabled: true
    force: false
    key: spring-dubbo-provider
    priority: 0
    runtime: true
    tags:
    - addresses:
      - 192.168.111.1:9999
      name: spring
    - addresses:
      - 192.168.111.1:20880
      name: main
    
    
  2. tagRouterRule 在解析后,保存了Tags 集合(其中保存了tags 节点的信息),并通过两个map保存了 ip -> tag 和 tag -> ip 的映射,如下图:在这里插入图片描述

2.2 TagRouter#route

当有请求通过时,会经过此方法路由,其实现如下:

 	@Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        if (CollectionUtils.isEmpty(invokers)) {
            return invokers;
        }
		// 如果动态路由没有配置,则匹配静态路由
        if (tagRouterRule == null || !tagRouterRule.isValid() || !tagRouterRule.isEnabled()) {
        	// 1. 对静态标签的匹配
            return filterUsingStaticTag(invokers, url, invocation);
        }
		// 2. 对动态路由的配置
        List<Invoker<T>> result = invokers;
        // 获取上下文路由tag,上下文路由优先级最高
        String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
                invocation.getAttachment(TAG_KEY);

        // if we are requesting for a Provider with a specific tag
        // 如果当前请求指定了 tag
        if (StringUtils.isNotEmpty(tag)) {
        	// 获取路由tag指定的服务地址
            List<String> addresses = tagRouterRule.getTagnameToAddresses().get(tag);
            // filter by dynamic tag group first
            if (CollectionUtils.isNotEmpty(addresses)) {
            	// 从地址中过滤出来和当前请求URL匹配的 result
                result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));
                // if result is not null OR it's null but force=true, return result directly
                // 如果过滤出来的结果不为空(则代表当前的tag 路由的服务存在) || force =true(强制使用tag路由)
                if (CollectionUtils.isNotEmpty(result) || tagRouterRule.isForce()) {
                    return result;
                }
            } else {
                // dynamic tag group doesn't have any item about the requested app OR it's null after filtered by
                // dynamic tag group but force=false. check static tag
                // 动态路由匹配失败,匹配静态路由
                result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
            }
            // If there's no tagged providers that can match the current tagged request. force.tag is set by default
            // to false, which means it will invoke any providers without a tag unless it's explicitly disallowed.
            // 如果没有可以与当前已标记请求匹配的已标记提供程序。默认情况下,force.tag设置为false,这意味着除非明确禁止,否则它将调用任何没有标签的提供程序。
            if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {
                return result;
            }
            // FAILOVER: return all Providers without any tags.
            else {
                List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
                        tagRouterRule.getAddresses()));
                return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
            }
        } else {
            // List<String> addresses = tagRouterRule.filter(providerApp);
            // return all addresses in dynamic tag group.
            // 如果动态标签路由不为空,则需要将Invokers 中标签路由中 地址剔除。即下面所说的规则二场景。
            List<String> addresses = tagRouterRule.getAddresses();
            if (CollectionUtils.isNotEmpty(addresses)) {
            	// 如果 Invoker 代表的提供者,在动态路由标签中已经配置,则不允许再返回。
                result = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(), addresses));
                // 1. all addresses are in dynamic tag group, return empty list.
                // 所有地址都在动态标签组中,返回空列表。
                if (CollectionUtils.isEmpty(result)) {
                    return result;
                }
                // 2. if there are some addresses that are not in any dynamic tag group, continue to filter using the
                // static tag group.
            }
            // 最后再与本地 tag 匹配校验
            return filterInvoker(result, invoker -> {
                String localTag = invoker.getUrl().getParameter(TAG_KEY);
                return StringUtils.isEmpty(localTag) || !tagRouterRule.getTagNames().contains(localTag);
            });
        }
    }
    
  // 对静态标签进行过滤
  private <T> List<Invoker<T>> filterUsingStaticTag(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        List<Invoker<T>> result = invokers;
        // Dynamic param
        // 1. 获取 当前需要匹配的 tag 参数信息。
        // 这里可以看到, 上下文的配置优于 url中的配置
        String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
                invocation.getAttachment(TAG_KEY);
        // Tag request
        // 2. 如果需要进行tag 匹配则进行过滤
        if (!StringUtils.isEmpty(tag)) {
            result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(Constants.TAG_KEY)));
            // 如果没有匹配上 && 并非强制匹配,则获取 tag = null 的 服务提供者 invoker 
            if (CollectionUtils.isEmpty(result) && !isForceUseTag(invocation)) {
                result = filterInvoker(invokers, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(Constants.TAG_KEY)));
            }
        } else {
        	// 不需要tag匹配,则获取 tag = null 的服务提供者 invoker
            result = filterInvoker(invokers, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(Constants.TAG_KEY)));
        }
        return result;
    }
    
    // 过滤 Invoker
    private <T> List<Invoker<T>> filterInvoker(List<Invoker<T>> invokers, Predicate<Invoker<T>> predicate) {
        return invokers.stream()
                .filter(predicate)
                .collect(Collectors.toList());
    }
    
  // 对  FORCE_USE_TAG 参数(dubbo.force.tag)校验,如果设置为 true,则强制匹配 tag,不可降级。
   private boolean isForceUseTag(Invocation invocation) {
        return Boolean.valueOf(invocation.getAttachment(FORCE_USE_TAG, url.getParameter(FORCE_USE_TAG, "false")));
    }

官方总结的规则描述如下:

  1. 规则一: request.tag=red 时优先选择 tag=red 的 provider。若集群中不存在与请求标记对应的服务,可以降级请求 tag=null 的 provider,即默认 provider。这里需要注意, 如果在上下文或者 URL参数中设置了 FORCE_USE_TAG 为 true (上下文配置优先于 URL 参数),则表示强制匹配tag,则不会再降级匹配 tag = null 的服务。
  2. 规则二:request.tag=null 时,只会匹配 tag=null 的 provider。即使集群中存在可用的服务,若 tag 不匹配就无法调用,这与规则1不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。

五、ConditionRouter

条件路由官方文档介绍的很清楚,这里就不再赘述(懒惰啊 )

基本使用 :https://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/
源码分析:https://dubbo.apache.org/zh/docs/v2.7/dev/source/router/

六、多分组情况下路由失效

如果消费者多分组调用时,并且存在至少一个服务提供者的情况下,服务路由不起作用。
原因在于 RegistryDirectory#doList 中针对多分组情况直接返回了注册中心所有 Invokers。


首先需要明确下面的调用链路

MockClusterInvoker#doMockInvoke =MockClusterInvoker#selectMockInvoker =RegistryDirectory#doList => RouterChain#route(在此方法中进行路由)

RegistryDirectory#doList 方法简化如下:

    @Override
    public List<Invoker<T>> doList(Invocation invocation) {
    	// 如果是多分组情况下,会直接返回invokers,并不会进行路由
        if (multiGroup) {
            return this.invokers == null ? Collections.emptyList() : this.invokers;
        }
        List<Invoker<T>> invokers = null;
        // 在此进行服务路由。
        invokers = routerChain.route(getConsumerUrl(), invocation);
        return invokers == null ? Collections.emptyList() : invokers;
    }

以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
https://blog.csdn.net/yuanshangshenghuo/article/details/108133661
https://blog.csdn.net/weixin_38308374/article/details/105918442
https://wangchengming.blog.csdn.net/article/details/106679772
https://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule-deprecated/
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值