欢迎访问我的个人博客休息的风
dubbo集群模块主要实现了集群容错和负载均衡功能。官网上有对此模块功能进行一个概述如下:
- 这里的 Invoker 是 Provider 的一个可调用 Service 的抽象,Invoker 封装了 Provider 地址及 Service 接口信息
- Directory 代表多个 Invoker,可以把它看成 List<Invoker> ,但与 List 不同的是,它的值可能是动态变化的,比如注册中心推送变更
- Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个
- Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离,应用隔离等
- LoadBalance 负责从多个 Invoker 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选
先来看下整体的类图情况如下图(看不清可在新标签页中查看)
主要关注实现Cluster、LoadBalance、Invoker这三个接口的图。这里首先介绍下框架有的集群容错模式及负载均衡策略。
集群容错的模式有以下六种:
- Failover Cluster(默认模式)
失败自动切换,当出现失败,重试其它服务器 1。通常用于读操作,但重试会带来更长延迟
- Failfast Cluster
快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
- Failsafe Cluster
失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
- Failback Cluster
失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
- Forking Cluster
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源
- Broadcast Cluster
广播调用所有提供者,逐个调用,任意一台报错则报错 2。通常用于通知所有提供者更新缓存或日志等本地资源信息。
负载均衡的策略有以下四种:
- Random LoadBalance(默认模式)
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
- RoundRobin LoadBalance
轮循,按公约后的权重设置轮循比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
- LeastActive LoadBalance
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
- ConsistentHash LoadBalance
一致性 Hash,相同参数的请求总是发到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
本文将从Failover Cluster集群容错模式和Random LoadBalance负载均衡策略这两个默认的配置入手,分析源码,了解其整个实现过程。
这一调用的入口在RegistryProtocol.doRefer方法中,会去执行“cluster.join(directory)”这一代码。根据SPI机制获取默认的Failover Cluster模式,在这个模式里,同样也会默认采用Random LoadBalance策略。具体我们来看下源码。
public <T> Invoker<T> join(Directory<T> directory) throws RpcException { return new FailoverClusterInvoker<T>(directory); }FailoverCluster类中会去创建一个Invoker,具体的调用过程将在这个类的doInvoke中处理。过程定义在AbstractClusterInvoker父类中。
public Result invoke(final Invocation invocation) throws RpcException { checkWhetherDestroyed(); LoadBalance loadbalance; //RegistryDirectory处理通知响应(notify)时,将注册中心最新的列表接取下来,存放在静态变量缓存中 //这里的invokers列表,调用directory.list(invocation),从RegistryDirectory的methodInvokerMap中获取的 List<Invoker<T>> invokers = list(invocation); //通过SPI机制获取loadbalance if (invokers != null && invokers.size() > 0) { loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl() .getMethodParameter(invocation.getMethodName(), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE)); } else { loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE); } RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation); //再到子类中具体处理调用过程 return doInvoke(invocation, invokers, loadbalance); }具体子类的调用过程,这里是FailoverClusterInvoker:
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException { List<Invoker<T>> copyinvokers = invokers; checkInvokers(copyinvokers, invocation); int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1; if (len <= 0) { len = 1; } // retry loop. RpcException le = null; // last exception. List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers. Set<String> providers = new HashSet<String>(len); for (int i = 0; i < len; i++) { //重试时,进行重新选择,避免重试时invoker列表已发生变化. //注意:如果列表发生了变化,那么invoked判断会失效,因为invoker示例已经改变 if (i > 0) { checkWhetherDestroyed(); copyinvokers = list(invocation); //重新检查一下 checkInvokers(copyinvokers, invocation); } //使用loadbalance负载均衡选择哪个服务invoker去调用 Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked); invoked.add(invoker); RpcContext.getContext().setInvokers((List) invoked); try { //调用执行调用链 Result result = invoker.invoke(invocation); //省略一些代码。。。。。 }这个调用处理如果失败,默认会重试三次。AbstractClusterInvoker父类中对于选择哪个invoker定义的过程:
private Invoker<T> doselect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException { if (invokers == null || invokers.size() == 0) return null; if (invokers.size() == 1) return invokers.get(0); // 如果只有两个invoker,退化成轮循 if (invokers.size() == 2 && selected != null && selected.size() > 0) { return selected.get(0) == invokers.get(0) ? invokers.get(1) : invokers.get(0); } //真正调用负载策略处理的入口 Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation); //如果 selected中包含(优先判断) 或者 不可用&&availablecheck=true 则重试. if ((selected != null && selected.contains(invoker)) || (!invoker.isAvailable() && getUrl() != null && availablecheck)) { try { Invoker<T> rinvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck); if (rinvoker != null) { invoker = rinvoker; } else { //看下第一次选的位置,如果不是最后,选+1位置. int index = invokers.indexOf(invoker); try { //最后在避免碰撞 invoker = index < invokers.size() - 1 ? invokers.get(index + 1) : invoker; //省略代码 。。。。 } return invoker; }如果只有两个invoker,会退化成轮循;否则从具体负载均衡选择invoker,选择后还需进行是否可用的判断,如果不可用再重新选择。在RandomLoadBalance处理具体选择哪个invoker如下:
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); // 总个数 int totalWeight = 0; // 总权重 boolean sameWeight = true; // 权重是否都一样 for (int i = 0; i < length; i++) { int weight = getWeight(invokers.get(i), invocation); totalWeight += weight; // 累计总权重 if (sameWeight && i > 0 && weight != getWeight(invokers.get(i - 1), invocation)) { sameWeight = false; // 计算所有权重是否一样 } } if (totalWeight > 0 && !sameWeight) { // 如果权重不相同且权重大于0则按总权重数随机 int offset = random.nextInt(totalWeight); // 并确定随机值落在哪个片断上 for (int i = 0; i < length; i++) { offset -= getWeight(invokers.get(i), invocation); if (offset < 0) { return invokers.get(i); } } } // 如果权重相同或权重为0则均等随机 return invokers.get(random.nextInt(length)); }选完后如果这个invoker不可用,则需要得选,重选的代码在AbstractClusterInvoker父类中:
private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck) throws RpcException { //预先分配一个,这个列表是一定会用到的. List<Invoker<T>> reselectInvokers = new ArrayList<Invoker<T>>(invokers.size() > 1 ? (invokers.size() - 1) : invokers.size()); //先从非select中选 if (availablecheck) { //选isAvailable 的非select for (Invoker<T> invoker : invokers) { if (invoker.isAvailable()) { if (selected == null || !selected.contains(invoker)) { reselectInvokers.add(invoker); } } } if (reselectInvokers.size() > 0) { return loadbalance.select(reselectInvokers, getUrl(), invocation); } } else { //选全部非select for (Invoker<T> invoker : invokers) { if (selected == null || !selected.contains(invoker)) { reselectInvokers.add(invoker); } } if (reselectInvokers.size() > 0) { return loadbalance.select(reselectInvokers, getUrl(), invocation); } } //最后从select中选可用的. { if (selected != null) { for (Invoker<T> invoker : selected) { if ((invoker.isAvailable()) //优先选available && !reselectInvokers.contains(invoker)) { reselectInvokers.add(invoker); } } } if (reselectInvokers.size() > 0) { return loadbalance.select(reselectInvokers, getUrl(), invocation); } } return null; }至此,总结一下这个过程。首先从RegistryProtocol.doRefer的cluster.join为入口,开始进行集群容错。默认的容错模式为FailoverCluster。调用过程如果不成功,则重试三次;从RegistryDirectory中获取注册中心的invoker等信息,之后选择默认的负载均衡策略RandomLoadBalance进行invoker的选择。选择之后还需判断invoker是否可用,不可用还需重选。这样,一个大概的容错和负载均衡的过程就完成了。