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


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

最新文章

  1. Be careful about the upper limit of integer
  2. jsoup简单的爬取网页数据
  3. NFV技术中遇到的新名词
  4. hdu5119 dp
  5. python基本数据结构-字典-方法
  6. must implement the inherited abstract method DialogInterface.OnClickListener.onClick(DialogInterface, int)问题
  7. 几个 PHP 的“魔术常量”
  8. linux image writes boot log to console
  9. 1305 Pairwise Sum and Divide
  10. Java线程池实现原理之自定义线程池(一)
  11. RESTClient 使用
  12. 【blog】MarkDown语法解析为HTML工具
  13. SqlSessionFactoryBean的构建流程
  14. python基础知识小结-运维笔记
  15. style css
  16. hg 添加用户
  17. Codefroces 628B New Skateboard(数位+思维)
  18. spark+phoenix
  19. FIR滤波器实例
  20. 【spark】原理

热门文章

  1. [fw]Linux系统使用time计算命令执行的时间
  2. ZOJ 2706 Thermal Death of the Universe
  3. k8s入门教程
  4. jquery实现表单验证与页面加载之后执行渲染
  5. HTML5中canvas与SVG有什么区别
  6. ubuntu vim8.1编译安装
  7. 【linux 进程杀死】批量杀死进程
  8. how to pass variable from shell script to sqlplus
  9. Linux双网卡绑定bond详解
  10. 牛客小白月赛16 D 小阳买水果 (思维题)