Dubbo-负载均衡原理解析

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

Integer weight = (Integer) weights[i];

totalWeight += weight;

//判断权重是否都相等

if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) {

sameWeight = false;

}

}

Random random = new Random();

int randomPos = random.nextInt(totalWeight);

//权重值不相等

if (!sameWeight) {

for (String ip : ServerIps.WEIGHT_LIST.keySet()) {

Integer value = ServerIps.WEIGHT_LIST.get(ip);

if (randomPos < value) {

return ip;

}

randomPos = randomPos - value;

}

}

return (String) ServerIps.WEIGHT_LIST.keySet().toArray()[new Random().nextInt(ServerIps.WEIGHT_LIST.size())];

}

public static void main(String[] args) {

//连续调用10次

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

System.out.println(getServer());

}

}

}

轮询算法-RoundRobinLoadBalance


特点

  • 加权轮询,按公约后的权重设置轮询比率,循环调用节点

  • 缺点:同样存在慢的提供者累积请求的问题。

代码实现(简单的轮询算法)

public class RoundRobin {

private static Integer pos = 0;

public static String getServer() {

String ip = “”;

//pos同步

synchronized (pos) {

if (pos >= ServerIps.LIST.size()) {

pos = 0;

}

ip = ServerIps.LIST.get(pos);

pos++;

}

return ip;

}

public static void main(String[] args) {

//连续调用10次

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

System.out.println(getServer());

}

}

}

执行结果如下:

192.168.0.1

192.168.0.2

192.168.0.3

192.168.0.4

192.168.0.5

192.168.0.6

192.168.0.7

192.168.0.8

192.168.0.9

192.168.0.10

192.168.0.1

这种算法很简单,也很公平,每台服务轮流来进行服务,但是有的机器性能好,所以能者多劳,和随机算法一样,加上权重维度以后,其中一种实现方法就是复制法,复制法的缺点是,比较消耗内存。

介绍另外一种算法:这种算法引入一个概念:调用编号,比如第1次调用为1,第2次调用为2,第100次调用为100,调用编号是递增的,所以我们可以根据这个调用编号推算出服务器。

假设我们有三台服务器servers = [A, B, C],对应的权重为 weights = [ 2, 5, 1], 总权重为8,我们可以理解为有8台“服务器”,这是8台“不具有并发功能”,其中有2台为A,5台为B,1台为C,一次调用过来的时候,需要按顺序访问,比如有10次调用,那么服务器调用顺序为AABBBBBCAA,调用编号会越来越大,而服务器是固定的,所以需要把调用编号“缩小”,这里对调用编号进行取余,除数为总权重和

比如:

  1. 1号调用,1%8=1;

  2. 2号调用,2%8=2;

  3. 3号调用,3%8=3;

  4. 8号调用,8%8=0;

  5. 9号调用,9%8=1;

  6. 100号调用,100%8=4;

我们发现调用编号可以被缩小为0-7之间的8个数字,问题是怎么根据这个8个数字找到对应的服务器呢?和我们随机算法类似,这里也可以把权重想象为一个坐标轴“0-----2-----7-----8”

  1. 1号调用,1%8=1,offset = 1, offset <= 2 is true,取A;

  2. 2号调用,2%8=2;offset = 2,offset <= 2 is true, 取A;

  3. 3号调用,3%8=3;offset = 3, offset <= 2 is false, offset = offset - 2, offset = 1, offset <= 5,取B

  4. 8号调用,8%8=0;offset = 0, 特殊情况,offset = 8,offset <= 2 is false, offset = offset - 2, offset = 6, offset <= 5 is false, offset = offset - 5, offset = 1, offset <= 1 is true, 取C;

  5. 9号调用,9%8=1;// …

  6. 100号调用,100%8=4; //…

模拟调用编号获取工具:

public class Sequence {

public static Integer num=0;

public static Integer getAndIncrement(){

return ++num;

}

}

public class WeightRoundRobin {

private static Integer pos = 0;

public static String getServer() {

int totalWeight = 0;

boolean sameWeight = true;

Object[] weights = ServerIps.WEIGHT_LIST.values().toArray();

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

Integer weight = (Integer) weights[i];

totalWeight += weight;

if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) {

sameWeight = false;

}

}

Integer sequenceNum = Sequence.getAndIncrement();

Integer offset = sequenceNum % totalWeight;

offset = offset == 0 ? totalWeight : offset;

if (!sameWeight) {

for (String ip : ServerIps.WEIGHT_LIST.keySet()) {

Integer weight = ServerIps.WEIGHT_LIST.get(ip);

if (offset <= weight) {

return ip;

}

offset = offset - weight;

}

}

String ip = “”;

synchronized (pos) {

if (pos >= ServerIps.LIST.size()) {

pos = 0;

}

ip = ServerIps.LIST.get(pos);

pos++;

}

return ip;

}

public static void main(String[] args) {

// 连续调用11次

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

System.out.println(getServer());

}

}

}

执行结果如下

192.168.0.2

192.168.0.2

192.168.0.2

192.168.0.2

192.168.0.2

192.168.0.2

192.168.0.2

192.168.0.2

192.168.0.1

192.168.0.4

192.168.0.4

但是这种算法有一个缺点:一台服务器的权重特别大的时候,他需要连续的的处理请求,但是实际上我们想达到的效果是,对于100次请求,只要有100*8/50=16次就够了,这16次不一定要连续的访问,比如假设我们有三台服务器 servers = [A, B, C],对应的权重为 weights = [5, 1, 1] , 总权重为7,那么上述这个算法的结果是:AAAAABC,那么如果能够是这么一个结果呢:AABACAA,把B和C平均插入到5个A中间,这样是比较均衡的了。

我们这里可以改成平滑加权轮询。

平滑加权轮询


特点

思路:每个服务器对应两个权重,分别为weight 和currentWeight.其中weight是固定的,currentWeight会动态调整,初始值为0.当有新的请求进来时,遍历服务器列表,让它的currentWeight加上自身权重。遍历完成后,找到最大的 currentWeight,并将其减去权重总和,然后返回相应的服务器即可。

假设我们有三台服务器 servers = [A, B, C],对应的权重为 weights = [5, 1, 1] , 总权重为7

| 请求编号 | currentWeight 数组 (current_weight += weight) | 选择结果(max(currentWeight)) | 减去权重总和后的currentWeight 数组(max(currentWeight) -= sum(weight)) |

| — | — | — | — |

| 1 | [5, 1, 1] | A | [-2, 1, 1] |

| 2 | [3, 2, 2] | A | [-4, 2, 2] |

| 3 | [1, 3, 3] | B | [1, -4, 3] |

| 4 | [6, -3, 4] | A | [-1, -3, 4] |

| 5 | [4, -2, 5] | C | [4, -2, -2] |

| 6 | [9, -1, -1] | A | [2, -1, -1] |

| 7 | [7, 0, 0] | A | [0, 0, 0] |

如上,经过平滑性处理后,得到的服务器序列为 [A, A, B, A, C, A, A],相比之前的序列 [A, A, A, A, A, B, C],分布性要好一些。初始情况下 currentWeight = [0, 0, 0],第7个请求处理完后,currentWeight 再次变为 [0, 0, 0]。

代码实现

// 增加一个Weight类,用来保存ip, weight(固定不变的原始权重), currentweight(当前会变化的权重)

public class Weight {

private String ip;

private Integer weight;

private Integer currentWeight;

public Weight(String ip, Integer weight, Integer currentWeight) {

this.ip = ip;

this.weight = weight;

this.currentWeight = currentWeight;

}

public String getIp() {

return ip;

}

public void setIp(String ip) {

this.ip = ip;

}

public Integer getWeight() {

return weight;

}

public void setWeight(Integer weight) {

this.weight = weight;

}

public Integer getCurrentWeight() {

return currentWeight;

}

public void setCurrentWeight(Integer currentWeight) {

this.currentWeight = currentWeight;

}

}

public class WeightRoundRobinV2 {

private static Map<String, Weight> weightMap = new HashMap<String, Weight>();

public static String getServer() {

// 获取权重之和

int totalWeight = ServerIps.WEIGHT_LIST1.values().stream().reduce(0, (w1, w2) -> w1 + w2);

//初始化weightMap,初始时将currentWeight赋值为weight

if (weightMap.isEmpty()) {

ServerIps.WEIGHT_LIST1.forEach((key, value) -> {

weightMap.put(key, new Weight(key, value, value));

});

}

//找出currentWeight最大值

Weight maxCurrentWeight = null;

for (Weight weight : weightMap.values()) {

if (maxCurrentWeight == null || weight.getCurrentWeight() > maxCurrentWeight.getCurrentWeight()) {

maxCurrentWeight = weight;

}

}

//将maxCurrentWeight减去总权重和

maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight() - totalWeight);

//所有的ip的currentWeight统一加上原始权重

for (Weight weight : weightMap.values()) {

weight.setCurrentWeight(weight.getCurrentWeight() + weight.getWeight());

}

//返回maxCurrentWeight所对应的ip

return maxCurrentWeight.getIp();

}

public static void main(String[] args) {

// 连续调用10次

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

System.out.println(getServer());

}

}

}

ServerIps里添加数据WEIGHT_LIST1:

public static final Map<String, Integer> WEIGHT_LIST1 = new HashMap<String, Integer>();

static {

// 权重之和为50

WEIGHT_LIST1.put(“A”, 5);

WEIGHT_LIST1.put(“B”, 1);

WEIGHT_LIST1.put(“C”, 1);

}

执行结果如下:

A

A

B

A

C

A

A

A

A

B

一致性哈希算法-ConsistentHashLoadBalance


服务器集群接收到一次请求调用时,可以根据请求的信息,比如客户端的Ip地址,或请求路径与请求参数等信息进行哈希,可以得到一个哈希值,特点是对于相同的ip地址,或请求路径和请求参数哈希出来的值是不一样的,只要能再增加一个算法,能够把这个哈希值映射成一个服务端ip地址,就可以使相同的请求(相同的ip地址,或请求路径和请求参数)落到同一服务器上。

因为客户端发起的请求情况是无穷无尽的(客户端地址不同,请求参数不同等),所以对于的哈希值是无穷大的,所以我们不可能把所有的哈希值都进行映射到服务端ip上。所以这里用到了哈希环。

在这里插入图片描述

  • 哈希值如果需要ip1和ip2之间的,则应该选择ip2作为结果;

  • 哈希值如果需要ip2和ip3之间的,则应该选择ip3作为结果;

  • 哈希值如果需要ip3和ip4之间的,则应该选择ip4作为结果;

  • 哈希值如果需要ip4和ip1之间的,则应该选择ip1作为结果;

上面这情况是比较均匀情况,如果出现ip4服务器不存在,那就是这样了:

在这里插入图片描述

通过图片会发现,ip3和ip1直接的范围是比较大的,会有更多的请求落在ip1上,这是不“公平的”,解决这个问题需要加入虚拟节点,比如:

在这里插入图片描述

其中ip2-1, ip3-1就是虚拟结点,并不能处理节点,而是等同于对应的ip2和ip3服务器。

实际上,这只是处理这种不均衡性的一种思路,实际上就算哈希环本身是均衡的,你也可以增加更多的虚拟节点来使这个环更加平滑,比如:

在这里插入图片描述

这个彩环也是“公平的”,并且只有ip1,2,3,4是实际的服务器ip,其他的都是虚拟ip。

特点

  • 一致性 Hash,相同参数的请求总是发到同一提供者。

  • 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

代码实现

对于我们的服务端ip地址,我们肯定知道总共有多少个,需要多少个虚拟节点也有我们自己控制,虚拟节点越多则流量越均衡,另外哈希算法也是很关键的,哈希算法越散列流量也将越均衡。

public class ConsistentHash {

private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();

private static final int VIRTUAL_NODES = 160;

static {

//对每个真实节点添加虚拟节点,虚拟节点会根据哈希算法进行散列

for (String ip : ServerIps.LIST) {

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

int hash = getHash(ip + “VN” + 1);

virtualNodes.put(hash, ip);

}

}

}

private static int getHash(String str) {

final int p = 16777619;

int hash = (int) 2166136261L;

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

hash = (hash ^ str.charAt(i)) * p;

hash += hash << 13;

hash ^= hash >> 7;

hash += hash << 3;

hash ^= hash >> 17;

hash += hash << 5;

}

// 如果算出来的值为负数则取其绝对值

if (hash < 0)

hash = Math.abs(hash);

return hash;

}

private static String getServer(String client) {

int hash = getHash(client);

// 得到大于该Hash值的排好序的Map

SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);

// 大于该hash值的第一个元素的位置

Integer nodeIndex = subMap.firstKey();

// 如果不存在大于该hash值的元素,则返回根节点

if (nodeIndex == null) {

nodeIndex = virtualNodes.firstKey();

}

// 返回对应的虚拟节点名称

return subMap.get(nodeIndex);

}

public static void main(String[] args) {

// 连续调用10次,随机10个client

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

System.out.println(getServer(“client” + i));

}

}

}

运行结果如下:

192.168.0.4

192.168.0.3

192.168.0.2

192.168.0.7

192.168.0.7

192.168.0.10

192.168.0.5

192.168.0.4

192.168.0.5

192.168.0.4

最小活跃数算法-LeastActiveLoadBalance


前面几种方法主要目标是使服务端分配到的调用次数尽量均衡,但是实际情况是这样吗?调用次数相同,服务器的负载就均衡吗?当然不是,这里还要考虑每次调用的时间,而最小活跃数算法则是解决这种问题的。

活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求。此时应优先将请求分配给该服务提供者。在具体实现中,每个服务提供者对应一个活跃数。初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求、这就是最小活跃数负载均衡算法的基本思想。除了最小活跃数,最小活跃数算法在实现上还引入了权重值。所以准确的来说,最小活跃数算法是基于加权最小活跃数算法实现的。举个例子说明一下,在一个服务提供者集群中,有两个性能优异的服务提供者。某一时刻它们的活跃数相同,则会根据它们的权重去分配请求,权重越大,获取到新请求的概率就越大。如果两个服务提供者权重相同,此时随机选择一个即可。

特点

  • 加权最少活跃调用优先,活跃数越低,越优先调用,相同活跃数的进行加权随机。活跃数指调用前后计数差(针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供者处理能力越强。

  • 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大;相对的,处理能力越强的节点,处理更多的请求。

代码实现

因为活跃数是需要服务器请求处理相关逻辑配合的,一次调用开始时活跃数+1,结束是活跃数-1,所以这里就不对这部分逻辑进行模拟了,直接使用一个map来进行模拟。

public class LeastActive {

private static String getServer() {

//找出当前活跃数最小的服务器

Optional minValue = ServerIps.ACTIVITY_LIST.values().stream().min(Comparator.naturalOrder());

if (minValue.isPresent()) {

List minActivityIps = new ArrayList<>();

ServerIps.ACTIVITY_LIST.forEach((ip, activity) -> {

if (activity.equals(minValue.get())) {

minActivityIps.add(ip);

}

});

// 最小活跃数的ip有多个,则根据权重来选,权重大的优先

if (minActivityIps.size() > 1) {

Map<String, Integer> weightList = new LinkedHashMap<String, Integer>();

ServerIps.WEIGHT_LIST.forEach((ip, weight) -> {

if (minActivityIps.contains(ip)) {

weightList.put(ip, ServerIps.WEIGHT_LIST.get(ip));

}

});

int totalWeight = 0;

boolean sameWeight = true;//如果所有权重都相等,那么随机一个ip即可

Object[] weights = weightList.values().toArray();

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

Integer weight = (Integer) weights[i];

totalWeight += weight;

if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) {

sameWeight = false;

}

}

Random random = new Random();

int randomRos = random.nextInt(totalWeight);

if (!sameWeight) {

for (String ip : weightList.keySet()) {

Integer value = weightList.get(ip);

if (randomRos < value) {

return ip;

}

randomRos = randomRos - value;

}

}

return (String) weightList.keySet().toArray()[new Random().nextInt(weightList.size())];

} else {

return minActivityIps.get(0);

}

} else {

return (String) ServerIps.WEIGHT_LIST.keySet().toArray()[new Random().nextInt(ServerIps.WEIGHT_LIST.size())];

}

}

public static void main(String[] args) {

//连续调用10次,随机10个client

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

System.out.println(getServer());

}

}

}

Dubbo中负载均衡

=====================================================================

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
().nextInt(weightList.size())];

} else {

return minActivityIps.get(0);

}

} else {

return (String) ServerIps.WEIGHT_LIST.keySet().toArray()[new Random().nextInt(ServerIps.WEIGHT_LIST.size())];

}

}

public static void main(String[] args) {

//连续调用10次,随机10个client

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

System.out.println(getServer());

}

}

}

Dubbo中负载均衡

=====================================================================

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-4ISCQv4H-1715667774538)]

[外链图片转存中…(img-Wvsz4Vtk-1715667774538)]

[外链图片转存中…(img-XiPSYoZH-1715667774538)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值