# Dubbo源码解析:详解集群容错和负载均衡策略

# 介绍
在服务引入的过程中,Cluster会把多个Invoker合并,只暴露出一个Invoker让调用方使用
// RegistryProtocol#doRefer
Invoker invoker = cluster.join(directory);
根据配置的集群容错的策略,会返回不同的Cluster,而每个Cluster的逻辑也很简单。用不用的包装类对Invoker进行包装,集群容错的逻辑在各个包装的Invoker中
@SPI(FailoverCluster.NAME)
public interface Cluster {
@Adaptive
<T> Invoker<T> join(Directory<T> directory) throws RpcException;
}
public class FailoverCluster implements Cluster {
public final static String NAME = "failover";
@Override
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new FailoverClusterInvoker<T>(directory);
}
}
FailoverCluster#join方法只是简单的返回了一个FailoverClusterInvoker,其他集群容错的策略和这个一样,都是返回对应的Invoker
前面我们说过,集群容错的过程就是从一堆Invoker中,根据负载均衡策略选出最终发起调用的Invoker,然后发起调用。所以我们今天看一下集群容错策略和负载均衡策略有哪些?
# 集群容错策略
| 实现类 | 解释 |
|---|---|
| AvailableCluster | 找到一个可用的节点,直接发起调用 |
| FailoverCluster | 失败重试(默认) |
| FailfastCluster | 快速失败 |
| FailsafeCluster | 安全失败 |
| FailbackCluster | 失败自动恢复 |
| ForkingCluster | 并行调用 |
| BroadcastCluster | 广播调用 |
Failover Cluster:失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。
Failfast Cluster:快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。
Failsafe Cluster:失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。
Failback Cluster:失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。
Forking Cluster:并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks=”2″ 来设置最大并行数。
Broadcast Cluster:广播调用所有提供者,逐个调用,任意一台报错则报错 。通常用于通知所有提供者更新缓存或日志等本地资源信息。
读操作建议使用 Failover 失败自动切换,默认重试两次其他服务器。写操作建议使用 Failfast 快速失败,发一次调用失败就立即报错。 
# FailoverClusterInvoker实现
public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyInvokers = invokers;
checkInvokers(copyInvokers, invocation);
String methodName = RpcUtils.getMethodName(invocation);
// 获取重试次数
int len = getUrl().getMethodParameter(methodName, 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++) {
//Reselect before retry to avoid a change of candidate `invokers`.
//NOTE: if `invokers` changed, then `invoked` also lose accuracy.
if (i > 0) {
// 当前实例已经被销毁,则抛出异常
checkWhetherDestroyed();
// 重新获取服务提供者
copyInvokers = list(invocation);
// check again
// 重新检查一下
checkInvokers(copyInvokers, invocation);
}
// 通过负载均衡选择 Invoker,已经调用过的不会再选择
Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List) invoked);
try {
// 发起远程调用
Result result = invoker.invoke(invocation);
if (le != null && logger.isWarnEnabled()) {
logger.warn("Although retry the method " + methodName
+ " in the service " + getInterface().getName()
+ " was successful by the provider " + invoker.getUrl().getAddress()
+ ", but there have been failed providers " + providers
+ " (" + providers.size() + "/" + copyInvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost()
+ " using the dubbo version " + Version.getVersion() + ". Last error is: "
+ le.getMessage(), le);
}
return result;
} catch (RpcException e) {
// 业务类的异常直接跑出来
if (e.isBiz()) { // biz exception.
throw e;
}
le = e;
} catch (Throwable e) {
le = new RpcException(e.getMessage(), e);
} finally {
providers.add(invoker.getUrl().getAddress());
}
}
throw new RpcException(le.getCode(), "Failed to invoke the method "
+ methodName + " in the service " + getInterface().getName()
+ ". Tried " + len + " times of the providers " + providers
+ " (" + providers.size() + "/" + copyInvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version "
+ Version.getVersion() + ". Last error is: "
+ le.getMessage(), le.getCause() != null ? le.getCause() : le);
}
}
执行调用的时候,doInvoke方法会执行,其中invokers(路由过后的Invokers),loadbalance(负载均衡策略)都是由AbstractClusterInvoker根据配置决定的,doInvoke调用成功则直接返回,否则遍历invokers列表继续遍历,直到超过重试次数。如果此时还没有调用成功,则会抛出RpcException
#
# LoadBalance(负载均衡)
经过路由规则过滤后的Invoker,如果只有一个,就可以直接发起调用了。如果有多个,此时就涉及到负载均衡策略了,Dubbo提供了如下策略,如果不能满足你的需求,你可以自定义LoadBalance接口的实现
| 实现类 | 解释 |
|---|---|
| RandomLoadBalance | 随机策略(默认) |
| RoundRobinLoadBalance | 轮询策略 |
| LeastActiveLoadBalance | 最少活跃调用数 |
| ConsistentHashLoadBalance | 一致性hash策略 |
AbstractLoadBalance主要提供了一个方法getWeight,根据服务的启动时间,给服务定一个权重。
我们来分析一下RandomLoadBalance
思路如下:
RandomLoadBalance 是加权随机算法的具体实现,它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字3会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。比如,经过一万次选择后,服务器 A 被选中的次数大约为5000次,服务器 B 被选中的次数约为3000次,服务器 C 被选中的次数约为2000次。
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// Number of invokers
int length = invokers.size();
// Every invoker has the same weight?
boolean sameWeight = true;
// the weight of every invokers
int[] weights = new int[length];
// the first invoker's weight
// 获取第一个服务的权重
int firstWeight = getWeight(invokers.get(0), invocation);
weights[0] = firstWeight;
// The sum of weights
int totalWeight = firstWeight;
// 下面这个循环有两个作用
// 1. 计算总权重
// 2. 检测所有服务的权重是否相同
for (int i = 1; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
// save for later use
weights[i] = weight;
// Sum
// 类加权重
totalWeight += weight;
if (sameWeight && weight != firstWeight) {
sameWeight = false;
}
}
// 下面的 if 分支主要用于获取随机数,并计算随机数落在哪个区间上
if (totalWeight > 0 && !sameWeight) {
// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
// 随机获取一个 [0, totalWeight) 区间内的数字
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// Return a invoker based on the random value.
// 循环让 offset 数减去服务提供者权重值,当 offset 小于0时,返回相应的 Invoker。
// 举例说明一下,我们有 servers = [A, B, C],weights = [5, 3, 2],offset = 7。
// 第一次循环,offset - 5 = 2 > 0,即 offset > 5,
// 表明其不会落在服务器 A 对应的区间上。
// 第二次循环,offset - 3 = -1 < 0,即 5 < offset < 8,
// 表明其会落在服务器 B 对应的区间上
for (int i = 0; i < length; i++) {
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果所有服务提供者权重值相同,此时直接随机返回一个即可
// If all invokers have the same weight value or totalWeight=0, return evenly.
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
}
其他的你可以看一下官网的分析,写的很清楚