负载均衡算法详解与实践

项目github地址:bitcarmanlee easy-algorithm-interview-and-practice
欢迎大家star,留言,一起学习进步

负载均衡算法在实际中使用及其广泛,比如某个大型网站或者某个大型服务,服务器的数量可能成百上千台,这个时候是一定会有负载均衡算法的。现在总结一下常见的几种负载均衡算法。

1.轮询法(RoundRobin)

轮询法是一种比较简单的算法,也比较好理解。假设后台有N台服务器,那么来请求以后,按照请求到达的先后顺序,将流量分配到服务器上,这样后端服务器上的流量都是相同的。这种方式的好处是不用关注服务器本身的负载,链接数等信息。但是坏处也比较明显,如果每台服务器的处理能力不一样,那比较强的服务器就无法发挥优势。Nginx中默认的就是轮询法。

import java.util.Arrays;
import java.util.List;

/**
 * Created by WangLei on 18-6-21.
 */
public class RoundRobin {

    private static List<String> servers = Arrays.asList("192.168.0.1","192.168.0.2","192.168.0.3","192.168.0.4");
    private static Integer pos = 0;

    public static String getServer() {
        String server = null;

        synchronized (pos) {
            if(pos == servers.size()) {
                pos = 0;
            }
            server = servers.get(pos);
            pos++;
        }

        return server;
    }

    public static void main(String[] args) {
        for(int i=0; i<10; i++) {
            System.out.println(RoundRobin.getServer());
        }
    }
}

运行结果:

192.168.0.1
192.168.0.2
192.168.0.3
192.168.0.4
192.168.0.1
192.168.0.2
192.168.0.3
192.168.0.4
192.168.0.1
192.168.0.2

代码里面需要注意的是对于pos参数的处理,因为实际中并发的场景很常见,所以用synchronized将pos包起来,这样保证pos每次只被一个线程修改。

2.随机法

随机的方式也比较容易理解。将流量随机分配到后端某一台服务器上。根据简单的统计理论可以得知,随着流量越来越大,随机分配的实际效果越来越接近平均分配,最终的实际效果跟轮询一致。

import java.util.Arrays;
import java.util.List;
import java.util.Random;

/**
 * Created by WangLei on 18-6-21.
 */
public class RandomMethod {

    private static List<String> servers = Arrays.asList("192.168.0.1","192.168.0.2","192.168.0.3","192.168.0.4");
    private static int pos = 0;

    public static String getServer() {
        String server = null;

        Random random = new Random();
        int randomPos = random.nextInt(servers.size());

        server = servers.get(randomPos);

        return server;
    }

    public static void main(String[] args) {
        for(int i=0; i<10; i++) {
            System.out.println(RandomMethod.getServer());
        }
    }

}
192.168.0.4
192.168.0.4
192.168.0.1
192.168.0.4
192.168.0.3
192.168.0.1
192.168.0.4
192.168.0.3
192.168.0.1
192.168.0.3

根据代码可以看出,在并发的情况下,随机方式不需要像轮询一样加锁,并发能力比轮询要强。

3.主备算法

这个算法核心的思想是将请求尽量的放到某个固定机器的服务上,而其他机器的服务则用来做备份,如果出现问题就切换到另外的某台机器的服务上。这么做的好处之一就是每个流量分配到哪个服务器上是固定的,在某些场合会比较方便。

比如我们下面实现一个简单的主备,根据客户端的ip将流量分配到固定的机器上。

import java.util.Arrays;
import java.util.List;

/**
 * Created by WangLei on 18-6-21.
 */
public class HashMethod {

    private static List<String> servers = Arrays.asList("192.168.0.1","192.168.0.2","192.168.0.3","192.168.0.4");
    private static int pos = 0;

    public static String getServer(String ip) {
        String server = null;

        int hashCode = ip.hashCode();
        pos = hashCode % servers.size();

        server = servers.get(pos);

        return server;
    }

    public static void main(String[] args) {
        String ip = "192.168.255.254";
        System.out.println(HashMethod.getServer(ip));
    }
}
192.168.0.3

4.一致性Hash

一致性Hash算法通过一个叫做一致性Hash环的数据结构实现Key到后端服务器的Hash映射。如果是缓存服务,实现的则是Key到缓存服务器的Hash映射。从网上找了一张示意图。
这里写图片描述

算法的具体逻辑如下:将[0,2^32)所有的整数投射到一个圆上,然后再将你的机器的唯一编码(比如:IP)通过hash运算得到的整数也投射到这个圆上(Node-A、Node-B)。如果一个请求来了,就将这个请求的唯一编码(比如:用户id)通过hash算法运算得到的整数也投射到这个圆上(request-1、request-2),通过顺时针方向,找到第一个对应的机器。

一致性Hash需要解决的是以下两个问题:
1、散列的不变性:就是同一个请求(比如:同一个用户id)尽量的落入到一台机器,不要因为时间等其他原因,落入到不同的机器上了;
2、异常以后的分散性:当某些机器坏掉(或者增加机器),原来落到同一台机器的请求(比如:用户id为1,101,201),尽量分散到其他机器,不要都落入其他某一台机器。这样对于系统的冲击和影响最小。
一致Hash算法用的最多的场景,就是分配cache服务。将某一个用户的数据缓存在固定的某台服务器上,那么我们基本上就不用多台机器都缓存同样的数据,这样对我们提高缓存利用率有极大的帮助。
(此部分来自https://blog.csdn.net/zgwangbo/article/details/51533657)

先看一个一致性Hash的简单实现版本。

import java.util.Arrays;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Created by WangLei on 18-6-21.
 */
public class ConsisentHashNoVirtualNode {

    private static String[] serversarray = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111","192.168.0.3:111", "192.168.0.4:111"};
    private static List<String> servers = Arrays.asList(serversarray);

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

    static {
        for(int i=0; i<servers.size(); i++) {
            int hash = hashCode(servers.get(i));
            System.out.println(servers.get(i) + " join collections, hashcode is: " + hash);
            sortedMap.put(hash, servers.get(i));
        }
        System.out.println();
    }

	// 重写hashcode方法,使用FNV1_32_HASH算法,让节点在hash环上分布更均匀。
    private static int hashCode(String ip) {
        final int p = 16777619;
        int hash = (int)2166136261L;
        for(int i=0; i<ip.length(); i++) {
            hash = (hash ^ ip.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 = hashCode(client);
        // 得到大于该Hash值的所有Map。注意有可能subMap一个值没有,此时默认分给第一个server。
        SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
        if(subMap != null && subMap.size() > 0) {
            int i = subMap.firstKey();
            return subMap.get(i);
        } else {
            return sortedMap.get(hashCode(servers.get(0)));
        }
    }

    public static void main(String[] args) {
        String[] clients =  {"127.0.0.1:22", "221.226.0.1:80", "10.211.0.1:8080"};
        //String[] clients =  {"127.0.0.1:22", "221.226.0.1:80"};

        for(int i=0; i<clients.length; i++) {
            System.out.println(clients[i] + " hashcode is: " + hashCode(clients[i]) + ", the router node is: " + getServer(clients[i]));
        }
    }
}

运行结果:

192.168.0.0:111 join collections, hashcode is: 575774686
192.168.0.1:111 join collections, hashcode is: 8518713
192.168.0.2:111 join collections, hashcode is: 1361847097
192.168.0.3:111 join collections, hashcode is: 1171828661
192.168.0.4:111 join collections, hashcode is: 1764547046

127.0.0.1:22 hashcode is: 1739501660, the router node is: 192.168.0.4:111
221.226.0.1:80 hashcode is: 99109700, the router node is: 192.168.0.0:111
10.211.0.1:8080 hashcode is: 1976495495, the router node is: 192.168.0.0:111

上面的一致性Hash算法实现,可以在很大程度上解决很多分布式环境下不好的路由算法导致系统伸缩性差的问题,但是会带来另外一个问题:负载不均。

比如说有Hash环上有A、B、C三个服务器节点,分别有100个请求会被路由到相应服务器上。现在在A与B之间增加了一个节点D,这导致了原来会路由到B上的部分节点被路由到了D上,这样A、C上被路由到的请求明显多于B、D上的,原来三个服务器节点上均衡的负载被打破了。某种程度上来说,这失去了负载均衡的意义,因为负载均衡的目的本身就是为了使得目标服务器均分所有的请求。

解决这个问题的办法是引入虚拟节点,其工作原理是:将一个物理节点拆分为多个虚拟节点,并且同一个物理节点的虚拟节点尽量均匀分布在Hash环上。采取这样的方式,就可以有效地解决增加或减少节点时候的负载不均衡的问题。

import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * Created by WangLei on 18-6-21.
 */
public class ConsisenthashWithVituralNode {

    private static String[] serversarray = {"192.168.0.1","192.168.0.2","192.168.0.3","192.168.0.4"};

    private static List<String> realservers = new LinkedList<>();

    private static final int VIRTUAL_NUM = 5;

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

    static {
        for(int i=0; i<serversarray.length; i++) {
            realservers.add(serversarray[i]);
        }
        //双层循环,每个实际节点都添加对应的虚拟节点
        for(String ip : realservers) {
            for(int i = 0; i < VIRTUAL_NUM; i++) {
                String virtualNode = ip + "#" + String.valueOf(i);
                int hash = hashCode(virtualNode);
                System.out.println("virtual node of " + virtualNode + " add, hash is: " + hash);
                sortedMap.put(hash, virtualNode);
            }
        }
        System.out.println();
    }


    private static int hashCode(String input) {
        final int p = 16777619;
        int hash = (int)2166136261L;
        for(int i=0; i<input.length(); i++) {
            hash = (hash ^ input.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) {
        String virtualNode = null;
        int hash = hashCode(client);

        SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
        if(subMap != null && subMap.size() > 0) {
            int i = subMap.firstKey();
            virtualNode = subMap.get(i);
        } else {
            virtualNode = sortedMap.get(serversarray[0]);
        }
        return virtualNode.substring(0, virtualNode.indexOf("#"));
    }

    public static void main(String[] args) {
        String[] clients =  {"127.0.0.1:22", "110.226.0.1:80", "255.21.0.1:8080"};
        for(int i=0; i<clients.length; i++) {
            System.out.println(clients[i] + " hashcode is: " + hashCode(clients[i]) + " the router node is: " + getServer(clients[i]));
        }
    }
}

运行结果:

virtual node of 192.168.0.1#0 add, hash is: 1267794962
virtual node of 192.168.0.1#1 add, hash is: 1405473402
virtual node of 192.168.0.1#2 add, hash is: 520282580
virtual node of 192.168.0.1#3 add, hash is: 902681916
virtual node of 192.168.0.1#4 add, hash is: 705135863
virtual node of 192.168.0.2#0 add, hash is: 938864723
virtual node of 192.168.0.2#1 add, hash is: 697737174
virtual node of 192.168.0.2#2 add, hash is: 758522939
virtual node of 192.168.0.2#3 add, hash is: 1305037326
virtual node of 192.168.0.2#4 add, hash is: 1002536378
virtual node of 192.168.0.3#0 add, hash is: 392944983
virtual node of 192.168.0.3#1 add, hash is: 2046386183
virtual node of 192.168.0.3#2 add, hash is: 1604877649
virtual node of 192.168.0.3#3 add, hash is: 95135893
virtual node of 192.168.0.3#4 add, hash is: 2030051947
virtual node of 192.168.0.4#0 add, hash is: 1614569944
virtual node of 192.168.0.4#1 add, hash is: 744068005
virtual node of 192.168.0.4#2 add, hash is: 1628537157
virtual node of 192.168.0.4#3 add, hash is: 537291676
virtual node of 192.168.0.4#4 add, hash is: 1173079769

127.0.0.1:22 hashcode is: 1739501660 the router node is: 192.168.0.3
110.226.0.1:80 hashcode is: 1320900860 the router node is: 192.168.0.1
255.21.0.1:8080 hashcode is: 1187650066 the router node is: 192.168.0.1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值