手记

Dubbo 路由规则之条件路由

前言

大家好,今天开始给大家分享 — Dubbo 专题之 Dubbo 路由规则之条件路由。在前一个章节中我们介绍了 Dubbo 令牌验证和优雅停机,以及我们也例举了常见的使用场景并且进行了源码解析来分析其实现原理,同时知道 Dubbo 中的令牌验证核心思想就是通过服务提供端提供的token或者随机产生的token放入注册中心进行管理,然后服务消费端获取token令牌并且在调用服务提供端时携带 token,服务提供端根据消费端携带的token进行验证。有的小伙伴可能会想:我们多个服务提供者能否通过一定的规则对调用的服务提供者进行过滤和限制呢?那接下来我们就围绕着这个问题一起来学习下 Dubbo 中的路由规则。下面就让我们快速开始吧!

1. 条件路由简介

首先我们得了解什么是路由规则?假设有这样一个场景如下图所示:


上图中我们可以看到有两个机房分别是机房A、机房B,其中机房A只能访问到 Service A 和 Service B ,而机房B 只能访问到 Service C 和 Service D。要实现上面这种场景我们就需要用到所谓的路由规则。路由规则是在发起一次RPC调用前过滤目标服务器地址,而过滤后的地址列表,将作为消费端最终发起RPC调用的备选地址。在 Dubbo 中支持两种路由规则今天我们主要讨论条件路由。

  • 条件路由:支持以接口服务或消费者应用为粒度配置路由规则。

2. 使用方式

下面我们简单的讨论下条件路由使用方式:

条件路由

  • 接口服务粒度

    # demo-consumer1 的消费者只能消费所有端口为20880的服务实例
    # demo-consumer2 的消费者只能消费所有端口为20881的服务实例
    ---
    scope: application #应用粒度
    force: true
    runtime: true
    enabled: true
    key: demo-provider 
    conditions:
      - application=demo-consumer1 => address=*:20880
      - application=demo-consumer2 => address=*:20881
    
  • 应用粒度

    # BookFacade 的 queryAll 方法只能消费所有端口为20880的服务实例
    # BookFacade 的 queryByName 方法只能消费所有端口为20881的服务实例
    ---
    scope: service #服务粒度
    force: true
    runtime: true
    enabled: true
    key: com.muke.dubbocourse.common.api.BookFacade
    conditions:
      - method=queryAll => address=*:20880
      - method=queryByName => address=*:20881
    
  • 字段说明:

    编号 字段名称 说明 必填
    1 scope 路由规则的作用粒度,scope的取值会决定key的取值。
    service 服务粒度 application 应用粒度。
    必填
    2 Key 明确规则体作用在哪个接口服务或应用。 scope=service时,
    key取值为[{group}:]{service}[:{version}]的组合 scope=application时,
    key取值为application名称 。
    必填
    3 enabled enabled=true 当前路由规则是否生效,,缺省生效。 可不填
    4 force force=false 当路由结果为空时,是否强制执行,如果不强制执行,
    路由结果为空的路由规则将自动失效,缺省为 false
    可不填
    5 runtime runtime=false 是否在每次调用时执行路由规则,
    否则只在提供者地址列表变更时预先执行并缓存结果,
    调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true
    需要注意设置会影响调用的性能,缺省为 false
    可不填
    6 priority priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,缺省为 0 可不填
    7 conditions 定义具体的路由规则内容。 必填
  • Conditions规则体

    格式:

    • => 之前的为消费者匹配条件,所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。
    • => 之后为提供者地址列表的过滤条件,所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。
    • 如果匹配条件为空,表示对所有消费方应用,如:=> host != 192.168.53.11
    • 如果过滤条件为空,表示禁止访问,如:host = 192.168.53.10 =>

    表达式:

    参数支持:

    • 服务调用信息,如:method, argument 等,暂不支持参数路由
    • URL 本身的字段,如:protocol, host, port 等
    • 以及 URL 上的所有参数,如:application, organization 等

    条件支持:

    • 等号 = 表示"匹配",如:host = 192.168.53.10
    • 不等号 != 表示"不匹配",如:host != 192.168.53.10

    值支持:

    • 以逗号 , 分隔多个值,如:host != 192.168.53.10,192.168.53.11
    • 以星号 * 结尾,表示通配,如:host != 10.20.*
    • 以美元符 $ 开头,表示引用消费者参数,如:host = $host

Tips:conditions部分是规则的主体,由1到任意多条规则组成,下面我们就每个规则的配置语法做详细说明:

3. 使用场景

从上面的简单介绍我们可以大致了解到,当我们消费对访问服务提供者时我们可以通过一定的规则对服务提供者列表进行过滤。那下面我们列举下工作中常使用的场景:

  1. 黑名单: 比如我们需要禁止某些服务消费者消费服务
host = 192.168.53.10,192.168.53.11 =>

上面配置表示禁止192.168.53.10192.168.53.11消费者访问服务提供者。

  1. 服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉
=> host = 192.168.53.1*,192.168.53.2*

上面配置表示只能放192.168.53.1*192.168.53.2 ip 地址开头的服务提供者。

  1. 读写分离:读取数据和写入数据操作分开
method = find*,list*,get*,is* => host = 192.168.53.10,192.168.53.11,192.168.53.12
method != find*,list*,get*,is* => host = 192.168.20.97,192.168.53.21

上面配置表示以find*,list*,get*,is*方法命名开始的方法只能访问192.168.53.10,192.168.53.11,192.168.53.12服务提供者,而不是find*,list*,get*,is*方法命名开始的方法只能访问192.168.20.97,192.168.53.21服务提供者。

  1. 提供者与消费者部署在同集群内,本机只访问本机的服务
=> host = $host

上面配置表示所有消费者只能访问集群内的服务。

4. 示例演示

我们以获取图书列表为例进行实例演示,其中我们会启动两个服务提供者配置两个端口:2088020881,然后指定路由规则为:调用方法queryAll访问20880、调用方法queryByName访问20881服务。项目结构图如下:

这里我们主要看服务提供端dubbo-provider-xml.xml配置内容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <dubbo:protocol port="20880"/>
<!--分别使用 20881和20880配置启动两个服务-->
<!--    <dubbo:protocol port="20881"/>-->

    <dubbo:application name="demo-provider" metadata-type="remote"/>

    <dubbo:registry address="zookeeper://127.0.0.1:2181"/>

    <bean id="bookFacade" class="com.muke.dubbocourse.tokenverify.provider.BookFacadeImpl"/>

    <!--暴露服务为Dubbo服务-->
    <dubbo:service interface="com.muke.dubbocourse.common.api.BookFacade" ref="bookFacade" token="12345"/>

</beans>

上面我们指定了服务提供者的端口,这里请求小伙伴分别以2088020881启动两个服务。接下来我们看看在 Dubbo Admin 中配置的路由规则:

enabled: true
runtime: false
force: true
conditions:
 - 'method = queryAll => address=*:20880'
 - 'method = queryByName => address=*:20881'

如下图所示:

**Tips:**这里的Service Unique ID 配置规则为:接口权限定名:版本:分组。

5. 实现原理

根据前面的介绍我们知道在消费端调用远程服务时通过路由规则进行服务的过滤,那么我们通过源码简单的分析下这个处理过程。这里我们直接看到路由规则的调用核心代码org.apache.dubbo.rpc.cluster.RouterChain#route核心方法如下:

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

下面展示了我们运行过程中的路由规则:

其中ConditionRouter就是我们的条件路由核心代码如下:

    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
            throws RpcException {
        if (!enabled) {
            return invokers;
        }

        if (CollectionUtils.isEmpty(invokers)) {
            return invokers;
        }
        try {
            //匹配 method = queryAll => address=*:20880 表示中的 method = queryAll 部分
            if (!matchWhen(url, invocation)) {
                return invokers;
            }
            List<Invoker<T>> result = new ArrayList<Invoker<T>>();
            if (thenCondition == null) {
                //..
                return result;
            }
            for (Invoker<T> invoker : invokers) {
                //匹配 method = queryAll => address=*:20880 表示中的 address=*:20880 部分
                if (matchThen(invoker.getUrl(), url)) {
                    result.add(invoker);
                }
            }
            if (!result.isEmpty()) {
                return result;

                //配置force: true 表示如果通过路由规则后没有服务条件的返回一个空集合,否则路由规则无效返回过滤器的 Invoker 远程服务代理列表
            } else if (force) {
                //..
                return result;
            }
        } catch (Throwable t) {
            //...
        }
        return invokers;
    }

这里有两个最为重要的方法分别是:org.apache.dubbo.rpc.cluster.router.condition.ConditionRouter#matchWhenorg.apache.dubbo.rpc.cluster.router.condition.ConditionRouter#matchThenmatchWhen方法主要负责匹配前置条件例如:method = queryAll => address=*:20880 表示中的 method = queryAll 部分,matchThen方法主要负责匹配后置条件method = queryAll => address=*:20880 表示中的 address=*:20880 部分。matchWhen核心代码如下:

   boolean matchWhen(URL url, Invocation invocation) {
        //判断whenCondition条件是否为空,并且执行matchCondition匹配表达式
        return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
    }

方法org.apache.dubbo.rpc.cluster.router.condition.ConditionRouter#matchCondition核心代码如下:

/**
     *
     * 匹配条件
     *
     * @author liyong
     * @date 11:36 PM 2020/11/28
     * @param condition
     * @param url
     * @param param
     * @param invocation
     * @exception
     * @return boolean
     **/
    private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {
        Map<String, String> sample = url.toMap();
        boolean result = false;
        //method = queryAll => address=*:20880
        for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) {
            String key = matchPair.getKey();
            String sampleValue;
            //从invocation中获取调用的真实方法名称
            if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
                sampleValue = invocation.getMethodName();
                //判断是否配置 address
            } else if (ADDRESS_KEY.equals(key)) {
                sampleValue = url.getAddress();
                //判断是否配置 host
            } else if (HOST_KEY.equals(key)) {
                sampleValue = url.getHost();
            } else {
                //从URL转换的map中获取对应key的值
                sampleValue = sample.get(key);
                if (sampleValue == null) {
                    sampleValue = sample.get(key);
                }
            }
            if (sampleValue != null) {
                //匹配条件配置和真实调用参数值是否匹配
                if (!matchPair.getValue().isMatch(sampleValue, param)) {
                    return false;
                } else {
                    result = true;
                }
            } else {
                //没有匹配的条件
                if (!matchPair.getValue().matches.isEmpty()) {
                    return false;
                } else {
                    result = true;
                }
            }
        }
        return result;
    }

上面的代码非常简单小伙伴可以根据注释去学习,其中Map<String, MatchPair> condition结构如下:

从数据结构中我们可以看出我们配置的method = queryByName,我们这里没有配置host默认为*。接下来我们继续看看matchThen方法核心代码如下:

    private boolean matchThen(URL url, URL param) {
         //判断thenCondition条件是否为空,并且执行matchCondition匹配表达式
        return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
    }

这里和上面的matchWhen方法都调用matchCondition,那我们看看thenCondition的数据结构:


是我们配置的规则后半部分address=*:20881。由此我们可以总结:假设我们的条件路由规则是method = queryByName => address=*:20881那么先对服务调用方匹配method = queryByName前部分,如果满足前面部分则继续去匹配规则的后面部分address=*:20881,如果都匹配则 Invoker 代理对象将作为调用代理候选者。

6. 小结

在本小节中我们主要学习了 Dubbo 中路由规则之条件路由以及使用方式。同时也分析了条件路由实现的原理,其本质上是通过过滤器对服务提供者列表进行规则的匹配,如果匹配不上则过滤掉服务提供者。

本节课程的重点如下:

  1. 理解 Dubbo 路由规则和条件路由

  2. 了解了条件路由使用方式

  3. 了解条件路由实现原理

  4. 了解条件路由使用场景

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。

0人推荐
随时随地看视频
慕课网APP