负载均衡在微服务层次主要解决的是服务器压力的问题,通过负载均衡算法可以将请求分摊到不同的微服务上,从而解决某台微服务因请求压力过大导致服务奔溃现象。本文就dubbo中的几种负载均衡算法进行分析,通过了解dubbo中负载均衡算法的实现,从而熟悉负载均衡算法的核心思想。
dubbo对负载均衡提供了抽象接口LoadBalance,总共有五种实现,分别是:
如果我们要在dubbo中使用自己的负载均衡算法,只需要实现该接口,重写其方法,然后再根据dubbo spi 配置好实现,在使用的时候在服务调用者端配置loadbalance属性告诉dubbo使用哪种负载均衡算法即可。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
public abstract class AbstractLoadBalance implements LoadBalance {
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
int ww = (int) (Math.round(Math.pow((uptime / (double) warmup), 2) * weight));
return ww < 1 ? 1 : (Math.min(ww, weight));
}
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
if (invokers.size() == 1) {
return invokers.get(0);
}
return doSelect(invokers, url, invocation);
}
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
int getWeight(Invoker<?> invoker, Invocation invocation) {
int weight;
URL url = invoker.getUrl();
if (invoker instanceof ClusterInvoker) {
url = ((ClusterInvoker<?>) invoker).getRegistryUrl();
}
// Multiple registry scenario, load balance among multiple registries.
if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
weight = url.getParameter(WEIGHT_KEY, DEFAULT_WEIGHT);
} else {
weight = url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
if (weight > 0) {
long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
long uptime = System.currentTimeMillis() - timestamp;
if (uptime < 0) {
return 1;
}
int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
if (uptime > 0 && uptime < warmup) {
weight = calculateWarmupWeight((int)uptime, warmup, weight);
}
}
}
}
return Math.max(weight, 0);
}
}
在分布式系统中,一致性hash算法可以做到将一部分请求固定打到指定的机器上,走到分在均衡的效果。一致性hash算法中有一个hash环的概念,在dubbo中,使用TreeMap来实现hash环,TreeMap特点是将key按照从小到大的方式进行存储。
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String methodName = RpcUtils.getMethodName(invocation);
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
// 计算整个节点集合list的hashcode,目的在于判断集合中的元素是否发生了变化。
int invokersHashCode = getCorrespondingHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
// 如果根据key取出来的selector是空的,或者hashcode已经发生了变化,就说明节点信息已经发生了变化,那么就需要重新构造hash环。
if (selector == null || selector.identityHashCode != invokersHashCode) {
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
// 选择节点
return selector.select(invocation);
}
// 构造hash环
ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
argumentIndex = new int[index.length];
for (int i = 0; i < index.length; i++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
// 循环所有的节点信息,构造hash环。
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
// replicaNumber默认为160,这里会根据每一个节点的ip生成40个虚拟节点。
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = Bytes.getMD5(address + i);
// 为每一个虚拟节点生成四个存储位置
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
// key就是当前的节点的ip计算出来的hash值,value就是当前循环的节点。
virtualInvokers.put(m, invoker);
}
}
totalRequestCount = new AtomicLong(0);
serverCount = invokers.size();
erRequestCountMap.clear();
}
// 最终调用这个方法进行选择节点
private Invoker<T> selectForKey(long hash) {
// 参数hash就是根据hash参数计算出来的下标,这里根据下标获取大于等于该hash值的最小节点信息
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
// 如果节点信息不存在,那么说明当前hash值就是hash环上最大的那么节点信息,
if (entry == null) {
// 那么接下来的那个下标就是hash环上的第一个元素了。
entry = virtualInvokers.firstEntry();
}
// 取出节点信息的地址
String serverAddress = entry.getValue().getUrl().getAddress();
// 这里设定了一个请求线程数阈值
double overloadThread = ((double) totalRequestCount.get() / (double) serverCount) * OVERLOAD_RATIO_THREAD;
// 如果当前选定的这个节点已经接收过请求并且接收的请求数超过了设定的这个线程数阈值,说明这个节点不可用
while (serverRequestCountMap.containsKey(serverAddress)
&& serverRequestCountMap.get(serverAddress).get() >= overloadThread) {
// 如果这个节点不可用,就需要获取大于给定的key的最小节点信息
entry = getNextInvokerNode(virtualInvokers, entry);
// 重新获取节点信息的地址
serverAddress = entry.getValue().getUrl().getAddress();
}
// 当前选定的这个节点没有接收过请求,那么就说明可用,将当前节点信息假如到serverRequestCountMap中
if (!serverRequestCountMap.containsKey(serverAddress)) {
serverRequestCountMap.put(serverAddress, new AtomicLong(1));
} else {
// 接收的请求数未超过设定的这个线程数阈值,那么就说明可用,将当前节点信息已经接收的请求数加1
serverRequestCountMap.get(serverAddress).incrementAndGet();
}
totalRequestCount.incrementAndGet();
return entry.getValue();
}
根据服务处理请求的频率进行选择,频率越低越符合条件。
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 记录服务数量
int length = invokers.size();
// 用来记录所有服务中,最小活跃度最低的那个服务的最小活跃数
int leastActive = -1;
// 具有相同最小活跃数的服务个数
int leastCount = 0;
// 具有最小活跃数的服务集合
int[] leastIndexes = new int[length];
// 每一个服务的权重集合
int[] weights = new int[length];
// 所有服务总权重和
int totalWeight = 0;
// 第一个最小活跃数的服务的权重,类似于选中了一个标准,之后用这个标准和每一个服务的权重进行比较,用来判断是否所有的服务的具有相同的权重。
int firstWeight = 0;
// 标志是否所有的服务的具有相同的权重,默认为true
boolean sameWeight = true;
// 循环所有的服务
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取当前服务的活跃数
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
// 获取当前服务的权重,默认为100
int afterWarmup = getWeight(invoker, invocation);
// 将当前服务的权重按照序号加入到weights数组中。
weights[i] = afterWarmup;
// 如果当前服务是第一个服务或者当前服务的活跃数比之前记录的最小活跃数还小
if (leastActive == -1 || active < leastActive) {
// 当前服务的活跃数为最小活跃数
leastActive = active;
// 具有相同最小活跃数的服务个数,也就是当前服务器的个数
leastCount = 1;
// 将当前服务按照序号加入到具有相同最小活跃数的服务集合中
leastIndexes[0] = i;
// 当前服务的权重就是权重之和
totalWeight = afterWarmup;
// 当前服务的权重就是第一个最小活跃数的服务的权重
firstWeight = afterWarmup;
// 这种情况下,所有的服务的权重都是一样的。
sameWeight = true;
// 如果当前服务的活跃数和之前记录的最小活跃数是一样的
} else if (active == leastActive) {
// 当前服务加入到具有最小活跃数的服务集合中,具有相同最小活跃数的服务个数加1
leastIndexes[leastCount++] = i;
// 总权重加上当前服务的权重
totalWeight += afterWarmup;
// 如果所有的服务的权重都是一样的,并且当前服务的权重和第一个最小活跃数的服务的权重不相等
if (sameWeight && afterWarmup != firstWeight) {
// 说明所有的服务的权重不一样的
sameWeight = false;
}
}
}
// 如果上边的所有的流程完了之后,只有一个最小活跃数的服务
if (leastCount == 1) {
// 那么就直接将这个服务返回即可,这个服务就是目前最小活跃数的服务
return invokers.get(leastIndexes[0]);
}
// 如果所有的服务的权重不一样,并且总权重之和大于0
if (!sameWeight && totalWeight > 0) {
// 随机从总权重中获取一个数
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
// 循环具有相同最小活跃数的服务集合
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
// 从权重集合中获取当前服务的权重,并用随机权重减去这个服务的权重
offsetWeight -= weights[leastIndex];
// 如果随机权重减去这个服务的权重之后小于0,那么就表明这个服务就是符合条件的服务。
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
// 如果所有服务的权重都是一样的,并且总权重为0,那么随机选取一个服务即可
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
加权随机算法是dubbo中默认的负载均衡算法,使用权重随机从服务列表中获取服务。
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取远程服务的个数
int length = invokers.size();
// 这是一个标识,标识所有的远程服务的权重是否都是一样的,后续再遍历所有的远程服务的时候,只要有一个远程服务的权重和其他的不一致,该标志就会被改为false,默认为true。
boolean sameWeight = true;
// 用来存放各个远程服务的权重
int[] weights = new int[length];
// 总权重,各个远程服务的权重之和。
int totalWeight = 0;
// 循环所有的远程服务
for (int i = 0; i < length; i++) {
// 获取当前远程服务的权重
int weight = getWeight(invokers.get(i), invocation);
// 计算所有的远程服务权重之和
totalWeight += weight;
// 保存当前远程服务的权重
weights[i] = totalWeight;
// 旧版本的是注释的这种写法,容易理解点:就是当前远程服务的权重和上一个远程服务的权重进行比较,如果不一样,就将sameWeight改为false,新版的这种写法不容易理解。
/**
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
**/
if (sameWeight && totalWeight != weight * (i + 1)) {
sameWeight = false;
}
}
// 如果总权重 > 0 并且所有的远程服务的权重都不一样
if (totalWeight > 0 && !sameWeight) {
// 随机从总权重中获取一个数字。
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 再次循环所有的远程服务集合
for (int i = 0; i < length; i++) {
// 如果随机数小于当前远程服务的权重,那么随机数刚好落在当前远程服务所占有的权重区间。
if (offset < weights[i]) {
return invokers.get(i);
}
}
}
// 如果总权重 <= 0 或者所有的远程服务的权重都一样,那么随机选取一个远程服务
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
轮询算法的思想就是每次都选取一个服务,这个服务必须和上次选择的不一致。
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// key就是接口全限定名.方法名
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
// 处理RoundRobinLoadBalance自身的缓存,key为接口全限定名.方法名,value为mao<每一个服务的标识identifyString,封装的WeightedRoundRobin>
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.computeIfAbsent(key, k -> new ConcurrentHashMap<>());
// 总权重
int totalWeight = 0;
// 设置的最大值,默认为long的最小值,为负数
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
// 选中的服务
Invoker<T> selectedInvoker = null;
// 选中服务对应的WeightedRoundRobin信息
WeightedRoundRobin selectedWRR = null;
// 开始循环所有服务
for (Invoker<T> invoker : invokers) {
// 获取当前服务的标识
String identifyString = invoker.getUrl().toIdentityString();
// 获取当前服务的权重,默认为100
int weight = getWeight(invoker, invocation);
// 处理缓存,如果存在当前服务的WeightedRoundRobin信息,就返回当前服务的WeightedRoundRobin,如果没有,就构造一个WeightedRoundRobin,并将当前服务的权重设置进去。
WeightedRoundRobin weightedRoundRobin = map.computeIfAbsent(identifyString, k -> {
WeightedRoundRobin wrr = new WeightedRoundRobin();
wrr.setWeight(weight);
return wrr;
});
// 判断下,是不是权重发生了变化,如果发生了变化,需要重新设置
if (weight != weightedRoundRobin.getWeight()) {
weightedRoundRobin.setWeight(weight);
}
// 实际上这里获取的就是当前这个服务的对应的权重,但是这个值在后续还会处理,这个值会直接影响服务能不能被选中。
long cur = weightedRoundRobin.increaseCurrent();
// 设置对应的服务信息最后更新时间为当前时间
weightedRoundRobin.setLastUpdate(now);
// 如果cur大于maxCurrent,当前服务被选中,相当于在找最大权重的那个服务
if (cur > maxCurrent) {
// 设置cur为maxCurrent
maxCurrent = cur;
// 记录选中的服务
selectedInvoker = invoker;
// 记录选中的服务的其他信息
selectedWRR = weightedRoundRobin;
}
// 记录总权重
totalWeight += weight;
}
// 处理RoundRobinLoadBalance中自身的缓存,如果缓存中的服务信息和服务列表中的信息不一致,那么就需要从缓存中将超过1分钟还没有被更新的服务移除掉。
if (invokers.size() != map.size()) {
map.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
}
// 如果选中的服务不为空
if (selectedInvoker != null) {
// 这里会将当前选中的服务信息的cur设置成一个负数,也就是设置成所有权重的之和的负数,那么这个服务的cur就是最小的一个了,目的是为了保证下一次不会再次选中该服务
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
// 如果上述流程之后,还未选中服务,那么直接就返回第一个服务。
return invokers.get(0);
}
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 获取远程服务的个数
int length = invokers.size();
// 最大的响应时间,默认为long的最大值
long shortestResponse = Long.MAX_VALUE;
// 具有相同最短响应时间的服务的个数
int shortestCount = 0;
// 具有相同最短响应时间的服务的集合
int[] shortestIndexes = new int[length];
// 每一个服务的权重集合
int[] weights = new int[length];
// 所有服务总权重和
int totalWeight = 0;
// 第一个最短响应时间的服务的权重,类似于选中了一个标准,之后用这个标准和每一个服务的权重进行比较,用来判断是否所有的服务的具有相同的权重。
int firstWeight = 0;
// 标志是否所有的服务的具有相同的权重,默认为true
boolean sameWeight = true;
// 循环所有的服务
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
// 获取当前服务的rpc信息
RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
// 获取当前服务成功响应所花费的平均响应时间
long succeededAverageElapsed = rpcStatus.getSucceededAverageElapsed();
// 获取当前服务的活跃数,加1的目的就是算上本次请求。
int active = rpcStatus.getActive() + 1;
// 计算处理完这些请求需要花费的总体响应时间
long estimateResponse = succeededAverageElapsed * active;
// 获取当前服务的权重
int afterWarmup = getWeight(invoker, invocation);
// 按照序号记录权重
weights[i] = afterWarmup;
// 如果当前服务处理完这些请求需要花费的总体响应时间小于设置最大的响应时间,这里实际上就是在找最小响应时间的服务。
if (estimateResponse < shortestResponse) {
// 设置最大的响应时间为当前服务的总体响应时间
shortestResponse = estimateResponse;
// 最短响应时间的数量为1,也就是本服务一个
shortestCount = 1;
// 记录最短响应时间的这个服务
shortestIndexes[0] = i;
// 总权重就是当前这个服务的权重
totalWeight = afterWarmup;
// 当前服务的权重就是第一个最短响应时间的服务的权重
firstWeight = afterWarmup;
// 这种情况下,所有的服务的权重都是一样的。
sameWeight = true;
// 如果当前服务的最短响应时间和之前记录的最短响应时间是一样的
} else if (estimateResponse == shortestResponse) {
// 当前服务加入到具有最短响应时间的服务集合中,具有相同最短响应时间的服务个数加1
shortestIndexes[shortestCount++] = i;
// 总权重加上当前服务的权重
totalWeight += afterWarmup;
// 如果所有的服务的权重都是一样的,并且当前服务不是列表中第一个服务,并且当前服务的权重和第一个最短响应时间的服务的权重不相等
if (sameWeight && i > 0
&& afterWarmup != firstWeight) {
// 说明所有的服务的权重不一样的
sameWeight = false;
}
}
}
// 如果上边的所有的流程完了之后,只有一个最短响应时间的服务
if (shortestCount == 1) {
// 那么就直接将这个服务返回即可,这个服务就是目前最短响应时间的服务
return invokers.get(shortestIndexes[0]);
}
// 如果所有的服务的权重不一样,并且总权重之和大于0
if (!sameWeight && totalWeight > 0) {
// 随机从总权重中获取一个数
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
// 循环具有相同最短响应时间的服务集合
for (int i = 0; i < shortestCount; i++) {
int shortestIndex = shortestIndexes[i];
// 从权重集合中获取当前服务的权重,并用随机权重减去这个服务的权重
offsetWeight -= weights[shortestIndex];
// 如果随机权重减去这个服务的权重之后小于0,那么就表明这个服务就是符合条件的服务。
if (offsetWeight < 0) {
return invokers.get(shortestIndex);
}
}
}
// 如果所有服务的权重都是一样的,并且总权重为0,那么随机选取一个服务即可
return invokers.get(shortestIndexes[ThreadLocalRandom.current().nextInt(shortestCount)]);
}