前言
当我们的Dubbo应用出现多个服务提供者时,服务消费者如何选择哪一个来调用呢?这就涉及到负载均衡算法。
LoadBalance 中文意思为负载均衡,它的职责是将网络请求,或者其他形式的负载“均摊”到不同的机器上。避免集群中部分服务器压力过大,而另一些服务器比较空闲的情况。通过负载均衡,可以让每台服务器获取到适合自己处理能力的负载。在为高负载服务器分流的同时,还可以避免资源浪费,一举两得。
Dubbo中提供了4种负载均衡实现:
-
基于权重随机算法的 RandomLoadBalance
-
基于最少活跃调用数算法的 LeastActiveLoadBalance
-
基于 hash 一致性的 ConsistentHashLoadBalance
-
基于加权轮询算法的 RoundRobinLoadBalance
一、LoadBalance
在Dubbo中,所有的负载均衡实现类都继承自抽象类AbstractLoadBalance
,该类实现LoadBalance
接口。
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
/**
* select one invoker in list.
*
* @param invokers invokers.
* @param url refer url
* @param invocation invocation.
* @return selected invoker.
*/
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
可以看到,该接口的SPI注解指定了默认的实现RandomLoadBalance
,不过不着急,我们先看看抽象类的逻辑。
1、选择服务
我们先来看负载均衡的入口方法 select,它逻辑比较简单。校验服务提供者是否为空;如果 invokers 列表中仅有一个 Invoker,直接返回即可,无需进行负载均衡;有多个Invoker就调用子类实现进行负载均衡。
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (invokers == null || invokers.isEmpty())
return null;
//如果只有一个服务提供者,直接返回,无需负载均衡
if (invokers.size() == 1)
return invokers.get(0);
return doSelect(invokers, url, invocation);
}
2、获取权重
这里包含两个逻辑,一个是获取配置的权重值,默认为100;另一个是根据服务运行时长重新计算权重。
protected int getWeight(Invoker<?> invoker, Invocation invocation) {
//获取权重值,默认为100
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight",100);
if (weight > 0) {
//服务提供者启动时间戳
long timestamp = invoker.getUrl().getParameter("remote.timestamp", 0L);
if (timestamp > 0L) {
//当前时间-启动时间=运行时长
int uptime = (int) (System.currentTimeMillis() - timestamp);
//获取服务预热时间 默认10分钟
int warmup = invoker.getUrl().getParameter("warmup", 600000 );
//如果服务运行时间小于预热时间,即服务启动未到达10分钟
if (uptime > 0 && uptime < warmup) {
//重新计算服务权重
weight = calculateWarmupWeight(uptime, warmup, weight);
}
}
}
return weight;
}
如上代码,获取服务权重值。然后判断服务启动时长是否小于服务预热时间,然后重新计算权重。服务预热时间默认是10分钟。大致流程如下:
- 获取配置的权重值,默认为100
- 获取服务启动的时间戳
- 当前时间 - 服务启动时间 = 服务运行时长
- 获取服务预热时间,默认为10分钟
- 判断服务运行时长是否小于预热时间,条件成立则重新计算权重
重新计算权重其实就是降权的过程。
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
return ww < 1 ? 1 : (ww > weight ? weight : ww);
}
代码看起来很简单,但却不大好理解。我们可以把上面的代码换成下面的公式来看:uptime / warmup) * weight
,即进度百分比*权重。
假设我们把权重设置为100,预热时间为10分钟。那么:
运行时长 | 公式 | 计算后权重 |
---|---|---|
1分钟 | 1/10 * 100 | 10 |
2分钟 | 2/10 * 100 | 20 |
5分钟 | 5/10 * 100 | 50 |
10分钟 | 10/10 * 100 | 100 |
由此可见,在未达到服务预热时间之前,权重都被降级了。Dubbo为什么要这样做呢?
主要用于保证当服务运行时长小于服务预热时间时,对服务进行降权,避免让服务在启动之初就处于高负载状态。服务预热是一个优化手段,与此类似的还有 JVM 预热。主要目的是让服务启动后“低功率”运行一段时间,使其效率慢慢提升至最佳状态。
二、权重随机算法
RandomLoadBalance 是加权随机算法的具体实现,也是Dubbo中负载均衡算法默认的实现。这里我们需要先把服务器按照权重进行分区,比如:
假设有三台服务器:【A、B、C】
它们对应的权重为:【1、3、6】,总权重为10
那么,我们可以得出:
区间 | 所属服务器 |
---|---|
0-1 | A |
1-4 | B |
4-10 | C |
剩下的就简单了,我们获取总权重totalWeight,然后生成[0-totalWeight]之间的随机数,计算随机数会落在哪个区间就好了。
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-totalWeight]之间的随机数
int offset = random.nextInt(totalWeight);
//计算随机数处于哪个区间,返回对应invoker
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
//如果权重相同,随机返回
return invokers.get(random.nextInt(length));
}
我们以上面的例子,总结一下上面代码的流程:
- 获取服务提供者数量 = 3
- 累加,计算总权重 = 10
- 校验服务权重是否相等,不相等。依次为1、3、6
- 获取0 - 10直接的随机数,假设 offset = 6
- 第1次循环,6-=1>0,条件不成立,offset = 5
- 第2次循环,5-=3>0,条件不成立,offset = 2
- 第3次循环,2-=6<0,条件成立,返回第3组服务器
最后,如果权重都相同,直接随机返回一个服务Invoker。
三、最小活跃数算法
最小活跃数负载均衡算法对应LeastActiveLoadBalance。活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求,此时应优先将请求分配给该服务提供者。
Dubbo会为每个服务提供者Invoker分配一个active,代表活跃数大小。调用之前做自增操作,调用完成后做自减操作。这样有的服务处理的快,有的处理的慢。越快的,active数量越小,就优先分配。
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
//服务提供者列表数量
int length = invokers.size();
//默认的最小活跃数值
int leastActive = -1;
//最小活跃数invoker数量
int leastCount = 0;
//最小活跃数invoker索引
int[] leastIndexs = new int[length];
//总权重
int totalWeight = 0;
//第一个Invoker权重值 用于比较invoker直接的权重是否相同
int firstWeight = 0;
boolean sameWeight = true;
//循环比对Invoker的活跃数大小
for (int i = 0; i < length; i++) {
//获取当前Invoker对象
Invoker<T> invoker = invokers.get(i);
//获取活跃数大小
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
//获取权重值
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100);
//对比发现更小的活跃数,重置
if (leastActive == -1 || active < leastActive) {
//更新最小活跃数
leastActive = active;
//更新最小活跃数 数量为1
leastCount = 1;
//记录坐标
leastIndexs[0] = i;
totalWeight = weight;
firstWeight = weight;
sameWeight = true;
//如果当前Invoker的活跃数 与 最小活跃数相等
} else if (active == leastActive) {
leastIndexs[leastCount++] = i;
totalWeight += weight;
if (sameWeight && i > 0
&& weight != firstWeight) {
sameWeight = false;
}
}
}
//如果只有一个Invoker具有最小活跃数,直接返回即可
if (leastCount == 1) {
return invokers.get(leastIndexs[0]);
}
//多个Invoker具体相同的最小活跃数,但权重不同,就走权重的逻辑
if (!sameWeight && totalWeight > 0) {
int offsetWeight = random.nextInt(totalWeight);
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers