前言
之前分析了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工具可见其调用栈,直接截图:
解释下,画个时序图方便整理:
步骤解析:
- 当我们在控制台动态增加一条路由规则时,将会触发
RegistryDirectory
的notify()
方法。 - 在
notify()
内调用setRouter()
方法,将通过ConditionRouterFactory
获取ConditionRouter
路由对象并添加到路由列表。 - 继续调用
refreshInvoker()
方法,这个方法之前介绍过,关键步骤在于toMethodInvokers()
获取一个方法名与提供者列表的映射列表。 - 调用内部
route()
方法开始获取可用提供者列表。 RegistryDirectory
的notify()
方法内部实际上是调用了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画图,提供的模板还是比较强大的。