上篇文章介绍了服务目录 Directory。服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。Dubbo 目前提供了三种服务路由实现,分别为条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter
如图所示,经过Router过滤后的Invoker列表实际是Directory中Invoker列表的子集,起到过滤的作用
本文目的不在于分析Router实现的原理,个人认为学会Router路由规则的使用对实际工作更加有效果。本文会结合Dubbo keeper和实际例子来说明Router路由规则中条件路由和脚本路由的使用
插播我自己瞎想面试题
- 如何屏蔽掉部分提供者或者消费者?
- 如何在Dubbo中实现读写分离(读写接口调用不同机器)?
希望读者能有目的性的去看文章
条件路由ConditionRouter
什么是条件路由
条件路由即基于条件表达式的路由规则,如:host = 10.20.153.10 => host = 10.20.153.11
什么意思呢?
- => 之前的为消费者匹配条件,所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。
- => 之后为提供者地址列表的过滤条件,所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。
- 如果匹配条件为空,表示对所有消费方应用,如:=> host != 10.20.153.11
- 如果过滤条件为空,表示禁止访问,如:host = 10.20.153.10 =>
所以上面条件路由例子的意思就是:所有消费者ip为10.20.153.10的请求全部交由ip为10.20.153.11的提供者处理
更多的规则说明可以参考Dubbo官方文档之路由规则
下面结合Dubbo keeper运维平台来说明条件路由规则使用,首先定义如下接口,就是简单三个方法,
服务提供者同时部署在192.168.207.89(我本地机器),192.168.112.12测试机器上,相当于有2个提供者
假设我调用sayHello方法,则会同时路由到2台机器上,调用结果如下,既会调用到本地机器,也会调用到测试机器,具体调用哪个由配置的负载均衡策略决定
屏蔽部分提供者
比如192.168.112.12这台机器要上线,需要暂时切断到这台机器的dubbo调用,怎么做呢?
基本思路肯定是过滤掉提供者ip为192.168.112.12的Invoker
DubboKeeper中找到对应服务,点击按钮新建路由规则
路由详情中如下填下
此时再调用sayHello方法,效果图如下,192.168.112.12这台机器便不会被调用到
如果192.168.112.12发布、升级成功,可以被调用了,则点击路由规则的禁用按钮或者删除按钮即可
读写分离
有的时候,读写接口调用次数不平衡(读多写少的场景),所有请求全部打到一台机器上,可能会影响写入接口性能。基于Dubbo的Router也可以实现读写分离机制。即读接口调用A机器,写接口调用B机器,这样互不影响,减少了读接口对写接口的影响。
具体怎么做呢,还是以上面的读写接口为例
读接口read方法配置如下规则,等价于如下表达式:method = read* => host = 192.168.207.89
,即读方法全部走本机调用
调用读方法可以看到全部由我本机执行
写方法类似,等价于 method = write* => host = 192.168.112.12
,即写方法全部调用测试机
脚本路由 ScriptRouter
什么是脚本路由?
脚本路由规则 支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript
举个例子,如下脚本等价于,所有包含get的方法全部调用192.168.0.23这台提供者
(function(invokers,invocation,context){
var methodName = invocation.methodName;
var resultInvokers=[];
for(var i=0;i<invokers.length;i++){
var invoker = invokers[i];
var currentConsumerUrl = invoker.url;
var providerUrl = invoker.providerUrl;
var consumerHost=currentConsumerUrl.host;
var providerHost = providerUrl.host;
var providerProtocol = providerUrl.protocol;
var currentService = providerUrl.getParameter("interface");
if(methodName.indexOf('get')>=0&&providerHost=='192.168.0.23'){
resultInvokers.push(invoker);
}
}
//该方法返回值必须是一个Invoker的集合或者数组
return resultInvokers;})(invokers,invocation,context);
脚本路由实现读写分离
下面仍然以上文读写分离的例子来说明脚本路由的使用,目标还是读方法调用我本机(192.168.207.88),写方法调用(192.168.112.12)
Dubbo keeper中点击如下按钮新增脚本路由规则
配置如下脚本路由规则
(function route(invokers,invocation) {
var result = new java.util.ArrayList();
//读方法只调用192.168.207.88
if("read".equals(invocation.getMethodName())) {
for (var i=0;i<invokers.size(); i++) {
if(invokers.get(i).getUrl().getHost() =="192.168.207.88") {
result.add(invokers.get(i));
}
}
}else if("write".equals(invocation.getMethodName())){
//写方法只调用192.168.112.12
for (var i=0;i<invokers.size(); i++) {
if(invokers.get(i).getUrl().getHost() =="192.168.112.12") {
result.add(invokers.get(i));
}
}
}
return result;
}(invokers,invocation));
可以看到读写分离已经起到了效果
最后再介绍下脚本路由中的入参说明,还是以上文脚本路由代码为例
(function(invokers,invocation,context){
var methodName = invocation.methodName;
var resultInvokers=[];
for(var i=0;i<invokers.length;i++){
var invoker = invokers[i];
var currentConsumerUrl = invoker.url;
var providerUrl = invoker.providerUrl;
var consumerHost=currentConsumerUrl.host;
var providerHost = providerUrl.host;
var providerProtocol = providerUrl.protocol;
var currentService = providerUrl.getParameter("interface");
if(methodName.indexOf('get')>=0&&providerHost=='192.168.0.23'){
resultInvokers.push(invoker);
}
}
//该方法返回值必须是一个Invoker的集合或者数组
return resultInvokers;})(invokers,invocation,context);
Dubbo在调用脚本的时候均会调用该函数,Dubbo会提供三个入参
1、invokers:这个是一个InvokerDelegete类的实体集合,该类有三个属性invoker(具备远程通信能力的Invoker子类对象),url(当前消费端的URL对象),providerUrl(当前invoker包装的提供端地址URL对象), 可以简单理解为包含所有提供者ip、端口信息的列表
2、invocation:由于Dubbo的服务调用是以方法为单位,那么每个方法都会匹配一批Invoker集合,该参数则指明了当前方法信息, 不过由于当前并没有进行方法调用,只是服务的订阅,所以此对象只有一个属性可用methodName
3、context:当前线程的服务调用上线文,由于当前服务并不是处于调用状态,所以该参数并没有实际用途
由第二点可知,如果想从invocation中去获取当次调用的入参,那是不可行的,只有方法名methodName可以获取到