Fork me on GitHub

Dubbo路由层之Router

前言

之前分析了Dubbo路由层的Directory,它的工作主要是负责获取服务提供者列表。回忆一下dubbo官网介绍的图: 下一步的操作是在Router接口这里,今天来看一下它主要做哪些工作。

Router

先看官方文档怎么说:

Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等

之前Directory获取了当前可用的服务提供者,Router则再从里按规则过滤获取需要的提供者。

读写分离,应用隔离,很容易就想到管理dubbo服务相关操作,即我们在管理后台里的禁用等功能可能就是这里做了文章。

看一下接口设计:

public interface Router extends Comparable<Router> {

    /**
     * get the router url.
     *
     * @return url
     */
    URL getUrl();

    /**
     * route.
     *
     * @param invokers
     * @param url        refer url
     * @param invocation
     * @return routed invokers
     * @throws RpcException
     */
    <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

就俩方法:

  • 获取url
  • 路由

再看其实现的子类有哪些:

以及Router家族谱:

三个实现:

  • ScriptRouter
  • ConditionRouter
  • MockInvokersSelector

MockInvokersSelector

先看这个类,看名称就知道跟降级有关。

实现的两个方法之getUrl

public URL getUrl() {
    return null;
}

可以说根本用不到这个方法。

实现的两个方法之route

public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers,
                                  URL url, final Invocation invocation) throws RpcException {
    if (invocation.getAttachments() == null) {
        return getNormalInvokers(invokers); // 获取正常的提供者
    } else {
        String value = invocation.getAttachments().get(Constants.INVOCATION_NEED_MOCK);
        if (value == null)
            return getNormalInvokers(invokers); // 获取正常的提供者
        else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
            return getMockedInvokers(invokers); // 获取降级的提供者,即协议是mock
        }
    }
    return invokers;
}

这也正如注释所说:

A specific Router designed to realize mock feature.

If a request is configured to use mock, then this router guarantees that only the invokers with protocol MOCK appear in final the invoker list, all other invokers will be excluded.

如果url请求被配置为mock,那么只有协议是mock会被保留到服务提供者列表,其他都被排除。代码如下:

private <T> List<Invoker<T>> getMockedInvokers(final List<Invoker<T>> invokers) {
    if (!hasMockProviders(invokers)) {
        return null;
    }
    List<Invoker<T>> sInvokers = new ArrayList<Invoker<T>>(1);
    for (Invoker<T> invoker : invokers) {
        if (invoker.getUrl().getProtocol().equals(Constants.MOCK_PROTOCOL)) {
            sInvokers.add(invoker);
        }
    }
    return sInvokers;
}

ConditionRouter

条件路由。

基于条件表达式的路由规则,如:host = 10.20.153.10 => host = 10.20.153.11

关于规则以及表达式写法介绍,请参考dubbo用户文档手册

管理控制台使用示例

准备工作:启动两个provider(本地单机模拟,仅修改端口号,生产需要两个节点上部署),控制台显示如下:

再启动consumer可见控制台:

管理控制台添加路由规则: 这个规则的意义是把 192.168.99.243添加到黑名单host中。

保存后此时看consumer日志: 可以看到注册中心发起通知给所有消费者,将该Host的服务添加到黑名单中,由于本地开发机只有该Host的两个服务,于是都被加入黑名单后没有可用的提供者,于是报错。

源码分析

根据日志和debug工具,我们很容易定位到何时触发通知,即之前分析过的RegistryDirectory类的notify方法。

如上图所示,这里看到标注的toRouters 方法,将url转成Router对象。

private List<Router> toRouters(List<URL> urls) {
    List<Router> routers = new ArrayList<Router>();
    if (urls == null || urls.isEmpty()) {
        return routers;
    }
    if (urls != null && !urls.isEmpty()) {
        for (URL url : urls) {
            if (Constants.EMPTY_PROTOCOL.equals(url.getProtocol())) {
                continue;
            }
            String routerType = url.getParameter(Constants.ROUTER_KEY);
            if (routerType != null && routerType.length() > 0) {
                url = url.setProtocol(routerType);
            }
            try {
                Router router = routerFactory.getRouter(url); // 将url转为Router对象
                if (!routers.contains(router))
                    routers.add(router);
            } catch (Throwable t) {
                logger.error("convert router url to router error, url: " + url, t);
            }
        }
    }
    return routers;
}

这里的routerFactory工厂为ConditionRouterFactory,所以这里获取的Router对象为ConditionRouterFactory.其创造的对象是构造器创建:

public ConditionRouter(URL url) {
    this.url = url;
    this.priority = url.getParameter(Constants.PRIORITY_KEY, 0);
    this.force = url.getParameter(Constants.FORCE_KEY, false);
    try {
        String rule = url.getParameterAndDecoded(Constants.RULE_KEY);
        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);
        // NOTE: It should be determined on the business level whether the `When condition` can be empty or not.
        this.whenCondition = when;
        this.thenCondition = then;
    } catch (ParseException e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

可见这里针对url做了解析,可见url在dubbo中是多么核心的存在。具体的解析在方法parseRule(String rule)中,就不贴代码了。其次,根据debug工具可见其调用栈,直接截图:

解释下,画个时序图方便整理:

步骤解析:

  1. 当我们在控制台动态增加一条路由规则时,将会触发RegistryDirectorynotify()方法。
  2. notify()内调用setRouter()方法,将通过ConditionRouterFactory获取ConditionRouter路由对象并添加到路由列表。
  3. 继续调用refreshInvoker()方法,这个方法之前介绍过,关键步骤在于toMethodInvokers()获取一个方法名与提供者列表的映射列表。
  4. 调用内部route()方法开始获取可用提供者列表。
  5. RegistryDirectorynotify()方法内部实际上是调用了ConditionRouter对象的route方法过滤获取最终可用提供者列表。

ScriptRouter

脚本路由规则,这个使用的不是很多,这里引用官网截图:

可以简单看一下源码里:

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
    try {
        List<Invoker<T>> invokersCopy = new ArrayList<Invoker<T>>(invokers);
        Compilable compilable = (Compilable) engine;
        Bindings bindings = engine.createBindings();
        bindings.put("invokers", invokersCopy);
        bindings.put("invocation", invocation);
        bindings.put("context", RpcContext.getContext());
        CompiledScript function = compilable.compile(rule);
        Object obj = function.eval(bindings);
        if (obj instanceof Invoker[]) {
            invokersCopy = Arrays.asList((Invoker<T>[]) obj);
        } else if (obj instanceof Object[]) {
            invokersCopy = new ArrayList<Invoker<T>>();
            for (Object inv : (Object[]) obj) {
                invokersCopy.add((Invoker<T>) inv);
            }
        } else {
            invokersCopy = (List<Invoker<T>>) obj;
        }
        return invokersCopy;
    } catch (ScriptException e) {
        //fail then ignore rule .invokers.
        logger.error("route error , rule has been ignored. rule: " + rule + ", method:" + invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
        return invokers;
    }
}

其通过一个ScriptEngine对象,编译了规则获取一个function脚本,其可支持执行eval方法过滤获得提供者列表。

结束语

Router的过程不算复杂,可以发现比较核心的地方还是在之前分析Directory时的notify方法,一切路由规则都是从这触发的。

ps:这次整理的有些慢了,还是不太熟悉画图的过程,画图比较慢,mac上推荐使用OmniGraffle画图,提供的模板还是比较强大的。

-------------本文结束,感谢您的阅读-------------
贵在坚持,如果您觉得本文还不错,不妨打赏一下~
0%