dubbo提供了5种负载均衡策略,分别是:
- RandomLoadBalance --加权随机
- LeastActiveLoadBalance --最小活跃数
- ShortestResponseLoadBalance --最短响应时间
- RoundRobinLoadBalance --加权轮询
- ConsistentHashLoadBalance --一致性hash
首先我们知道在集群ClusterInvoker选择Invoker时调用到LoadBanlance的select方法,我们先看一下:
@Override
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
// 如果 invokers 列表中仅有一个 Invoker,直接返回即可,无需进行负载均衡
if (invokers.size() == 1) {
return invokers.get(0);
}
// 调用 doSelect 方法进行负载均衡,该方法为抽象方法,由子类实现
return doSelect(invokers, url, invocation);
}
这个是AbstractLoadBalance的实现,如果备选列表中只有一个直接取出来返回,否则调用模板方法doSelect,由子类实现。下面会详细分析每个子类的doSelect方法。
AbstractLoadBalance 除了实现了 LoadBalance 接口方法,还封装了一些公共逻辑,比如服务提供者权重计算逻辑。
int getWeight(Invoker<?> invoker, Invocation invocation) {
int weight;
URL url = invoker.getUrl();
// Multiple registry scenario, load balance among multiple registries.
if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEY, DEFAULT_WEIGHT);
} else {
// 从 url 中获取权重 weight 配置值
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;
}
// 获取服务预热时间,默认为10分钟
int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
// 如果服务运行时间小于预热时间,则重新计算服务权重,即降权
if (uptime > 0 && uptime < warmup) {
// 重新计算服务权重。 (启动时间/预热时间)*权重 启动时间越长,前面比值越接近1
// 当启动时间小于预热时间时,实际上是对权重做降权处理。
weight = calculateWarmupWeight((int)uptime, warmup, weight);
}
}
}
}
return Math.max(weight, 0);
}
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
// 计算权重,下面代码逻辑上形似于 (uptime / warmup) * weight。
// 随着服务运行时间 uptime 增大,权重计算值 ww 会慢慢接近配置值 weight
int ww = (int) ( uptime / ((float) warmup / weight));
return ww < 1 ? 1 : (Math.min(ww, weight));
}
我们看到,计算权重时并不是直接取的用户定义的权重值,而是根据启动时间慢慢接近到配置的权重值。这个时间默认是10分钟。这样做的好处是对于刚启动的服务可以有效防止瞬时流量过大。起到服务预热的作用。
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次。
很巧妙的一种方法。
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// Number of invokers
// invokers的数量
int length = invokers.size();
// Every invoker has the same weight?
// 是否具有相同的权重
boolean sameWeight = true;
// the weight of every invokers
// 各个invokers的权重集合
int[] weights = new int[length];
// the first invoker's weight
// 第一个invoker的权重
int firstWeight = getWeight(invokers.get(0), invocation);
// 第一个赋值
weights[0] = firstWeight;
// The sum of weights
// 总权重
int totalWeight = firstWeight;
// 下面这个循环有两个作用,第一是计算总权重 totalWeight,
// 第二是检测每个服务提供者的权重是否相同
for (int i = 1; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
// save for later use
// 第N个权重赋值
weights[i] = weight;
// Sum
// 累加权重
totalWeight += weight;
// 检测当前服务提供者的权重与第一个服务提供者的权重是否相同,
// 不相同的话,则将 sameWeight 置为 false。
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],