Dubbo 系列(07-2)集群容错 - 服务路由

1. 背景介绍

相关文档推荐:

  1. Dubbo 路由规则配置
  2. Dubbo 源码解读 - 服务路由

在上一节 Dubbo 系列(06-1)集群容错 - 服务字典 中分析服务字典的源码,服务字典是 Dubbo 集群容错的基础,这节只在服务字典的基础上继续分析服务路由策略。

Dubbo 服务路由分为条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。其中条件路由是我们最常使用的。

  • 条件路由:用户使用 Dubbo 定义的语法规则定义路由规则;
  • 文件路由:需要提交一个文件,里面定义的路由规则;
  • 脚本路由:使用 JDK 自身的脚本引擎解析路由规则脚本。

路由配置规则: [服务消费者匹配条件] => [服务提供者匹配条件] 。 如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。如 host = 10.20.153.10 => host = 10.20.153.11 ,表示 IP 为 10.20.153.10 的消费者会路由到 IP 为 10.20.153.11 的服务者。

1.1 继承体系

图1 Dubbo服务路由继承体系图

总结: Dubbo 设计的核心理念是:微内核富插件。和其它组件一样,路由策略也是通过 Dubbo SPI 动态生成的。每一个路由规则都对应一个工厂类,工厂类则是 SPI 自适应扩展点。

public interface Router extends Comparable<Router> {
<T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException; // routerUrl
URL getUrl();
boolean isForce();
...
}

总结: Router 最核心的方法是 route,会根据路由规则过滤 invokers,返回可用的 Invoker。

1.2 SPI

RouterFactory 是一个 SPI 接口,没有默认值,通过获取 URL.protocol 协议来创建对应的 Router 路由规则。

@SPI
public interface RouterFactory {
@Adaptive("protocol")
Router getRouter(URL url);
}

总结: 在 Dubbo-2.7.3 中默认的 org.apache.dubbo.rpc.cluster.RouterFactory 规则有以下几个。

file=org.apache.dubbo.rpc.cluster.router.file.FileRouterFactory
script=org.apache.dubbo.rpc.cluster.router.script.ScriptRouterFactory
condition=org.apache.dubbo.rpc.cluster.router.condition.ConditionRouterFactory
service=org.apache.dubbo.rpc.cluster.router.condition.config.ServiceRouterFactory
app=org.apache.dubbo.rpc.cluster.router.condition.config.AppRouterFactory
tag=org.apache.dubbo.rpc.cluster.router.tag.TagRouterFactory
mock=org.apache.dubbo.rpc.cluster.router.mock.MockRouterFactory

其中自适应扩展点有四个(也就是默认会加载的路由规则策略),按优先级依次为:mock > tag > app > service。

2. 源码分析

在上一节的分析服务字典源码时,当注册信息更新时会调用 notify 方法通知 RegistryDirectory 更新服务列表,其中一步就是根据配置的路由 routerURLs 解析 Router。先回顾一下之前的代码:

// 1. 路由规则创建
@Override
public synchronized void notify(List<URL> urls) {
// routerURLs
List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
toRouters(routerURLs).ifPresent(this::addRouters);
...
} // 2. 路由规则使用,过滤 invokers
@Override
public List<Invoker<T>> doList(Invocation invocation) {
...
List<Invoker<T>> invokers = routerChain.route(getConsumerUrl(), invocation);
return invokers == null ? Collections.emptyList() : invokers;
}

总结: toRouters(routerURLs) 实际上在解析路由规则,如果有更新则重新设置 routeChain 的路由规则。而 doList 方法时会根据路由规则过滤服务。routeChain 会依次调用 routers,最终得到可用的 invokers。

2.1 创建路由规则

ROUTER_FACTORY 会读取 routerUrl.protocol 参数,决定使用那种路由策略,再根据 routerUrl.rule 参数解析对应的路由规则。

private static final RouterFactory ROUTER_FACTORY = ExtensionLoader.getExtensionLoader(RouterFactory.class)
.getAdaptiveExtension(); // routerUrl 中 router 定义路由类型,rule 定义具体的路由规则
private Optional<List<Router>> toRouters(List<URL> urls) {
if (urls == null || urls.isEmpty()) {
return Optional.empty();
} List<Router> routers = new ArrayList<>();
for (URL url : urls) {
if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
continue;
}
// 将 url.router 参数设置为 protocol
String routerType = url.getParameter(ROUTER_KEY);
if (routerType != null && routerType.length() > 0) {
url = url.setProtocol(routerType);
}
try {
Router router = ROUTER_FACTORY.getRouter(url);
if (!routers.contains(router)) {
routers.add(router);
}
} catch (Throwable t) {
logger.error("convert router url to router error, url: " + url, t);
}
}
return Optional.of(routers);
}

总结: toRouters 最核心的代码就是 Router router = ROUTER_FACTORY.getRouter(url) 创建路由规则。路由规则类型是由 url.router 决定的。

2.2 RouteChain

RouteChain 用于管理所有的路由规则,内部维护有几个重要的集合:

// 1. Dubbo 内部默认的路由规则(四种):
// MockInvokersSelector > TagRouter > AppRouter > ServiceRouter
private List<Router> builtinRouters = Collections.emptyList();
// 2. 自定义的路由规则
private volatile List<Router> routers = Collections.emptyList(); // 3. 所有可用的服务(Invoker 可简单理解为一个可执行的服务)
private List<Invoker<T>> invokers = Collections.emptyList();

2.2.1 内建路由规则

RouterChain#buildChain 会调用私有的构造函数,在初始化时会创建默认的路由规则。

// url: 订阅者URL
// 默认有四个路由规则:MockInvokersSelector/TagRouter/AppRouter/ServiceRouter
private RouterChain(URL url) {
List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)
.getActivateExtension(url, (String[]) null); // 创建内建的路由规则
List<Router> routers = extensionFactories.stream()
.map(factory -> factory.getRouter(url))
.collect(Collectors.toList());
initWithRouters(routers);
}
public void initWithRouters(List<Router> builtinRouters) {
this.builtinRouters = builtinRouters;
this.routers = new ArrayList<>(builtinRouters);
this.sort();
}

2.2.2 更新路由规则

当路由规则更新时会调用 addRouters 更新路由规则,更新时仍保留内建的路由规则。

public void addRouters(List<Router> routers) {
List<Router> newRouters = new ArrayList<>();
newRouters.addAll(builtinRouters);
newRouters.addAll(routers);
CollectionUtils.sort(newRouters);
this.routers = newRouters;
}

总结: 可以看到 builtinRouters 内建的路由规则仍会保留,路由规则的会通过排序来保证执行顺序。其实 Spring 的 BeanPostProcessor 也是保存在一个 List 中通过排序来保证执行顺序的。路由规则的更新是在 RegistryDirectory#notify 通知时。

2.2.3 更新服务列表

同路由规则的更新一样,也是在 RegistryDirectory#notify 时更新服务列表。

public void setInvokers(List<Invoker<T>> invokers) {
this.invokers = (invokers == null ? Collections.emptyList() : invokers);
routers.forEach(router -> router.notify(this.invokers));
}

2.2.4 执行服务路由

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

总结: 逐个调用 Router 进行路由,这个就很简单了。

2.3 条件路由

路由规则使用见 Dubbo 路由使用手册

"condition://0.0.0.0/org.apache.demo.DemoService?category=routers&dynamic=false&rule=" + URL.encode("host=10.20.153.10 => host=10.20.153.11")

2.3.1 条件路由规则解析

在 ConditionRouterFactory#getRouter(URL url) 直接 new ConditionRouter(url) 后就返回了,Dubbo SPI 的工厂类一般都很简单。

public ConditionRouter(URL url) {
this.url = url;
this.priority = url.getParameter(PRIORITY_KEY, 0); // 优先级
this.force = url.getParameter(FORCE_KEY, false); // 过滤后没有服务可用,是否强制执行
this.enabled = url.getParameter(ENABLED_KEY, true); // enabled是否启动
init(url.getParameterAndDecoded(RULE_KEY)); // 解析路由规则 url.rule
}

总结: ConditionRouter 解析 url.rule 配置的路由规则。条件路由配置示例如下:host = 10.20.153.10 => host = 10.20.153.11,左侧是消费者配置规则,右侧是服务者配置规则,表示消费者 host=10.20.153.10 会路由到服务者 host=10.20.153.11。

// host = 10.20.153.10 => host = 10.20.153.11
public void init(String rule) {
try {
if (rule == null || rule.trim().length() == 0) {
throw new IllegalArgumentException("Illegal route rule!");
}
rule = rule.replace("consumer.", "")
.replace("provider.", "");
int i = rule.indexOf("=>");
String whenRule = i < 0 ? null : rule.substring(0, i).trim();
String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
// 消费者规则解析
Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ?
new HashMap<String, MatchPair>() : parseRule(whenRule);
// 服务者规则解析
Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ?
null : parseRule(thenRule); this.whenCondition = when;
this.thenCondition = then;
} catch (ParseException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}

总结: 在 ConditionRouter 中使用两个 Map 保存了对应的匹配规则,最终解析都是在 parseRule(thenRule) 方法中完成的。

Map<String, MatchPair> when;
Map<String, MatchPair> then; private static final class MatchPair {
final Set<String> matches = new HashSet<String>();
final Set<String> mismatches = new HashSet<String>();
}

这个 Map 的 key 表示匹配项,最终将匹配规则解析成如下结构:

// host = 2.2.2.2 & host != 1.1.1.1 & method = hello
{
"host": {
"matches": ["2.2.2.2"],
"mismatches": ["1.1.1.1"]
},
"method": {
"matches": ["hello"],
"mismatches": []
}
}

2.3.2 执行条件路由

执行条件路由其实就是路由规则的匹配过程:

  1. 如果禁用路由规则,直接返回原列表。
  2. 如果服务消费者匹配上,就匹配其可用的服务列表。
  3. 服务消费者匹配条件为空,表示不对服务消费者进行限制。
  4. 如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。
  5. 如果匹配后没有可用的服务,force=true表示强制执行路由规则,返回空列表,否则返回原列表。
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
// 1.1 禁用路由规则
if (!enabled) {
return invokers;
} // 1.2 没有可用的服务
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
try {
// 先对服务消费者条件进行匹配,如果匹配失败,表明服务消费者 url 不符合匹配规则,
// 无需进行后续匹配,直接返回 Invoker 列表即可。比如下面的规则:
// host = 10.20.153.10 => host = 10.0.0.10
// 这条路由规则希望 IP 为 10.20.153.10 的服务消费者调用 IP 为 10.0.0.10 机器上的服务。
// 当消费者 ip 为 10.20.153.11 时,matchWhen 返回 false,表明当前这条路由规则不适用于
// 当前的服务消费者,此时无需再进行后续匹配,直接返回即可。
// 2.1 消费者规则无法匹配,表示不对服务消费者进行限制
if (!matchWhen(url, invocation)) {
return invokers;
}
List<Invoker<T>> result = new ArrayList<Invoker<T>>();
// 2.2 服务者规则放弃匹配,表明对指定的服务消费者禁用服务
if (thenCondition == null) {
return result;
}
// 2.3 服务提供者匹配规则,匹配成功表示进行服务路由
for (Invoker<T> invoker : invokers) {
// 若匹配成功,表明当前 Invoker 符合服务提供者匹配规则
if (matchThen(invoker.getUrl(), url)) {
result.add(invoker);
}
} // 2.4 匹配后没有服务可用,是否强制执行,也就是没有服务可用
// force=false 表示路由规则将自动失效
if (!result.isEmpty()) {
return result;
} else if (force) {
return result;
}
} catch (Throwable t) {
}
// 2.5 出现异常或force=false,表示该条路由规则失效
return invokers;
}

总结: 具体的匹配逻辑都委托给了 matchWhen(url, invocation) 方法。

关于条件路由,规则的解析和具体的匹配过程都没有深入分析,目前来说,了解路由规则的整个运行流程更重要,如果以后用到 Dubbo 的路由规则,出了问题再做具体的深入研究,现在就到此为至。感兴趣的朋友可以参考:Dubbo 源码解读 - 服务路由


每天用心记录一点点。内容也许不重要,但习惯很重要!

Dubbo 系列(07-2)集群容错 - 服务路由的更多相关文章

  1. Dubbo 系列(07-1)集群容错 - 服务字典

    Dubbo 系列(07-1)集群容错 - 服务字典 [toc] Spring Cloud Alibaba 系列目录 - Dubbo 篇 1. 背景介绍 本篇文章,将开始分析 Dubbo 集群容错方面的 ...

  2. Dubbo 源码分析 - 集群容错之 LoadBalance

    1.简介 LoadBalance 中文意思为负载均衡,它的职责是将网络请求,或者其他形式的负载"均摊"到不同的机器上.避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况.通 ...

  3. Dubbo 源码分析 - 集群容错之 Cluster

    1.简介 为了避免单点故障,现在的应用至少会部署在两台服务器上.对于一些负载比较高的服务,会部署更多台服务器.这样,同一环境下的服务提供者数量会大于1.对于服务消费者来说,同一环境下出现了多个服务提供 ...

  4. Dubbo 源码分析 - 集群容错之 Router

    1. 简介 上一篇文章分析了集群容错的第一部分 -- 服务目录 Directory.服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由.上一篇文章关于服务路由相关逻辑没有 ...

  5. Dubbo工作原理,集群容错,负载均衡

    Remoting:网络通信框架,实现了sync-over-async和request-response消息机制. RPC:一个远程过程调用的抽象,支持负载均衡.容灾和集群功能. Registry:服务 ...

  6. Dubbo 源码分析 - 集群容错之 Directory

    1. 简介 前面文章分析了服务的导出与引用过程,从本篇文章开始,我将开始分析 Dubbo 集群容错方面的源码.这部分源码包含四个部分,分别是服务目录 Directory.服务路由 Router.集群 ...

  7. Dubbo负载均衡与集群容错机制

    1  Dubbo简介 Dubbo是一款高性能.轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现. 作为一个轻量级RPC框架,D ...

  8. dubbo源码分析- 集群容错之Cluster(一)

    1.集群容错的配置项 failover - 失败自动切换,当出现失败,重试其他服务器(缺省),通常用于读操作,但重试会带来更长的延时. failfast - 快速失效,只发起一次调用,失败立即报错.通 ...

  9. Dubbo的10种集群容错模式

    学习Dubbo源码的过程中,首先看到的是dubbo的集群容错模式,以下简单介绍10种集群容错模式 1.AvailableCluster 顾名思义,就是可用性优先,遍历所有的invokers,选择可用的 ...

随机推荐

  1. elasticsearch 基础 —— 索引、更新文档

    索引文档 通过使用 index API ,文档可以被 索引 -- 存储和使文档可被搜索 . 但是首先,我们要确定文档的位置.正如我们刚刚讨论的,一个文档的 _index . _type 和 _id 唯 ...

  2. 2018-2-13-WPF-资源冻结

    title author date CreateTime categories WPF 资源冻结 lindexi 2018-2-13 17:23:3 +0800 2018-2-13 17:23:3 + ...

  3. Redis这篇就够了

    Redis 简介 Redis 优势 Redis 数据类型 string hash list set Zset 小总结 基本命令 发布订阅 简介 实例 发布订阅常用命令 事务 实例 Redis 事务命令 ...

  4. 记一次redis读取超时的排查过程(SADD惹的祸)

    问题背景 在业务使用redis过程中,出现了read timeout 的异常. 问题排查 直接原因 运维查询redis慢查询日志,发现在异常时间节点,有redis慢查询日志,执行sadd 命令花费了1 ...

  5. 靶场练习--sqli(3&4)

    第三关 先解决一下第二关遗留下来的问题,嘻嘻.看来数据库原理应当过一遍~ 1.首先判断是否有SQL注入,然后再看是数字型.字符型.发现这里是字符型. 2.order by 查询字段数,记得后面要加一个 ...

  6. MySQL--17 配置binlog-server 及中间件

    目录 配置binlog-server MySQL中间件Atlas Atlas管理接口 配置binlog-server 修改mha配置文件 [root@mysql-db03 ~]# vim /etc/m ...

  7. HTML基础 用div布局实现一个简单网页

    div布局如下 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <t ...

  8. bzoj5161 最长上升子序列 状压DP(DP 套 DP) + 打表

    题目传送门 https://lydsy.com/JudgeOnline/problem.php?id=5161 题解 回顾一下以前用二分求 LIS 的方法:令 \(f[i]\) 表示长度为 \(i\) ...

  9. 分布式架构的CAP原理

    CAP 定理的含义   一.分布式系统的三个指标 1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标. Consistency Availability Parti ...

  10. HTTP协议缓存

    缓存的概念 缓存这个东西真的是无处不在, 有浏览器端的缓存, 有服务器端的缓存,有代理服务器的缓存, 有ASP.NET页面缓存,对象缓存. 数据库也有缓存, 等等. http中具有缓存功能的是浏览器缓 ...