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
复制代码
字段说明:
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. 使用场景
从上面的简单介绍我们可以大致了解到,当我们消费对访问服务提供者时我们可以通过一定的规则对服务提供者列表进行过滤。那下面我们列举下工作中常使用的场景:
黑名单: 比如我们需要禁止某些服务消费者消费服务
host = 192.168.53.10,192.168.53.11 =>
复制代码
上面配置表示禁止192.168.53.10
、192.168.53.11
消费者访问服务提供者。
服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉
=> host = 192.168.53.1*,192.168.53.2*
复制代码
上面配置表示只能放192.168.53.1*
、192.168.53.2
ip 地址开头的服务提供者。
读写分离:读取数据和写入数据操作分开
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
服务提供者。
提供者与消费者部署在同集群内,本机只访问本机的服务
上面配置表示所有消费者只能访问集群内的服务。
4. 示例演示
我们以获取图书列表为例进行实例演示,其中我们会启动两个服务提供者配置两个端口:20880
、20881
,然后指定路由规则为:调用方法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>
复制代码
上面我们指定了服务提供者的端口,这里请求小伙伴分别以20880
和20881
启动两个服务。接下来我们看看在 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#matchWhen
和org.apache.dubbo.rpc.cluster.router.condition.ConditionRouter#matchThen
,matchWhen
方法主要负责匹配前置条件例如: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 中路由规则之条件路由以及使用方式。同时也分析了条件路由实现的原理,其本质上是通过过滤器对服务提供者列表进行规则的匹配,如果匹配不上则过滤掉服务提供者。
本节课程的重点如下:
理解 Dubbo 路由规则和条件路由
了解了条件路由使用方式
了解条件路由实现原理
了解条件路由使用场景
作者
个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号:青年 IT 男 获取最新技术文章推送!
博客地址: http://youngitman.tech
微信公众号:
评论