Dubbo提供了集群部署、路由、负载均衡等容错机制,在客户端引用服务时,由MockClusterInvoker封装具体的集群策略类,默认是FailoverCluster类,具体逻辑见《4.4.2 远程引用服务》部分。
由集群策略类将Directory中的多个Invoker伪装成一个Invoker,对上层调用端是透明的,不同的集群策略有不同的处理方式。主要流程基本是一致的。
1)首先调用MockClusterInvoker.invoke方法,判断是调用Mock功能还是具体的集群策略类,具体逻辑见《消费端调用》部分。后续调用具体集群策略类的invoke方法。
2)调用Directory.list获取Invoker列表;
3)调用Router.route进行路由选择;
4)LoadBalance进行负载均衡选择;
5)最后获取Invoker对象;
上图是集群服务的Invoker类的封装顺序。其中,FailoverClusterInovker是默认的集群策略类。
二、注册中心的集群
对于注册中心的Invoker代理的创建,在ReferenceConfig.createProxy方法中处理。
若在消费端配置一个注册地址的,则直接由代理工厂Protocol$Adpative根据注册地址URL中的协议选择RegistryProtocol类,然后调用该类的refer ()方法创建Invoker代理对象并封装成MockClusterInvoker对象返回;
若在消费端配置多个注册地址的,则执行以下逻辑:
1、以每个注册地址URL为参数调用RegistryProtocol. refer ()方法创建Invoker代理对象并封装成MockClusterInvoker对象,生成了Invoker代理列表;
2、选择注册地址列表中最后一个注册地址作为创建最外层MockClusterInvoker对象的URL地址,大致逻辑是:
2.1)在该URL地址中增加cluster参数以及值“available”,表示在存在多个注册地址的情况下只能使用AvailableCluster集群策略;
2.2)使用StaticDirectory对第1步中生成的Invoker代理列表进行封装,将该列表存入StaticDirectory对象的invokers:List
2.3)调用AvailableCluster类的join(Directory
三、集群策略的种类
1)AvailableCluster: 获取可用的调用。遍历所有Invokers判断Invoker.isAvalible,只要一个有为true直接调用返回,不管成功与否;
2)BroadcastCluster: 广播调用。遍历所有Invokers, 逐个调用每个调用catch住异常不影响其他invoker调用;
3)FailbackCluster: 失败自动恢复, 对于invoker调用失败, 后台记录失败请求,任务定时重发, 通常用于通知;
4)FailfastCluster: 快速失败,只发起一次调用,失败立即保错,通常用于非幂等性操作;
5)FailoverCluster: 失败转移,当出现失败,重试其它服务器,通常用于读操作,但重试会带来更长延迟;
(1)目录服务directory.list(invocation) 列出方法的所有可调用服务列表(Invoker列表),
(2)从URL中获取参数loadbalance的值,根据该参数值选择负载均衡策略类,默认为RandomLoadBalance。
(3)获取重试次数,参数retries,默认重试两次;然后遍历retries次下面的逻辑,直到成功返回或者达到重试次数。
(2.1)若不是第一次调用(即是重试),则检查服务是否被销毁以及服务引用Invoker列表是否还有记录,若被销毁或者无服务引用了则直接抛异常;
(2.2)使用负载均衡策略从Inovker列表中选择本次使用的Inovker;并将该Invoker对象存入invokered数组变量中(即记录已经使用过的Invoker对象,便于重试时不再使用);
(2.3)若成功则返回;若失败则继续重复执行2.1至2.3的逻辑;
( 4 )重试次数达到最大值则抛出调用失败异常。6)FailsafeCluster: 失败安全,出现异常时,直接忽略,通常用于写入审计日志等操作。
7)ForkingCluster: 并行调用,只要一个成功即返回,通常用于实时性要求较高的操作,但需要浪费更多服务资源。
8)MergeableCluster: 分组聚合, 按组合并返回结果,比如菜单服务,接口一样,但有多种实现,用group区分,现在消费方需从每种group中调用一次返回结果,合并结果返回,这样就可以实现聚合菜单项。
(1) 根据MERGE_KEY从url获取参数值;
(2) 为空不需要merge, 正常调用;
(3) 按group分组调用,将返回接口保存到集合中;
(4) 获取MERGE_KEY如果是默认的话,获取默认merge策略,主要根据返回类型判断;
(5) 如果不是,获取自定义的merge策略;
(6) Merge策略合并调用结果返回;在每个集群策略Cluster类中,都是通过Directory.list来查找所有的Invoker列表,注册中心的集群策略使用的是StaticDirectory,服务的集群策略使用的是RegistryDirectory;这两个类均是AbstractDirectory的子类,在获取Invoker列表均是调用该父类的list方法,然后在该方法内部调用子类的dolist方法。
StaticDirectory:静态目录服务,该类的所有Invoker是通过构造函数传入的;对于多注册地址,首先遍历所有注册地址,以每个注册地址URL为参数调用RegistryProtocol. refer ()方法创建Invoker代理对象并封装成MockClusterInvoker对象,生成了Invoker代理列表;然后使用最后一个注册地址的URL以及Invoker列表来初始化StaticDirectory对象;如下代码所示。在StaticDirectory . dolist方法中直接返回所有多注册地址生成的invoker列表。
在AbstractDirectory.list方法中,通过具体的Directory.dolist方法获取到Invoker列表之后,然后根据Router列表对Invoker进行路由选择,该Router列表是在远程引用服务时初始化的,具体逻辑见《4.4.2远程引用服务》部分。
路由规则分为条件路由规则(ConditionRouter:)和脚本路由规则(ScriptRouter:)两种:
ConditionRouter: 条件表达式以 => 分割为whenRule和thenRule
ConditionRouter创建时:
1)从url根据RULE_KEY获取路由条件路由内容
2)rule.indexOf("=>") 分割路由内容
3)分别调用parseRule(rule) 解析路由为whenRule和thenRules
ConditionRouter执行route方法时:
1)如果url不满足when条件即过来条件, 不过滤返回所有invokers;
2)遍历所有invokers判断是否满足then条件, 将满足条件的加入集合result;
3)Result不为空,有满足条件的invokers返回;
4)Result为空, 没有满足条件的invokers, 判断参数FORCE_KEY是否强制过来,如果强制过滤返回空, 不是返回所有即不过滤;
ScriptRouter:通过url的RULE_KEY参数获取脚本内容,然后通过java的脚本引擎执行脚本代码, dubbo的测试用例都是通过javascript作为脚本但是理论上也支持groovy, jruby脚本,大家可以参考下测试用例ScriptRouterTest。
ScriptRouter创建时:
1) 从url获取脚本类型javascript, groovy等等;
2) 从url根据RULE_KEY获取路由规则内容;
3) 根据脚本类型获取java支持的脚本执行引擎;
ScriptRouter执行route方法时:
1) 执行引擎创建参数绑定;
2) 绑定执行的参数;
3) 执行引擎编译路由规则得到执行函数CompiledScript;
4) CompiledScript.eval(binds) 根据参数执行路由规则;
LoadBalance负载均衡, 负责从多个 Invokers中选出具体的一个Invoker用于本次调用,调用过程中包含了负载均衡的算法,调用失败后需要重新选择。目前有RandomLoadBalance、RoundRobinLoadBalance、LeastActiveLoadBalance、ConsistentHashLoadBalance四种负载均衡策略可用。
RandomLoadBalance:随机访问策略,按权重设置随机概率,是默认策略,具体算法如下:
1)获取所有invokers的个数;
2)遍历所有Invokers, 获取计算每个invokers的权重,并把权重累计加起来;每相邻的两个invoker比较他们的权重是否一样,有一个不一样说明权重不均等;
3) 总权重大于零且权重不均等的情况下;按总权重获取随机数offset = random.netx(totalWeight); 遍历invokers确定随机数offset落在哪个片段(invoker上) ;
4) 权重相同或者总权重为0, 根据invokers个数均等选择:invokers.get(random.nextInt(length));
实现代码如下:RoundRobinLoadBalance:轮询,按公约后的权重设置轮询比率
1) 获取轮询key = 服务名+方法名;
2)获取可供调用的invokers个数length;
3)设置最大权重的默认值maxWeight=0
4)设置最小权重的默认值minWeight=Integer.MAX_VALUE
5)遍历所有Inokers,比较出得出maxWeight和minWeight
6) 如果权重是不一样的,
6.1)根据key获取自增序列,自增序列加一与最大权重取模默认得到currentWeigth ;
6.2)遍历所有invokers筛选出大于currentWeight的invokers
6.3)设置可供调用的invokers的个数length;
6.4)若length=1,则直接返回该invoker;若大于1则进入第7步;
7) 自增序列加一并与length取模,从invokers获取invoker;
实现代码如下:
LeastActiveLoadBalance:最少活跃调用数,相同的活跃的随机选择。
活跃数是指调用前后的计数差, 使慢的提供者收到更少的请求,因为越慢的提供者前后的计数差越大。
活跃计数的功能消费者是在ActiveLimitFilter中设置的,代码如下,利用RpcStatus类的静态变量来统计功能方法的活跃数、调用总次数、总耗时、成功数、失败数等信息。
最少活跃的选择过程如下:
1)获取可调用invoker的总个数,初始化最小活跃数,相同最小活跃的个数,相同最小活跃数的下标数组等等;
2) 遍历所有invokers,获取每个invoker的活跃数active和权重;找出最小权重的invoker;
如果有相同最小权重的inovkers, 将下标记录到数组leastIndexs[]数组中,
累计所有的权重到totalWeight变量;
3)如果invokers的权重不相等且totalWeight大于0;
按总权重随机offsetWeight = random.nextInt(totalWeight),计算随机值在哪个片段上并返回invoker;
4)如果invokers的权重相等或者totalWeight等于0,均等随机;
实现代码如下:ConsistentHashLoadBalance:一致性hash, 相同参数的请求总是发到同一个提供者,当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。