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


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

posted on 2019-10-14 21:40  binarylei  阅读(713)  评论(0编辑  收藏  举报

导航