Java实现负载均衡算法–轮询和加权轮询

1.普通轮询算法

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

代码实现:

/**
 * 普通轮询算法
 */
public class RoundRobin {
    private static Integer index = 0;
    private static List nodes = new ArrayList<>();
    // 记录轮询输出结果
    private static StringBuffer stringBuffer = new StringBuffer();
    // 准备模拟数据
    static {
        nodes.add("192.168.1.101");
        nodes.add("192.168.1.103");
        nodes.add("192.168.1.102");
        System.out.println("普通轮询算法的所有节点:"+nodes);//打印所有节点
    }

    // 关键代码
    public String selectNode(){
        String ip = null;
//      之前写错的代码
//      synchronized (index){
        synchronized (RoundRobin.class){
            // 下标复位
            if(index>=nodes.size()) index = 0;
            ip = nodes.get(index);
            stringBuffer.append(Thread.currentThread().getName()+"==获取节点:"+ ip +"\n");
            index++;
        }
        return ip;
    }

    // 并发测试:两个线程循环获取节点
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            RoundRobin roundRobin1 = new RoundRobin();
            for (int i=1;i {
            RoundRobin roundRobin1 = new RoundRobin();
            for (int i=1;i

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

普通轮询算法的所有节点:[192.168.1.101, 192.168.1.103, 192.168.1.102]
Thread-0==获取节点:192.168.1.101
Thread-1==获取节点:192.168.1.103
Thread-1==获取节点:192.168.1.102
Thread-0==获取节点:192.168.1.101
Thread-1==获取节点:192.168.1.103
Thread-0==获取节点:192.168.1.102

2.加权轮询算法

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

将需要轮询的所有节点 按权重数循环生成一个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”}

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

代码实现:

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

/**
 *  简单版的加权轮询
 */public class WeightedRoundRobinSimple {
    private static Integer index = 0;
    private static Map mapNodes = new HashMap<>();
    // 记录轮询输出结果
    private static StringBuffer stringBuffer = new StringBuffer();

    // 准备模拟数据
    static {
        mapNodes.put("192.168.1.101",1);
        mapNodes.put("192.168.1.102",3);
        mapNodes.put("192.168.1.103",2);
        /* -- 以下代码只为了方便查看所有节点,删除不影响 -- S */
        List nodes = new ArrayList<>();
        Iterator> iterator = mapNodes.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry entry = iterator.next();
            String key = entry.getKey();
            for (int i=0;i nodes = new ArrayList<>();
        Iterator> iterator = mapNodes.entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry entry = iterator.next();
            String key = entry.getKey();
            for (int i=0;i=nodes.size()) index = 0;
            ip = nodes.get(index);
            stringBuffer.append(Thread.currentThread().getName()+"==获取节点:"+ ip +"\n");
            index++;
        }
        return ip;
    }

    // 并发测试:两个线程循环获取节点
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            WeightedRoundRobinSimple roundRobin1 = new WeightedRoundRobinSimple();
            for (int i=1;i {
            WeightedRoundRobinSimple roundRobin1 = new WeightedRoundRobinSimple();
            for (int i=1;i

执行结果:两个线程循环测试,输出结果会出现交替分配到不同的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]
Thread-0==获取节点:192.168.1.103
Thread-1==获取节点:192.168.1.103
Thread-1==获取节点:192.168.1.101
Thread-0==获取节点:192.168.1.102
Thread-0==获取节点:192.168.1.102
Thread-1==获取节点:192.168.1.102
Thread-0==获取节点:192.168.1.103
Thread-1==获取节点:192.168.1.103
Thread-1==获取节点:192.168.1.101
Thread-0==获取节点:192.168.1.102
Thread-0==获取节点:192.168.1.102
Thread-1==获取节点:192.168.1.102

本文的重点难点。

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

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

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

关键参数

  • 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

你面前有三个瓶子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

下面通过阅读代码来理解

节点对象

/**
 * String ip:负载IP
 * final Integer weight:权重,保存配置的权重
 * Integer effectiveWeight:有效权重,轮询的过程权重可能变化
 * Integer currentWeight:当前权重,比对该值大小获取节点
 *   第一次加权轮询时:currentWeight = weight = effectiveWeight
 *   后面每次加权轮询时:currentWeight 的值都会不断变化,其他权重不变
 */public class Node implements Comparable{
    private String ip;
    private final Integer weight;
    private Integer effectiveWeight;
    private Integer currentWeight;

    public Node(String ip,Integer weight){
        this.ip = ip;
        this.weight = weight;
        this.effectiveWeight = weight;
        this.currentWeight = weight;
    }

    public Node(String ip, Integer weight, Integer effectiveWeight, Integer currentWeight) {
        this.ip = ip;
        this.weight = weight;
        this.effectiveWeight = effectiveWeight;
        this.currentWeight = currentWeight;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public Integer getWeight() {
        return weight;
    }

    public Integer getEffectiveWeight() {
        return effectiveWeight;
    }

    public void setEffectiveWeight(Integer effectiveWeight) {
        this.effectiveWeight = effectiveWeight;
    }

    public Integer getCurrentWeight() {
        return currentWeight;
    }

    public void setCurrentWeight(Integer currentWeight) {
        this.currentWeight = currentWeight;
    }

    @Override
    public int compareTo(Node node) {
        return currentWeight > node.currentWeight ? 1 : (currentWeight.equals(node.currentWeight) ? 0 : -1);
    }

    @Override
    public String toString() {
        return "{ip='" + ip + "', weight=" + weight + ", effectiveWeight=" + effectiveWeight + ", currentWeight=" + currentWeight + "}";
    }
}

加权轮询算法

/**
 * 加权轮询算法
 */public class WeightedRoundRobin {

    private static List nodes = new ArrayList<>();
    // 权重之和
    private static Integer totalWeight = 0;
    // 准备模拟数据
    static {
        nodes.add(new Node("192.168.1.101",1));
        nodes.add(new Node("192.168.1.102",3));
        nodes.add(new Node("192.168.1.103",2));
        nodes.forEach(node -> totalWeight += node.getEffectiveWeight());
    }

    /**
     * 按照当前权重(currentWeight)最大值获取IP
     * @return Node
     */
    public Node selectNode(){
        if (nodes ==null || nodes.size() 0 ? tempNodeOfMaxWeight : node;
            }
            // 必须new个新的节点实例来保存信息,否则引用指向同一个堆实例,后面的set操作将会修改节点信息
            nodeOfMaxWeight = new Node(tempNodeOfMaxWeight.getIp(),tempNodeOfMaxWeight.getWeight(),tempNodeOfMaxWeight.getEffectiveWeight(),tempNodeOfMaxWeight.getCurrentWeight());

            // 调整当前权重比:按权重(effectiveWeight)的比例进行调整,确保请求分发合理。
            tempNodeOfMaxWeight.setCurrentWeight(tempNodeOfMaxWeight.getCurrentWeight() - totalWeight);
            sb.append(" -> "+printCurrentWeight(nodes));

            nodes.forEach(node -> node.setCurrentWeight(node.getCurrentWeight()+node.getEffectiveWeight()));

            sb.append(" -> "+printCurrentWeight(nodes));
            System.out.println(sb); //打印权重变化过程
        }
        return nodeOfMaxWeight;
    }

    // 格式化打印信息
    private String printCurrentWeight(List nodes){
        StringBuffer stringBuffer = new StringBuffer("[");
        nodes.forEach(node -> stringBuffer.append(node.getCurrentWeight()+",") );
        return stringBuffer.substring(0, stringBuffer.length() - 1) + "]";
    }

    // 并发测试:两个线程循环获取节点
    public static void main(String[] args){
        Thread thread = new Thread(() -> {
            WeightedRoundRobin weightedRoundRobin1 = new WeightedRoundRobin();
            for(int i=1;i

执行结果:

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值)的功能。理解了方式二,后面再加这块功能进去就很好理解了;

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

Original: https://www.cnblogs.com/dennyLee2025/p/16128477.html
Author: 渊渟岳
Title: Java实现负载均衡算法–轮询和加权轮询

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/620476/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 树链剖分

    树链剖分 将树转化为线性序列,便于维护树上信息。 include: 重链剖分,长链剖分,实链剖分。 注:此文默认读者已经熟悉线段树的基本操作,不熟悉这可以先看这个:线段树 重链剖分…

    数据结构和算法 2023年6月12日
    0114
  • 一些C/C++的小问题

    回调函数与普通函数的区别 回调函数即函数指针来实现,可实现多态效果;信号处理函数一般也设置成回调,当事件发生,通过回调函数通知。 函数返回局部变量时的一些优化: 编译器的NRV优化…

    数据结构和算法 2023年6月8日
    0140
  • 关于nginx 和 uwsgi

    关于nginx和uWSGI和Django之间的关系,我觉得要理一下。 原文链接 为什么要用nginx 因为我们要使用https协议访问。(y总说django不支持,但是我查了一下,…

    数据结构和算法 2023年6月12日
    068
  • lua:三元运算符

    lua里面没有类似C++的三元运算符 a?b:c。 第一种实现 if-else — 三元运算符 function iif(condition, a, b) if conditio…

    数据结构和算法 2023年6月7日
    085
  • CentOS系统修改时区

    查看当前时区 ls -l /etc/localtime 我目前这台服务器的时区时美国纽约时间 查看时区选择列表 tzselect 执行 tzselect后会出现上图的选择内容,我们…

    数据结构和算法 2023年6月8日
    098
  • 有序数组构造平衡树 1、因为是有序数组,所以我们要取数组中间的数作为根节点2、接下来我们可以将数组分成左右两块3、根的左节点为左边的数组的中间的值,而根的右节点为右边数组的中间的值…

    数据结构和算法 2023年6月7日
    0106
  • SpringMVC笔记

    SpringMVC 一、SpringMVC介绍 1.什么是MVC 是一种软件架构的思想,将软件按照模型、视图、控制器来划分 M:Model,模型层,指工程中的JavaBean,作用…

    数据结构和算法 2023年6月7日
    093
  • Linux下Redis的安装

    配置⽂件⽬录为/usr/local/redis/redis.conf sudo cp /usr/local/redis/redis.conf /etc/redis/ Origina…

    数据结构和算法 2023年6月7日
    0100
  • 博客园主题第二弹

    主题备份&分享 页面定制css 点击查看代码 :root{–color-basic-1000: #f7f2a0;–color-basic-1100: #000;} :r…

    数据结构和算法 2023年6月7日
    0117
  • 做题记录2022.4.29洛谷P1631序列合并

    可以很容易地想出暴力思路,但复杂度高达O(n2),所以必须优化 思路1:不难发现在a数组到i,b数组到j处,在它们前面的有ij个,这ij个数不可能比它大,所以只要在暴力枚举过程中判…

    数据结构和算法 2023年6月12日
    091
  • G&GH05 删除文件和.gitignore

    平台: Windows 10 学习笔记 转载请注明出处 欢迎留言 本系列文章是 git & github 的入门教程. 本系列文章优势: 实际开发过程中, 我们经常会希望不…

    数据结构和算法 2023年6月12日
    091
  • 原型模式(创建型)

    原型模式 介绍 定义:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。 简单理解,就是当需要创建一个指定的对象时,我们刚好有一个这样的对象,但是又…

    数据结构和算法 2023年6月8日
    0166
  • 2022.7.21 特殊矩阵压缩

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    数据结构和算法 2023年6月8日
    093
  • 查找算法-插值查找算法

    插值查找算法 插值查找原理介绍: 插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。 将折半查找中的求 mid 索引的公式 , low 表示左边索引 l…

    数据结构和算法 2023年6月12日
    085
  • 洛谷P2002 消息扩散(tarjan缩点)

    题目链接:https://www.luogu.com.cn/problem/P2002 思路:由于图中每个强连通分量(scc)中的点是可以互相到达的,所以我们可以用tarjan求图…

    数据结构和算法 2023年6月8日
    096
  • 「比赛游记」2022省选游记

    2023/03/30:我写的什么玩意啊,你们就当黑历史看吧。 突然得知要参加省选。 就当长见识了吧(反正我才初一 打 NOI Online,由于服务器太垃圾没把程序交上去。 然后初…

    数据结构和算法 2023年6月8日
    0121
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球