集群容错中的第二个关键词Router,中文意思就是路由 前端的路由和后端的路由他们是不同的,但是思想是基本一致的. 鉴于很多技术文章都有一个诟病,就是只讲概念,却不讲应用场景,其实Router在应用隔离,读写分离,灰度发布中都有它的影子.因此本篇用灰度发布的例子来做前期的铺垫
灰度发布
- 百度百科
你发布应用的时候,不停止对外的服务,也就是让用户感觉不到你在发布
那么下面演示一下灰度发布
1.首先在192.168.56.2和192.168.56.3两台机器上启动Provider,然后启动Consumer,如下图
2.假设我们要升级192.168.56.2服务器上的服务,接着我们去dubbo的控制台配置路由,切断192.168.56.2的流量,配置完成并且启动之后,就看到此时只调用192.168.56.3的服务
3.假设此时你在192.168.56.2服务器升级服务,升级完成后再次将启动服务.
4.由于服务已经升级完成,那么我们此时我们要把刚才的禁用路由取消点,于是点了禁用,但是此时dubbo的这个管理平台就出现了bug,如下图所示
惊奇的发现点了禁用,数据就变两条了,继续点禁用,还是两条,而且删除还删除不了,这样就很蛋疼了…但是一直删不了也不是办法,解决办法也是有的,那就是去zookeeper上删除节点
Mac上好像没有特别好用的zookeeper可视化客户端工具,于是我就用了这个idea的zookeeper插件
只要将这个zookeeper节点删除
然后刷新控制台的界面,如下图那么就只剩下一条了
6.那么此时我们再看控制台的输出,已经恢复正常,整个灰度发布流程结束
Router的继承体系图
从图中可以看出,他有四个实现类
- MockInvokersSelector在Dubbo 源码解析(一) - 集群架构的设计中提到这里
- ScriptRouter在dubbo的测试用例中就有用到,这个类的源码不多,也就124行.引用官网的描述
脚本路由规则 支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。
当然看到这里可能你可能还是没有感觉出这个类有什么不可替代的作用,你注意一下这个类中有个ScriptEngine的属性
那么我可以举一个应用场景给你
假如有这么个表达式如下:
代码语言:javascript复制double d = (1 1-(2-4)*2)/24; //没有问题
// 但是假如这个表达式是这样的字符串格式,或者更复杂的运算,那么你就不好处理了
// 然后这个ScriptEngine类的eval方法就能很好处理这类字符串表达式的问题
"(1 1-(2-4)*2)/24"
本篇主要讲讲
ConditionRouter(条件路由)
条件路由主要就是根据dubbo管理控制台配置的路由规则来过滤相关的invoker,当我们对路由规则点击启用的时候,就会触发RegistryDirectory类的notify方法
代码语言:javascript复制 @Override
public synchronized void notify(List<URL> urls) {
List<URL> invokerUrls = new ArrayList<URL>();
List<URL> routerUrls = new ArrayList<URL>();
List<URL> configuratorUrls = new ArrayList<URL>();
for (URL url : urls) {
String protocol = url.getProtocol();
String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
if (Constants.ROUTERS_CATEGORY.equals(category)
|| Constants.ROUTE_PROTOCOL.equals(protocol)) {
routerUrls.add(url);
} else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
|| Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
configuratorUrls.add(url);
} else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
invokerUrls.add(url);
} else {
logger.warn("Unsupported category " category " in notified url: " url " from registry " getUrl().getAddress() " to consumer " NetUtils.getLocalHost());
}
}
// configurators
if (configuratorUrls != null && !configuratorUrls.isEmpty()) {
this.configurators = toConfigurators(configuratorUrls);
}
// routers
if (routerUrls != null && !routerUrls.isEmpty()) {
List<Router> routers = toRouters(routerUrls);
if (routers != null) { // null - do nothing
setRouters(routers);
}
}
List<Configurator> localConfigurators = this.configurators; // local reference
// merge override parameters
this.overrideDirectoryUrl = directoryUrl;
if (localConfigurators != null && !localConfigurators.isEmpty()) {
for (Configurator configurator : localConfigurators) {
this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
}
}
// providers
refreshInvoker(invokerUrls);
}
为什么这个notify方法传入的是List呢? 引用一段官网文档的描述
所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的 “对应URL参数” 列
其实对于Router
来说,我们最关心的就是他是怎么过滤的.所以下面这些流程代码我们先走一遍
/**
* 将 invokerURL 列表转换为Invoker Map。 转换规则如下:
* 1. 如果已将URL转换为invoker,则不再将重新引用该URL且直接从缓存中获取它,并且请注意,URL中的任何参数更改都将被重新引用。
* 2. 如果传入的invoker列表不为空,则表示它是最新的invoker列表
* 3. 如果传入的invokerUrl列表为空,则表示该规则只是覆盖规则或路由规则,需要重新进行比较以决定是否重新引用。
*
* @参数 invokerUrls 此参数不能为空
*/
// TODO: 2017/8/31 FIXME 应使用线程池刷新地址,否则可能会积累任务。
private void refreshInvoker(List<URL> invokerUrls) {
if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
&& Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
this.forbidden = true; // 禁止访问
this.methodInvokerMap = null; // 将方法invoker map设置为null
destroyAllInvokers(); //关闭所有invoker
} else {
this.forbidden = false; // 允许访问
Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // 本地引用
if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
invokerUrls.addAll(this.cachedInvokerUrls);
} else {
this.cachedInvokerUrls = new HashSet<URL>();
this.cachedInvokerUrls.addAll(invokerUrls);// 缓存的invoker网址,便于比较
}
if (invokerUrls.isEmpty()) {
return;
}
Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);// Translate url list to Invoker map
Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // 更改方法名称以映射Invoker Map
// state change
// If the calculation is wrong, it is not processed.
if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :" invokerUrls.size() ", invoker.size :0. urls :" invokerUrls.toString()));
return;
}
this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
this.urlInvokerMap = newUrlInvokerMap;
try {
destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap); // Close the unused Invoker
} catch (Exception e) {
logger.warn("destroyUnusedInvokers error. ", e);
}
}
}
代码语言:javascript复制 /**
* 使用方法将invokers列表转换为映射关系
*
* @param invokersMap Invoker Map
* @return Mapping relation between Invoker and method
*/
private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<>();
// 根据provider URL声明的方法分类,这些方法与注册表兼容以执行过滤的方法
List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>();
if (invokersMap != null && invokersMap.size() > 0) {
for (Invoker<T> invoker : invokersMap.values()) {
String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY);
if (parameter != null && parameter.length() > 0) {
String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter);
if (methods != null && methods.length > 0) {
for (String method : methods) {
if (method != null && method.length() > 0
&& !Constants.ANY_VALUE.equals(method)) {
List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
if (methodInvokers == null) {
methodInvokers = new ArrayList<Invoker<T>>();
newMethodInvokerMap.put(method, methodInvokers);
}
methodInvokers.add(invoker);
}
}
}
}
invokersList.add(invoker);
}
}
List<Invoker<T>> newInvokersList = route(invokersList, null);
newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList);
if (serviceMethods != null && serviceMethods.length > 0) {
for (String method : serviceMethods) {
List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
if (methodInvokers == null || methodInvokers.isEmpty()) {
methodInvokers = newInvokersList;
}
newMethodInvokerMap.put(method, route(methodInvokers, method));
}
}
// 排序且不可修改
for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) {
List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
Collections.sort(methodInvokers, InvokerComparator.getComparator());
newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers));
}
return Collections.unmodifiableMap(newMethodInvokerMap);
}
这个条件路由有一个特点,就是他的getUrl是有值的
从这里我们看到,此时实现类是ConditionRouter,由于接下来的逻辑如果直接让大家看源码图可能不够清晰,所以我又把这个核心的筛选过程用了一个高清无码图,并且用序号标注
最后的筛选结果如下,因为我们在管理后台配置了禁用192.168.56.2,所以最后添加进invokers的就只有192.168.56.3
参考
dubbo源码解析-router