经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
Java负载均衡算法实现之轮询和加权轮询
来源:jb51  时间:2022/4/12 8:46:41  对本文有异议

1.普通轮询算法

轮询(Round Robin,RR)是依次将用户的访问请求,按循环顺序分配到web服务节点上,从1开始到最后一台服务器节点结束,然后再开始新一轮的循环。这种算法简单,但是没有考虑到每台节点服务器的具体性能,请求分发往往不均衡。

代码实现:

  1. /**
  2. * 普通轮询算法
  3. */public class RoundRobin {
  4. private static Integer index = 0;
  5. private static List<String> nodes = new ArrayList<>();
  6. // 准备模拟数据
  7. static {
  8. nodes.add("192.168.1.101");
  9. nodes.add("192.168.1.103");
  10. nodes.add("192.168.1.102");
  11. System.out.println("普通轮询算法的所有节点:"+nodes);//打印所有节点
  12. }
  13. // 关键代码
  14. public String selectNode(){
  15. String ip = null;
  16. synchronized (index){
  17. // 下标复位
  18. if(index>=nodes.size()) index = 0;
  19. ip = nodes.get(index);
  20. index++;
  21. }
  22. return ip;
  23. }
  24.  
  25. // 并发测试:两个线程循环获取节点
  26. public static void main(String[] args) {
  27. new Thread(() -> {
  28. RoundRobin roundRobin1 = new RoundRobin();
  29. for (int i=1;i<=5;i++){
  30. String serverIp = roundRobin1.selectNode();
  31. System.out.println(Thread.currentThread().getName()+"==第"+i+"次获取节点:"+serverIp);
  32. }
  33. }).start();
  34.  
  35. RoundRobin roundRobin2 = new RoundRobin();
  36. for (int i=1;i<=nodes.size();i++){
  37. String serverIp = roundRobin2.selectNode();
  38. System.out.println(Thread.currentThread().getName()+"==第"+i+"次获取节点:"+serverIp);
  39. }
  40. }
  41. }

执行结果:不同线程访问,结果依旧是按顺序循环分配节点

普通轮询算法的所有节点:[192.168.1.101, 192.168.1.103, 192.168.1.102]

main==第1次获取节点:192.168.1.101

Thread-0==第1次获取节点:192.168.1.103

Thread-0==第2次获取节点:192.168.1.102

Thread-0==第3次获取节点:192.168.1.101

Thread-0==第4次获取节点:192.168.1.103

Thread-0==第5次获取节点:192.168.1.102

main==第2次获取节点:192.168.1.101

main==第3次获取节点:192.168.1.103

2.加权轮询算法

加权轮询(Weighted Round Robin,WRR)是根据设定的权重值来分配访问请求,权重值越大的,被分到的请求数也就越多。一般根据每台节点服务器的具体性能来分配权重。

2.1.实现方式一

将需要轮询的所有节点按权重数循环生成一个List 集合,然后就跟普通轮询算法一样,来一个、分配一个、进1位。

例如:

所有节点信息:{{“192.168.1.100“,5},{“192.168.1.101“,1},{“192.168.1.102“,3}}

那么生成的List 集合为:

{“192.168.1.100“,

“192.168.1.100“,

“192.168.1.100“,

“192.168.1.100“,

“192.168.1.100“,

“192.168.1.101“,

“192.168.1.102“,

“192.168.1.102“,

“192.168.1.102“}

后面就是普通轮询算法的逻辑

代码实现:

类似于二维数组 降维成 一维数组,然后使用普通轮询

  1. /**
  2. * 简单版的加权轮询
  3. */public class WeightedRoundRobinSimple {
  4. private static Integer index = 0;
  5. private static Map<String,Integer> mapNodes = new HashMap<>();
  6.  
  7. // 准备模拟数据
  8. static {
  9. mapNodes.put("192.168.1.101",1);
  10. mapNodes.put("192.168.1.102",3);
  11. mapNodes.put("192.168.1.103",2);
  12. /* -- 以下代码只为了方便查看所有节点,删除不影响 -- S */
  13. List<String> nodes = new ArrayList<>();
  14. Iterator<Map.Entry<String, Integer>> iterator = mapNodes.entrySet().iterator();
  15. while (iterator.hasNext()){
  16. Map.Entry<String, Integer> entry = iterator.next();
  17. String key = entry.getKey();
  18. for (int i=0;i<entry.getValue();i++){
  19. nodes.add(key);
  20. }
  21. }
  22. System.out.println("简单版的加权轮询:"+nodes);//打印所有节点
  23. /* -- 以上代码只为了方便查看所有节点,删除不影响-- E */
  24. }
  25.  
  26. // 关键代码:类似于二维数组 降维成 一维数组,然后使用普通轮询
  27. public String selectNode(){
  28. List<String> nodes = new ArrayList<>();
  29. Iterator<Map.Entry<String, Integer>> iterator = mapNodes.entrySet().iterator();
  30. while (iterator.hasNext()){
  31. Map.Entry<String, Integer> entry = iterator.next();
  32. String key = entry.getKey();
  33. for (int i=0;i<entry.getValue();i++){
  34. nodes.add(key);
  35. }
  36. }
  37. String ip = null;
  38. synchronized (index){
  39. // 下标复位
  40. if(index>=nodes.size()) index = 0;
  41. ip = nodes.get(index);
  42. index++;
  43. }
  44. return ip;
  45. }
  46.  
  47. // 并发测试:两个线程循环获取节点
  48. public static void main(String[] args) {
  49. new Thread(() -> {
  50. WeightedRoundRobinSimple roundRobin1 = new WeightedRoundRobinSimple();
  51. for (int i=1;i<=6;i++){
  52. String serverIp = roundRobin1.selectNode();
  53. System.out.println(Thread.currentThread().getName()+"==第"+i+"次获取节点:"+serverIp);
  54. }
  55. }).start();
  56.  
  57. WeightedRoundRobinSimple roundRobin2 = new WeightedRoundRobinSimple();
  58. for (int i=1;i<=6;i++){
  59. String serverIp = roundRobin2.selectNode();
  60. System.out.println(Thread.currentThread().getName()+"==第"+i+"次获取节点:"+serverIp);
  61. }
  62. }
  63. }

执行结果:两个线程循环测试,输出结果会出现交替分配到不同的IP,但最终的效果都是一个个按顺序分配,类似于普通轮询算法。

简单版的加权轮询:[192.168.1.103, 192.168.1.103, 192.168.1.101, 192.168.1.102, 192.168.1.102, 192.168.1.102]

main==第1次获取节点:192.168.1.103

main==第2次获取节点:192.168.1.103

main==第3次获取节点:192.168.1.101

main==第4次获取节点:192.168.1.102

main==第5次获取节点:192.168.1.102

Thread-0==第1次获取节点:192.168.1.102

Thread-0==第2次获取节点:192.168.1.103

main==第6次获取节点:192.168.1.103

Thread-0==第3次获取节点:192.168.1.101

Thread-0==第4次获取节点:192.168.1.102

Thread-0==第5次获取节点:192.168.1.102

Thread-0==第6次获取节点:192.168.1.102

2.2.实现方式二(重点难点)

本文的重点难点。

在实现方式一的算法中可以很明显的看到,同权重的IP会被连续分配,也就是说同一个IP在短时间内收到不同的请求,过了这个连续点,就要等到下一轮才会被分配到,并没有做到均匀分配节点。

实现方式二将尽可能地均匀分配每个节点,节点分配不再是连续的,但最终的权重比和上一个方式一样,这种加权轮询又被称为平滑加权轮询。

理解关键的几个参数和算法逻辑,方便理解代码的实现。

2.2.1.概述

关键参数

  • ip:负载IP

  • weight:权重,保存配置的权重

  • effectiveWeight:有效权重,轮询的过程权重可能变化

  • currentWeight:当前权重,比对该值大小获取节点

注意几个点:

weight 权重,在整个过程不会对它做修改,只用来保存配置时的权重参数值。如果直接拿weight 运算而不保存配置的最原始权重参数,那么将会丢失最关键的用户配置的权重参数。

effectiveWeight 有效权重,在整个过程可能会变化,初始值等于weight,主要用于当节点出现分配失败时降低权重值,成功时提高权重值(但不能大于weight值),本案例为了简化算法,并未加入这功能,因此本案例中effectiveWeight始终等于weight。

currentWeight 当前权重,通过循环所有节点比对该值大小来分配权重最大的节点,初始值等于weight。

三个权重参数的变化情况

仅仅针对本案例,因为本案例为了简化算法,并未加入[节点出现分配失败时降低权重值,成功时提高权重值(但不能大于weight值)的功能],所以有效权重effectiveWeight 不会发生变化。

  • 第一次加权轮询时:currentWeight = weight = effectiveWeight;

  • 后面每次加权轮询时:currentWeight 的值都会不断变化,weight 和effectiveWeight 的值不变;

  • 被分配的节点的currentWeight = currentWeight - 权重之和

  • 所有节点的currentWeight = currentWeight + effectiveWeight

2.2.2.举个例子理解算法

你面前有三个瓶子A、B、C,分别装有1L、3L、2L水。

第一轮分配情况:B多,所以把B瓶子的3L水,分1L给A,分2L给C(按权重分),分完之后:A、B、C分别为:2L、0L、4L

第二轮分配情况:C多,所以把C瓶子的4L水,分1L给A,分3L给B(按权重分),分完之后:A、B、C分别为:3L、3L、0L

第三轮分配情况:A和B一样多,那么拿谁去分呢?拿谁其实都一样(算法中写了A大于B才选A,现在等于,所以不选A),所以把B瓶子的3L水,分1L给A,分2L给C(按权重分),分完之后:A、B、C分别为:4L、0L、2L

然后不断的进行下去……

简化成数学逻辑(代码实现)的关键两步

  • 被分配的节点的currentWeight = currentWeight - 权重之和

  • 所有节点的currentWeight = currentWeight + effectiveWeight

下面通过阅读代码来理解

2.2.3.代码实现

节点对象

  1. /**
  2. * String ip:负载IP
  3. * final Integer weight:权重,保存配置的权重
  4. * Integer effectiveWeight:有效权重,轮询的过程权重可能变化
  5. * Integer currentWeight:当前权重,比对该值大小获取节点
  6. * 第一次加权轮询时:currentWeight = weight = effectiveWeight
  7. * 后面每次加权轮询时:currentWeight 的值都会不断变化,其他权重不变
  8. */public class Node implements Comparable<Node>{
  9. private String ip;
  10. private final Integer weight;
  11. private Integer effectiveWeight;
  12. private Integer currentWeight;
  13.  
  14. public Node(String ip,Integer weight){
  15. this.ip = ip;
  16. this.weight = weight;
  17. this.effectiveWeight = weight;
  18. this.currentWeight = weight;
  19. }
  20.  
  21. public Node(String ip, Integer weight, Integer effectiveWeight, Integer currentWeight) {
  22. this.ip = ip;
  23. this.weight = weight;
  24. this.effectiveWeight = effectiveWeight;
  25. this.currentWeight = currentWeight;
  26. }
  27.  
  28. public String getIp() {
  29. return ip;
  30. }
  31.  
  32. public void setIp(String ip) {
  33. this.ip = ip;
  34. }
  35.  
  36. public Integer getWeight() {
  37. return weight;
  38. }
  39.  
  40. public Integer getEffectiveWeight() {
  41. return effectiveWeight;
  42. }
  43.  
  44. public void setEffectiveWeight(Integer effectiveWeight) {
  45. this.effectiveWeight = effectiveWeight;
  46. }
  47.  
  48. public Integer getCurrentWeight() {
  49. return currentWeight;
  50. }
  51.  
  52. public void setCurrentWeight(Integer currentWeight) {
  53. this.currentWeight = currentWeight;
  54. }
  55.  
  56. @Override
  57. public int compareTo(Node node) {
  58. return currentWeight > node.currentWeight ? 1 : (currentWeight.equals(node.currentWeight) ? 0 : -1);
  59. }
  60.  
  61. @Override
  62. public String toString() {
  63. return "{ip='" + ip + "', weight=" + weight + ", effectiveWeight=" + effectiveWeight + ", currentWeight=" + currentWeight + "}";
  64. }
  65. }

加权轮询算法

  1. /**
  2. * 加权轮询算法
  3. */public class WeightedRoundRobin {
  4.  
  5. private static List<Node> nodes = new ArrayList<>();
  6. // 权重之和
  7. private static Integer totalWeight = 0;
  8. // 准备模拟数据
  9. static {
  10. nodes.add(new Node("192.168.1.101",1));
  11. nodes.add(new Node("192.168.1.102",3));
  12. nodes.add(new Node("192.168.1.103",2));
  13. nodes.forEach(node -> totalWeight += node.getEffectiveWeight());
  14. }
  15.  
  16. /**
  17. * 按照当前权重(currentWeight)最大值获取IP
  18. * @return Node
  19. */
  20. public Node selectNode(){
  21. if (nodes ==null || nodes.size()<=0) return null;
  22. if (nodes.size() == 1) return nodes.get(0);
  23.  
  24. Node nodeOfMaxWeight = null; // 保存轮询选中的节点信息
  25. synchronized (nodes){
  26. // 打印信息对象:避免并发时打印出来的信息太乱,不利于观看结果
  27. StringBuffer sb = new StringBuffer();
  28. sb.append(Thread.currentThread().getName()+"==加权轮询--[当前权重]值的变化:"+printCurrentWeight(nodes));
  29.  
  30. // 选出当前权重最大的节点
  31. Node tempNodeOfMaxWeight = null;
  32. for (Node node : nodes) {
  33. if (tempNodeOfMaxWeight == null)
  34. tempNodeOfMaxWeight = node;
  35. else
  36. tempNodeOfMaxWeight = tempNodeOfMaxWeight.compareTo(node) > 0 ? tempNodeOfMaxWeight : node;
  37. }
  38. // 必须new个新的节点实例来保存信息,否则引用指向同一个堆实例,后面的set操作将会修改节点信息
  39. nodeOfMaxWeight = new Node(tempNodeOfMaxWeight.getIp(),tempNodeOfMaxWeight.getWeight(),tempNodeOfMaxWeight.getEffectiveWeight(),tempNodeOfMaxWeight.getCurrentWeight());
  40.  
  41. // 调整当前权重比:按权重(effectiveWeight)的比例进行调整,确保请求分发合理。
  42. tempNodeOfMaxWeight.setCurrentWeight(tempNodeOfMaxWeight.getCurrentWeight() - totalWeight);
  43. sb.append(" -> "+printCurrentWeight(nodes));
  44.  
  45. nodes.forEach(node -> node.setCurrentWeight(node.getCurrentWeight()+node.getEffectiveWeight()));
  46.  
  47. sb.append(" -> "+printCurrentWeight(nodes));
  48. System.out.println(sb); //打印权重变化过程
  49. }
  50. return nodeOfMaxWeight;
  51. }
  52.  
  53. // 格式化打印信息
  54. private String printCurrentWeight(List<Node> nodes){
  55. StringBuffer stringBuffer = new StringBuffer("[");
  56. nodes.forEach(node -> stringBuffer.append(node.getCurrentWeight()+",") );
  57. return stringBuffer.substring(0, stringBuffer.length() - 1) + "]";
  58. }
  59.  
  60. // 并发测试:两个线程循环获取节点
  61. public static void main(String[] args){
  62. Thread thread = new Thread(() -> {
  63. WeightedRoundRobin weightedRoundRobin1 = new WeightedRoundRobin();
  64. for(int i=1;i<=totalWeight;i++){
  65. Node node = weightedRoundRobin1.selectNode();
  66. System.out.println(Thread.currentThread().getName()+"==第"+i+"次轮询选中[当前权重最大]的节点:" + node + "\n");
  67. }
  68. });
  69. thread.start();
  70. //
  71. WeightedRoundRobin weightedRoundRobin2 = new WeightedRoundRobin();
  72. for(int i=1;i<=totalWeight;i++){
  73. Node node = weightedRoundRobin2.selectNode();
  74. System.out.println(Thread.currentThread().getName()+"==第"+i+"次轮询选中[当前权重最大]的节点:" + node + "\n");
  75. }
  76.  
  77. }
  78. }

执行结果:

main==加权轮询--[当前权重]值的变化:[1,3,2] -> [1,-3,2] -> [2,0,4] main==第1次轮询选中[当前权重最大]的节点:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}

Thread-0==加权轮询--[当前权重]值的变化:[2,0,4] -> [2,0,-2] -> [3,3,0] Thread-0==第1次轮询选中[当前权重最大]的节点:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}

main==加权轮询--[当前权重]值的变化:[3,3,0] -> [3,-3,0] -> [4,0,2] main==第2次轮询选中[当前权重最大]的节点:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}

main==加权轮询--[当前权重]值的变化:[4,0,2] -> [-2,0,2] -> [-1,3,4] main==第3次轮询选中[当前权重最大]的节点:{ip='192.168.1.101', weight=1, effectiveWeight=1, currentWeight=4}

Thread-0==加权轮询--[当前权重]值的变化:[-1,3,4] -> [-1,3,-2] -> [0,6,0] Thread-0==第2次轮询选中[当前权重最大]的节点:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}

main==加权轮询--[当前权重]值的变化:[0,6,0] -> [0,0,0] -> [1,3,2] main==第4次轮询选中[当前权重最大]的节点:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=6}

Thread-0==加权轮询--[当前权重]值的变化:[1,3,2] -> [1,-3,2] -> [2,0,4] Thread-0==第3次轮询选中[当前权重最大]的节点:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}

main==加权轮询--[当前权重]值的变化:[2,0,4] -> [2,0,-2] -> [3,3,0] main==第5次轮询选中[当前权重最大]的节点:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}

Thread-0==加权轮询--[当前权重]值的变化:[3,3,0] -> [3,-3,0] -> [4,0,2] Thread-0==第4次轮询选中[当前权重最大]的节点:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=3}

main==加权轮询--[当前权重]值的变化:[4,0,2] -> [-2,0,2] -> [-1,3,4] main==第6次轮询选中[当前权重最大]的节点:{ip='192.168.1.101', weight=1, effectiveWeight=1, currentWeight=4}

Thread-0==加权轮询--[当前权重]值的变化:[-1,3,4] -> [-1,3,-2] -> [0,6,0] Thread-0==第5次轮询选中[当前权重最大]的节点:{ip='192.168.1.103', weight=2, effectiveWeight=2, currentWeight=4}

Thread-0==加权轮询--[当前权重]值的变化:[0,6,0] -> [0,0,0] -> [1,3,2] Thread-0==第6次轮询选中[当前权重最大]的节点:{ip='192.168.1.102', weight=3, effectiveWeight=3, currentWeight=6}

为了方便分析,简化两线程执行后的结果

[当前权重]值的变化:[1,3,2] -> [1,-3,2] -> [2,0,4]

[当前权重]值的变化:[2,0,4] -> [2,0,-2] -> [3,3,0]

[当前权重]值的变化:[3,3,0] -> [3,-3,0] -> [4,0,2]

[当前权重]值的变化:[4,0,2] -> [-2,0,2] -> [-1,3,4]

[当前权重]值的变化:[-1,3,4] -> [-1,3,-2] -> [0,6,0]

[当前权重]值的变化:[0,6,0] -> [0,0,0] -> [1,3,2]

[当前权重]值的变化:[1,3,2] -> [1,-3,2] -> [2,0,4]

[当前权重]值的变化:[2,0,4] -> [2,0,-2] -> [3,3,0]

[当前权重]值的变化:[3,3,0] -> [3,-3,0] -> [4,0,2]

[当前权重]值的变化:[4,0,2] -> [-2,0,2] -> [-1,3,4]

[当前权重]值的变化:[-1,3,4] -> [-1,3,-2] -> [0,6,0]

[当前权重]值的变化:[0,6,0] -> [0,0,0] -> [1,3,2]

因为整个过程只有当前权重发生变化,所以分析清楚它就明白了整个过程。

结论:

分配完成后当前权重发生变化,但权限之和还是等于最初值

每6轮(1+3+2权重)就出现权重全部为0,所以会出现重新循环,6正好等于权重之和,权重比等于1/6 : 3/6 : 2/6;

a=权重1,b=权重3,c=权重2,那么权重变化的6(a+b+c)次中,分配情况为:b c b a c b,很明显,每个节点均匀按权重分配,节点分配不再是连续的。这也是最重要的结论,正是实现方式二在文初提到的要实现的关键点。

该算法在权重比相差很大时,比如:A=1,B=5,那这个算法的结果就跟方式一没啥区别了,分配结果就变成了:{A,B,B,B,B,B},既然没区别,那根据算法复杂情况,那肯定方式一更好了,所以方式一和方式二可以互补,可以根据权重比选择不同的算法。

留下悬念

第一点:节点出现分配失败时降低有效权重值,成功时提高有效权重值(但不能大于weight值)的功能。理解了方式二,后面再加这块功能进去就很好理解了;

第二点:该算法实现的背后数学证明,用的是什么数学理论?

总结

到此这篇关于Java负载均衡算法实现之轮询和加权轮询的文章就介绍到这了,更多相关Java轮询和加权轮询算法内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号