dubbo源码深度解读六之cluster模块

来源:互联网 发布:高中生软件培训学校 编辑:程序博客网 时间:2024/06/05 07:01

前言:这是集群模块,将多个服务提供方伪装为一个提供方,包括:负载均衡, 容错,路由等,集群的地址列表可以是静态配置的,也可以是由注册中心下发。

下图描述了dubbo调用过程中的对于集群,负载等的调用关系,根据该图一步步进行解读。
这里写图片描述

一,Cluster
将Directory中的多个Invoker伪装成一个Invoker, 对上层透明,包含集群的容错机制

@SPI(FailoverCluster.NAME)public interface Cluster {    @Adaptive    <T> Invoker<T> join(Directory<T> directory) throws RpcException;}

Cluster类似于一个工厂类,根据不同的集群策略生成不同的cluster。从上可以看出默认的策略是FailoverCluster,当调用失败时候,重试其他服务,通常用于读操作,但重试会带来更长延迟。

public class FailoverCluster implements Cluster {    public final static String NAME = "failover";    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {        return new FailoverClusterInvoker<T>(directory);    }}

可以看出是通过创建一个FailoverClusterInvoker对象。在FailoverClusterInvoker中,会对列表中的invoker进行重新选择。
接下来看看其他的集群方案:

这里写图片描述

在配置时候,我们可以这样配置

<dubbo:service cluster="failsafe" />    服务提供方<dubbo:reference cluster="failsafe" />  服务消费方

二,Directory
1,这是集群目录服务,代表多个invoker,相当于List,它的值可能是动态变化的,比如注册中心推送变更,集群选择调用服务时通过目录服务找到所有服务。

public interface Directory<T> extends Node {    //服务的类型    Class<T> getInterface();    //返回所有服务的可执行对象    List<Invoker<T>> list(Invocation invocation) throws RpcException;}

2,两个实现类StaticDirectory和RegistryDirectory
(1)StaticDirectory
静态目录服务, 它的所有Invoker通过构造函数传入, 服务消费方引用服务的时候, 服务对多注册中心的引用,将Invokers集合直接传入 StaticDirectory构造器,再由Cluster伪装成一个Invoker

(2)RegistryDirectory:
注册目录服务, 它的Invoker集合是从注册中心获取的, 它实现了NotifyListener接口实现了回调接口notify(List)。
比如消费方要调用某远程服务,会向注册中心订阅这个服务的所有服务提供方,订阅时和服务提供方数据有变动时回调消费方的NotifyListener服务的notify方法NotifyListener.notify(List) 回调接口传入所有服务的提供方的url地址然后将urls转化为invokers, 也就是refer应用远程服务(zookeeper也因此很适合做为注册中心)

(三)router
服务路由, 根据路由规则从多个Invoker中选出一个子集。AbstractDirectory是所有目录服务实现的上层抽象, 它在list列举出所有invokers后,会在通过Router服务进行路由过滤。

public interface Router extends Comparable<Router> {    URL getUrl();    <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;}

路由主要有下面两个
(1)ConditionRouter: 条件路由
先看构造函数

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: When条件是允许为空的,外部业务来保证类似的约束条件            this.whenCondition = when;            this.thenCondition = then;        } catch (ParseException e) {            throw new IllegalStateException(e.getMessage(), e);        }    }

步骤大致如下:
1)从url根据RULE_KEY获取路由条件路由内容
2)rule.indexOf(“=>”) 分割路由内容
3)分别调用parseRule(rule) 解析路由为whenRule和thenRules

接下来看route方法

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)            throws RpcException {        if (invokers == null || invokers.size() == 0) {            return invokers;        }        try {            if (! matchWhen(url)) {                return invokers;            }            List<Invoker<T>> result = new ArrayList<Invoker<T>>();            if (thenCondition == null) {                logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());                return result;            }            for (Invoker<T> invoker : invokers) {                if (matchThen(invoker.getUrl(), url)) {                    result.add(invoker);                }            }            if (result.size() > 0) {                return result;            } else if (force) {                logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.RULE_KEY));                return result;            }        } catch (Throwable t) {            logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);        }        return invokers;    }

逻辑大致如下:
1)如果url不满足when条件即过来条件, 不过滤返回所有invokers
2)遍历所有invokers判断是否满足then条件, 将满足条件的加入集合result
3)Result不为空,有满足条件的invokers返回
4)Result为空, 没有满足条件的invokers, 判断参数FORCE_KEY是否强制过来,如果强制过滤返回空, 不是返回所有即不过滤

(2)ScriptRouter: 脚本路由
通过url的RULE_KEY参数获取脚本内容,然后通过Java的脚本引擎执行脚本代码

接下来先看构造函数

public ScriptRouter(URL url) {        this.url = url;        String type = url.getParameter(Constants.TYPE_KEY);        this.priority = url.getParameter(Constants.PRIORITY_KEY, 0);        String rule = url.getParameterAndDecoded(Constants.RULE_KEY);        if (type == null || type.length() == 0){            type = Constants.DEFAULT_SCRIPT_TYPE_KEY;        }        if (rule == null || rule.length() == 0){            throw new IllegalStateException(new IllegalStateException("route rule can not be empty. rule:" + rule));        }        ScriptEngine engine = engines.get(type);        if (engine == null){            engine = new ScriptEngineManager().getEngineByName(type);            if (engine == null) {                throw new IllegalStateException(new IllegalStateException("Unsupported route rule type: " + type + ", rule: " + rule));            }            engines.put(type, engine);        }        this.engine = engine;        this.rule = rule;    }

1)从url获取脚本类型javascript, groovy等等
2)从url根据RULE_KEY获取路由规则内容
3)根据脚本类型获取java支持的脚本执行引擎

接下来看route方法

@SuppressWarnings("unchecked")    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;        }    }

大致逻辑如下:
1)执行引擎创建参数绑定
2)绑定执行的参数
3)执行引擎编译路由规则得到执行函数CompiledScript
4)CompiledScript.eval(binds) 根据参数执行路由规则

Dubbo也支持通过FileRouterFactory从文件读取路由规则,将读取的规则设置到url的RULE_KEY参数上, 文件的后缀代表了路由的类型,选择具体的路由工厂 ConditionRouterFactory,ScriptRouterFactory来创建路由规则。

(四)LoadBalance
负载均衡, 负责从多个 Invokers中选出具体的一个Invoker用于本次调用,调用过程中包含了负载均衡的算法,调用失败后需要重新选择。

@SPI(RandomLoadBalance.NAME)public interface LoadBalance {    //Select方法设配类通过url的参数选择具体的算法, 在从invokers集合中根据具体的算法选择一个invoker    @Adaptive("loadbalance")    <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;}

类注解@SPI说明可以基于Dubbo的扩展机制进行自定义的负责均衡算法实现,默认是随机算法
方法注解@Adaptive说明能够生成设配方法

(1) RandomLoadBalance: 随机访问策略,按权重设置随机概率,是默认策略
1)获取所有invokers的个数
2)遍历所有Invokers, 获取计算每个invokers的权重,并把权重累计加起来
每相邻的两个invoker比较他们的权重是否一样,有一个不一样说明权重不均等
3)总权重大于零且权重不均等的情况下按总权重获取随机数offset = random.netx(totalWeight);遍历invokers确定随机数offset落在哪个片段(invoker上)
4)权重相同或者总权重为0, 根据invokers个数均等选择invokers.get(random.nextInt(length))

(2)RoundRobinLoadBalance:轮询,按公约后的权重设置轮询比率
1)获取轮询key 服务名+方法名,获取可供调用的invokers个数length
设置最大权重的默认值maxWeight=0,设置最小权重的默认值minWeight=Integer.MAX_VALUE
2)遍历所有Inokers,比较出得出maxWeight和minWeight
3)如果权重是不一样的,根据key获取自增序列,自增序列加一与最大权重取模默认得到currentWeigth,遍历所有invokers筛选出大于currentWeight的invokers,设置可供调用的invokers的个数length
4)自增序列加一并与length取模,从invokers获取invoker

(3)LeastActiveLoadBalance: 最少活跃调用数, 相同的活跃的随机选择,活跃数是指调用前后的计数差, 使慢的提供者收到更少的请求,因为越慢的提供者前后的计数差越大。

(4)ConsistentHashLoadBalance:一致性hash, 相同参数的请求总是发到同一个提供者,当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

原创粉丝点击