Dubbo进阶(十二):负载均衡

首先我们依然再看一遍Dubbo调用的流程

在这里插入图片描述

本文主要讲解负载均衡的相关知识。

Dubbo中实现了五种负载均衡的算法,先看一下UML图。

在这里插入图片描述

接下来我们可以通过下面这个表格大概了解一下每种负载均衡的特点。

算法名称特点说明
RandomLoadBalance(加权随机算法)随机,按照权重设置随机概率。在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按照概率使用权重后也比较均匀,有利于动态调整提供者的权重。
RoundRobinLoadBalance(权重轮询算法)轮循,按照公约的权重设置轮询比例。存在慢的提供者累计请求的问题,比如:第二台机器很慢,但是没有挂掉,当请求到第二台机器上的时候就会卡在那里,时间久了就会出现请求都卡在第二台机器上的问题。
LeastActiveLoadBalance(最少活跃调用数算法)最少活跃调用数,如果活跃数相同则随机相同,活跃数指调用前后计数差。使慢的提供者收到更少的请求,因为越慢的提供者的调用前后计数差越大。
ConsistentHashLoadBalance(一致性Hash算法)一致性Hash,让相同参数的请求总是发到同一 Provider。 当某一台 Provider 崩溃时,原本发往该 Provider 的请求,基于虚拟节点,平摊到其它 Provider,不会引起剧烈变动。缺省只对第一个参数 Hash,如果要修改,请配置 <dubbo:parameter key="hash.arguments" value="0,1" />。缺省用 160 份虚拟节点,如果要修改,请配置 <dubbo:parameter key="hash.nodes" value="320" />
ShortestResponseLoadBalance(最短响应时间算法)监控服务的响应时间,并根据响应时间排序,选择响应时间最短的服务器。

如何在项目中配置负载均衡策略

  • 服务端服务级别

<dubbo:service loadbalance="roundrobin" />

  • 客户端服务级别

<dubbo:reference loadbalance="roundrobin" />

  • 服务端方法级别

<dubbo:service>

<dubbo:method name="..." loadbalance="roundrobin"/>

</dubbo:service>

  • 客户端方法级别

<dubbo:reference>

<dubbo:method name="..." loadbalance="roundrobin"/>

</dubbo:reference>

源码分析

  • 首先看一下org.apache.dubbo.rpc.cluster.LoadBalance这个接口,通过源码可以Dubbo默认的负载均衡策略是加权随机算法RandomLoadBalance

@SPI(RandomLoadBalance.NAME)

public interface LoadBalance {

/**

* select one invoker in list.

*

* @param invokers invokers.

* @param url refer url

* @param invocation invocation.

* @return selected invoker.

*/

// 根据负载均衡算法获取将要调用的Invoker

@Adaptive("loadbalance")

<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}

  • 然后再看一下org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance这个抽象类,可以看出这是一个模板方法,父类完成了一些通用逻辑的编写,具体的算法会在对应的子类进行实现。

@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);

}

returndoSelect(invokers, url, invocation);

}

protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);

接下来我们分析一下每个负载均衡算法具体实现逻辑。

RandomLoadBalance

这个算法是Dubbo负载均衡的默认值,还是需要好好了解一下。首先我们先了解一下这个算法的思路,然后在通过源码分析一下具体的实现逻辑。Dubbo中,可以对 Provider 设置权重。比如机器性能好的,可以设置大一点的权重,性能差的,可以设置小一点的权重。权重会对负载均衡产生影响。可以在Dubbo Admin中对 Provider 进行权重的设置。

  • 加权随机算法的思路

比如我们有四台服务器,分别是A,B,C,D服务器。对应的权重分别是10,20,30,40。现在把这些权重值平铺在一维坐标值上,如下图所示:

+-----------------------------------------------------------------------------------+

| | | | |

+-----------------------------------------------------------------------------------+

1 10 30 60 100

|-----A----|-------B-------|-----------C-----------|---------------D--------------|

-------------------15

---------------------------------37

--------------------------------------------------------------74

上面的图中一共有4块区域,长度分别是A,B,C和D的权重。然后从100个数中随机选择一个。然后再判断该数分布在哪个区域。比如,如果随机到37,37是分布在C区域的,那么就选择 Invoker C。15是在B区域,74是在D区域。

  • 加权随机算法的源码

public class RandomLoadBalance extends AbstractLoadBalance {

// @SPI用

public static final String NAME = "random";

@Override

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

// invoker的数量

int length = invokers.size();

// 每个invoker的权重相同

boolean sameWeight = true;

// 每个invoker的权重的数组

int[] weights = new int[length];

// 第一个invoker的权重

int firstWeight = getWeight(invokers.get(0), invocation);

weights[0] = firstWeight;

// 所有invoker的权重之和

int totalWeight = firstWeight;

for (int i = 1; i < length; i++) {

int weight = getWeight(invokers.get(i), invocation);

// 保存以待后用

weights[i] = weight;

// 计算所有invoker的权重之和

totalWeight += weight;

if (sameWeight && weight != firstWeight) {

sameWeight = false;

}

}

// 如果并非所有invoker都具有相同的权重,并且至少有一个invoker的权重大于0,根据totalWeight进行选择

if (totalWeight > 0 && !sameWeight) {

// 根据总权重随机出一个偏移量

int offset = ThreadLocalRandom.current().nextInt(totalWeight);

// 根据随机值返回invoker

for (int i = 0; i < length; i++) {

offset -= weights[i];

if (offset < 0) {

return invokers.get(i);

}

}

}

// 如果所有invoker都具有相同的权重值或totalWeight = 0,则平均返回。

return invokers.get(ThreadLocalRandom.current().nextInt(length));

}

}

通过源码可以总结如下:

①计算所有invoker权重的总和并判断每个invoker的权重是否一样。主要通过遍历invoker list,计算所有invoker的权重总和。在遍历过程中会对比每个invoker的权重,判断每个invoker的权重是否一样。

②如果权重不同,在这个总权重范围内随机生成一个数字,用这个数字依次减去每个invoker的权重,当值变为<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 对应的区间上。

③如果权重相同,则随机选择一个invoker返回。

RoundRobinLoadBalance

权重轮询负载均衡算法会根据设置的权重来判断轮循的比例。权重轮循分为普通权重轮询和平滑权重轮询。普通权重轮询会造成某个节点会被频繁选中,容易导致一个节点的流量在短时间内增加很多。平滑权重轮询会在轮询时会穿插选择其他的节点,让整个服务器的选择过程比较均匀,不会导致一个节点的流量暴增。

  • 轮询的定义:

举个例子,我们有三台服务器 A、B、C。我们将第一个请求分配给服务器 A,第二个请求分配给服务器 B,第三个请求分配给服务器 C,第四个请求再次分配给服务器 A。这个过程就叫做轮询。轮询是一种无状态负载均衡算法,实现简单,适用于每台服务器性能相近的场景下。

  • 加权轮询的定义:

但现实情况下,我们并不能保证每台服务器性能均相近。如果我们将等量的请求分配给性能较差的服务器,这显然是不合理的。因此,这个时候我们需要对轮询过程进行加权,以调控每台服务器的负载。经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在8次请求中,服务器 A 将收到其中的5次请求,服务器 B 会收到其中的2次请求,服务器 C 则收到其中的1次请求。

  • 平滑加权轮询算法的源码

public class RoundRobinLoadBalance extends AbstractLoadBalance {

public static final String NAME = "roundrobin";

private static final int RECYCLE_PERIOD = 60000;

protected static class WeightedRoundRobin {

// invoker的权重值

private int weight;

// 考虑到并发场景下某个invoker会被同时选中,所以计算被所有线程选中的权重总和

private AtomicLong current = new AtomicLong(0);

// 最后一次更新时间

private long lastUpdate;

public int getWeight() {

return weight;

}

public void setWeight(int weight) {

this.weight = weight;

current.set(0);

}

public long increaseCurrent() {

return current.addAndGet(weight);

}

public void sel(int total) {

current.addAndGet(-1 * total);

}

public long getLastUpdate() {

return lastUpdate;

}

public void setLastUpdate(long lastUpdate) {

this.lastUpdate = lastUpdate;

}

}

private ConcurrentMap<String, ConcurrentMap<String, WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap<String, ConcurrentMap<String, WeightedRoundRobin>>();

protected <T> Collection<String> getInvokerAddrList(List<Invoker<T>> invokers, Invocation invocation) {

String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();

Map<String, WeightedRoundRobin> map = methodWeightMap.get(key);

if (map != null) {

return map.keySet();

}

return null;

}

@Override

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();

// 获取 url 到 WeightedRoundRobin 映射表,如果为空,则创建一个新的

ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.computeIfAbsent(key, k -> new ConcurrentHashMap<>());

int totalWeight = 0;

long maxCurrent = Long.MIN_VALUE;

long now = System.currentTimeMillis();

Invoker<T> selectedInvoker = null;

WeightedRoundRobin selectedWRR = null;

// 遍历 Invoker 列表

for (Invoker<T> invoker : invokers) {

String identifyString = invoker.getUrl().toIdentityString();

int weight = getWeight(invoker, invocation);

// 检测当前 Invoker 是否有相应的 WeightedRoundRobin,没有则创建

WeightedRoundRobin weightedRoundRobin = map.computeIfAbsent(identifyString, k -> {

WeightedRoundRobin wrr = new WeightedRoundRobin();

wrr.setWeight(weight);

return wrr;

});

// 检测 Invoker 权重是否发生了变化,若变化了,则更新 WeightedRoundRobin 的 weight 字段

if (weight != weightedRoundRobin.getWeight()) {

//weight changed

weightedRoundRobin.setWeight(weight);

}

// 让 current 字段加上自身权重,等价于 current += weight

long cur = weightedRoundRobin.increaseCurrent();

// 设置 lastUpdate 字段,即 lastUpdate = now

weightedRoundRobin.setLastUpdate(now);

// 寻找具有最大 current 的 Invoker,以及 Invoker 对应的 WeightedRoundRobin,暂存起来,留作后用

if (cur > maxCurrent) {

maxCurrent = cur;

selectedInvoker = invoker;

selectedWRR = weightedRoundRobin;

}

// 计算权重总和

totalWeight += weight;

}

if (invokers.size() != map.size()) {

// 若未更新时长超过阈值后,就会被移除掉,默认阈值为60秒。

map.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);

}

if (selectedInvoker != null) {

// 让 current 减去权重总和

selectedWRR.sel(totalWeight);

// 返回具有最大 current 的 Invoker

return selectedInvoker;

}

// should not happen here

return invokers.get(0);

}

}

LeastActiveLoadBalance

最少活跃调用数算法的思想就是"活跃调用数越少,证明服务效率越高,单位时间内可以处理的请求就越多"。每一个服务提供者对应一个active,初始值都是0,每增加一次请求就+1,每完成一次请求就-1。在服务运行一段时间之后,性能越好的服务的active就越小,处理请求的速度就越快,所以获得新请求的机会就越大,这就是最少活跃数负载均衡算法的基本思想。

LeastActiveLoadBalance在最少活跃数负载均衡算法的基础上增加权重的概念。举个例子来说明:有两个性能非常好的服务器,在某一时间active是相同的,此时 Dubbo 会根据它们的权重去分配请求,权重越大,获取到新请求的概率就越大。如果两个服务提供者权重相同,此时随机选择一个即可。

  • 最少活跃调用数算法的源码

public class LeastActiveLoadBalance extends AbstractLoadBalance {

public static final String NAME = "leastactive";

@Override

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {

// invoker的数量

int length = invokers.size();

// 所有invoker的最小活跃数

int leastActive = -1;

// 具有相同“最小活跃数”的服务者提供者

int leastCount = 0;

// 记录具有相同“最小活跃数”的 Invoker 在 invokers 列表中的下标信息

int[] leastIndexes = new int[length];

// 每一个invoker的权重

int[] weights = new int[length];

// 所有invoker的权重之和

int totalWeight = 0;

// 第一个最小活跃数的 Invoker 权重值,用于与其他具有相同最小活跃数的 Invoker 的权重进行对比,

// 以检测是否“所有具有相同最小活跃数的 Invoker 的权重”均相等

int firstWeight = 0;

boolean sameWeight = true;

// 开始遍历所有invokers

for (int i = 0; i < length; i++) {

Invoker<T> invoker = invokers.get(i);

// 获取当前索引的invoker的活跃数

int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();

// 获取权重,默认是100

int afterWarmup = getWeight(invoker, invocation);

// 保存后面使用

weights[i] = afterWarmup;

// 如果是第一个invoker,或者当前invoker的active小于最小活跃数

if (leastActive == -1 || active < leastActive) {

// 将当前invoker的active赋值给最小活跃数

leastActive = active;

// 更新leastCount为1

leastCount = 1;

// 记录当前下标值到 leastIndexs 中

leastIndexes[0] = i;

// 重置 totalWeight

totalWeight = afterWarmup;

// 记录第一个最小活跃数的 invoker 权重值

firstWeight = afterWarmup;

// 每一个invoker都有相同的权重(这里只有一个调用)

sameWeight = true;

// 如果当前invoker的active等于最小活跃数,则累加

} elseif (active == leastActive) {

// 在 leastIndexs 中记录下当前 Invoker 在 invokers 集合中的下标

leastIndexes[leastCount++] = i;

// 累加权重

totalWeight += afterWarmup;

// 检测当前 Invoker 的权重与 firstWeight 是否相等,不相等则将 sameWeight 置为 false

if (sameWeight && afterWarmup != firstWeight) {

sameWeight = false;

}

}

}

// 当只有一个 Invoker 具有最小活跃数,此时直接返回该 Invoker 即可

if (leastCount == 1) {

return invokers.get(leastIndexes[0]);

}

// 有多个 invoker 具有相同的最小活跃数,但它们之间的权重不同

if (!sameWeight && totalWeight > 0) {

// 生成一个随机数

int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);

// 循环让随机数减去具有最小活跃数的 Invoker 的权重值,

// 当 offset 小于等于0时,返回相应的 Invoker

for (int i = 0; i < leastCount; i++) {

int leastIndex = leastIndexes[i];

// 获取权重值,并让随机数减去权重值

offsetWeight -= weights[leastIndex];

if (offsetWeight < 0) {

return invokers.get(leastIndex);

}

}

}

// 如果权重相同或权重为0时,随机返回一个 Invoker

return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);

}

}

通过以上源码和注释可以总结如下:

①遍历 invokers 列表,寻找活跃数最小的Invoker。

②如果有多个Invoker具有相同的最小活跃数,此时记录下这些Invoker在invokers 集合中的下标,并累加它们的权重,比较它们的权重值是否相等。

③如果只有一个Invoker具有最小的活跃数,此时直接返回该Invoker即可。

④如果有多个Invoker具有最小活跃数,且它们的权重不相等,此时处理方式和 RandomLoadBalance 一致。

⑤如果有多个Invoker具有最小活跃数,但它们的权重相等,此时随机返回一个即可。

ConsistentHashLoadBalance

  • Hash算法的定义如下

把任意长度的输入,通过Hash算法变换成固定长度的输出,这个输出就是Hash值。哈希值的空间远小于输入的空间,所以可能会发生“哈希碰撞”,即两个不同的输入,产生了同一个输出。

Hash算法只是一个定义,并没有规定具体的实现。比如我们常见的MD5、SHA都属于Hash算法的实现。

  • 一致性Hash

使用一致性Hash可以解决因为横向伸缩导致的大规模数据变动。

先对服务器节点的IP进行Hash,然后除以2^{32}得到服务器节点在这个Hash环中的位置,然后并将这个Hash投射到 [0, 2^{32} - 1] 的圆环上。

在这里插入图片描述

假设这个时候来了一个请求,先对这个请求进行Hash求值,然后除以2^{32}求余,然后顺时针找到第一个节点,那么这个节点就是负责处理请求的节点。比如请求①和②就是节点A负责处理,③就是节点B负责处理,④就是节点C负责处理。

在这里插入图片描述

但是一致性Hash也存在一定的局限性。假如节点很少的情况下,一致性Hash就会出现分布不均匀的情况。如下所示,①②③④⑥的请求都会落在A节点上,这个时候就会对A节点增加过多的符合。

在这里插入图片描述

那么这个时候提出了虚拟节点的概念,通过虚拟节点均衡各个节点的请求量。所谓虚拟节点,就是除了对服务本身地址进行Hash映射外,还通过在它地址上做些处理(比如Dubbo中,在ip+port的字符串后加上计数符1、2、3......,分别代表虚拟节点1、2、3),以达到同一服务映射多个节点的目的。

在这里插入图片描述

参考文章

  • dubbo.apache.org/zh-cn/docs/…
  • dubbo.apache.org/zh-cn/blog/…

以上是 Dubbo进阶(十二):负载均衡 的全部内容, 来源链接: utcz.com/a/25280.html

回到顶部