zoukankan      html  css  js  c++  java
  • Dubbo 系列(07-2)集群容错

    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 源码解读 - 服务路由


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

  • 相关阅读:
    python 查看所有的关键字
    使用yum命令报错File "/usr/bin/yum", line 30 except KeyboardInterrupt, e: SyntaxError: invalid syntax问题
    安装Python3.6.2报错:zipimport.ZipImportError: can't decompress data; zlib not available
    在CentOS-7.0中安装Python3.6.2
    批处理基础
    linux创建线程之pthread_create
    嵌入式 printf函数
    滤波算法
    单片机启动文件
    SUID、SGID详解
  • 原文地址:https://www.cnblogs.com/binarylei/p/11674352.html
Copyright © 2011-2022 走看看