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 自适应扩展点。

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

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

1.2 SPI

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

  1. @SPI
  2. public interface RouterFactory {
  3. @Adaptive("protocol")
  4. Router getRouter(URL url);
  5. }

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

  1. file=org.apache.dubbo.rpc.cluster.router.file.FileRouterFactory
  2. script=org.apache.dubbo.rpc.cluster.router.script.ScriptRouterFactory
  3. condition=org.apache.dubbo.rpc.cluster.router.condition.ConditionRouterFactory
  4. service=org.apache.dubbo.rpc.cluster.router.condition.config.ServiceRouterFactory
  5. app=org.apache.dubbo.rpc.cluster.router.condition.config.AppRouterFactory
  6. tag=org.apache.dubbo.rpc.cluster.router.tag.TagRouterFactory
  7. mock=org.apache.dubbo.rpc.cluster.router.mock.MockRouterFactory

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

2. 源码分析

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

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

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

2.1 创建路由规则

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

  1. private static final RouterFactory ROUTER_FACTORY = ExtensionLoader.getExtensionLoader(RouterFactory.class)
  2. .getAdaptiveExtension();
  3. // routerUrl 中 router 定义路由类型,rule 定义具体的路由规则
  4. private Optional<List<Router>> toRouters(List<URL> urls) {
  5. if (urls == null || urls.isEmpty()) {
  6. return Optional.empty();
  7. }
  8. List<Router> routers = new ArrayList<>();
  9. for (URL url : urls) {
  10. if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
  11. continue;
  12. }
  13. // 将 url.router 参数设置为 protocol
  14. String routerType = url.getParameter(ROUTER_KEY);
  15. if (routerType != null && routerType.length() > 0) {
  16. url = url.setProtocol(routerType);
  17. }
  18. try {
  19. Router router = ROUTER_FACTORY.getRouter(url);
  20. if (!routers.contains(router)) {
  21. routers.add(router);
  22. }
  23. } catch (Throwable t) {
  24. logger.error("convert router url to router error, url: " + url, t);
  25. }
  26. }
  27. return Optional.of(routers);
  28. }

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

2.2 RouteChain

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

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

2.2.1 内建路由规则

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

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

2.2.2 更新路由规则

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

  1. public void addRouters(List<Router> routers) {
  2. List<Router> newRouters = new ArrayList<>();
  3. newRouters.addAll(builtinRouters);
  4. newRouters.addAll(routers);
  5. CollectionUtils.sort(newRouters);
  6. this.routers = newRouters;
  7. }

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

2.2.3 更新服务列表

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

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

2.2.4 执行服务路由

  1. public List<Invoker<T>> route(URL url, Invocation invocation) {
  2. List<Invoker<T>> finalInvokers = invokers;
  3. for (Router router : routers) {
  4. finalInvokers = router.route(finalInvokers, url, invocation);
  5. }
  6. return finalInvokers;
  7. }

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

2.3 条件路由

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

  1. "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 的工厂类一般都很简单。

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

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

  1. // host = 10.20.153.10 => host = 10.20.153.11
  2. public void init(String rule) {
  3. try {
  4. if (rule == null || rule.trim().length() == 0) {
  5. throw new IllegalArgumentException("Illegal route rule!");
  6. }
  7. rule = rule.replace("consumer.", "")
  8. .replace("provider.", "");
  9. int i = rule.indexOf("=>");
  10. String whenRule = i < 0 ? null : rule.substring(0, i).trim();
  11. String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
  12. // 消费者规则解析
  13. Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ?
  14. new HashMap<String, MatchPair>() : parseRule(whenRule);
  15. // 服务者规则解析
  16. Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ?
  17. null : parseRule(thenRule);
  18. this.whenCondition = when;
  19. this.thenCondition = then;
  20. } catch (ParseException e) {
  21. throw new IllegalStateException(e.getMessage(), e);
  22. }
  23. }

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

  1. Map<String, MatchPair> when;
  2. Map<String, MatchPair> then;
  3. private static final class MatchPair {
  4. final Set<String> matches = new HashSet<String>();
  5. final Set<String> mismatches = new HashSet<String>();
  6. }

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

  1. // host = 2.2.2.2 & host != 1.1.1.1 & method = hello
  2. {
  3. "host": {
  4. "matches": ["2.2.2.2"],
  5. "mismatches": ["1.1.1.1"]
  6. },
  7. "method": {
  8. "matches": ["hello"],
  9. "mismatches": []
  10. }
  11. }

2.3.2 执行条件路由

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

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

总结: 具体的匹配逻辑都委托给了 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. 20191114PHP文件操作

    <meta charset="utf-8"><?php// $fn=fopen("c:\\abc.txt","w"); / ...

  2. .ef core 多对对关系的关联方法

    最近在用.net core 重构博客,在使用ef core连表查询时,遇到了一些问题.记录一下. 关系:一个博客可以有多个标签,一个标签可以属于多个博客,博客和标签之间存在多对多的关系 下面是实体代码 ...

  3. XMPP即时通讯协议使用(十三)——获取当前在线用户或关闭指定用户

    1.开启REST API插件或根据需求修改其插件源码: 2.添加服务器->服务器管理->系统属性中添加 plugin.restapi.enabled=true 3.pom依赖 <de ...

  4. postman 上一个接口的返回值作为下一个接口的入参

    在使用postman做接口测试的时候,在多个接口的测试中,如果需要上一个接口的返回值作为下一个接口的入参,其基本思路是: 1.获取上一个接口的返回值 2.将返回值设置成环境变量或者全局变量 3.设置下 ...

  5. ob_start()、ob_get_contents() 等使用方法

    ob_start()ob_get_contents(); 获取缓冲区内容ob_end_clean():删除内部缓冲区的内容,并且关闭内部缓冲区 ob_end_flush() 发送内部缓冲区的内容到浏览 ...

  6. Django 配置 qq 邮箱发送邮件

    目录 一.实验环境 二.获取QQ邮箱授权码 1.什么是授权码? 2.怎么获取授权码? 三.Django中配置 setting.py中添加如下代码 文本邮件 HTML 邮件 一.实验环境 Python3 ...

  7. Kotlin 的 Array 转 List

    Kotlin 的 Array 转 List array.toList() as List<T> 1 Kotlin 的 Array 转 ArrayList array.toList() as ...

  8. webdriver原理

    WebDriver 安装C/S构架设计的: 代码(客户端)--->驱动(解析代码)----->浏览器(服务端) 代码通过http请求发给浏览器驱动,驱动解析代码把他们发给浏览器,浏览器执行 ...

  9. 获取配置文件yml的@ConfigurationProperties和@Value的区别

    首先,配置文件的事,我没讲properties,这个写中文的时候,会有乱码,需要去Idea里面设置一下编码格式为UTF-8 还有,我们的类和配置文件直接关联,我用的是ConfigurationProp ...

  10. BZOJ4269 再见xor

    考前挣扎 线性基裸题 mx直接求 次大直接从低到高枚举第一个非0位 然后次大就是异或上就行了[显然贪心呐qwq 不到800b可还行 //Love and Freedom. #include<cs ...