负载均衡算法(notes)

常用的负载均衡算法包括轮询法(round robin)随机法源地址哈希法加权轮询法加权随机法最小连接数算法,sticky

这里初始化一个serverWeightMap的Map变量来表示服务器地址和权重的映射,以此来模拟轮询算法的实现,其中设置的权重值在后面加权算法会使用到

public static Map<String, Integer> serverWeightMap = Maps.newHashMap();
static Integer pos = 0;
static {
    serverWeightMap.put("192.168.1.100", 1);
    serverWeightMap.put("192.168.1.101", 1);
    serverWeightMap.put("192.168.1.102", 4);
    serverWeightMap.put("192.168.1.103", 1);
    serverWeightMap.put("192.168.1.104", 1);
    serverWeightMap.put("192.168.1.105", 3);
    serverWeightMap.put("192.168.1.106", 1);
    serverWeightMap.put("192.168.1.107", 2);
    serverWeightMap.put("192.168.1.108", 1);
    serverWeightMap.put("192.168.1.109", 1);
    serverWeightMap.put("192.168.1.110", 1);
}

轮询法

将请求按顺序轮流地分配的后端服务器上,它均衡的对待后端每一台服务器,而不关心服务器实际的连接数和当前的系统负载

public static String getByRoundRobin() {
    Map<String, Integer> serverMap = Maps.newHashMap();
    serverMap.putAll(serverWeightMap);
    Set<String> keySet = serverMap.keySet();
    List<String> keyList = Lists.newArrayList();
    keyList.addAll(keySet);

    String server = null;
    synchronized (pos) {
        if (pos >= keySet.size()) {
            pos = 0;
        }

        server = keyList.get(pos);
        pos++;
    }
    return server;
}

由于serverWeightMap中的地址列表是动态的,随时可能有机器上线,下线或者宕机,因此,为了避免可能出现的并发问题,如数组越界,通过新建方法内的局部变量serverMap,先将域变量复制到线程本地,以避免被多个线程修改,这样可能会引入新的问题,复制以后serverWeightMap的修改将无法反映给serverMap,也就是说,在这一轮选择服务器的过程中,新增服务器或者下线服务器,负载均衡算法中将无法获知,新增比较好处理,而当服务器下线或者宕机时,服务消费者将有可能访问到不存在的地址。因此,在服务消费者的实现端需要考虑该问题,并且进行相应的容错处理,比如重新发起一次调用。

对于当前轮询的位置变量pos,为了保证服务器选择的顺序性,需要在操作时对其加上synchronized锁,使得在同一时刻只有一个线程能够修改pos的值,否则当pos变量被并发修改,则无法保证服务器选择的顺序性,甚至有可能导致keyList数组越界。

使用轮询策略的目的在于,希望做到请求转移的绝对均衡,但付出的代价也是相当大的。为了pos保证变量修改的互斥性,需要引入重量级的悲观锁synchronized,将会导致该段轮询代码的并发吞吐量发生明显的下降。

随机法

通过系统随机函数,根据后端服务器列表的大小值来随机的选择其中一台进行访问。由概率统计理论可以得知,随着调用量的增大,其实际效果越来越接近于平均分配流量到没一台后端服务器,也就是轮询的效果。

public static String getByRondom() {
    Map<String, Integer> serverMap = Maps.newHashMap();
    serverMap.putAll(serverWeightMap);
    Set<String> keySet = serverMap.keySet();
    List<String> keyList = Lists.newArrayList();
    keyList.addAll(keySet);

    String server = null;

    Random random = new Random();
    int randomPos = random.nextInt(keySet.size());
    server = keyList.get(randomPos);
    return server;
}

基于概率统计的理论,吞吐量越大,随机算法的效果越接近轮询算法的效果。因此,你还会考虑一定要使用需要付出一定代价的轮询算法么?

源地址哈希法

源地址哈希的思想是获取客户端访问的ip地址值,通过哈希函数计算得到一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是要访问的服务器的序号。采用哈希法进行负载均衡,同一个ip地址的客户端,当后端服务器列表不变时,它每次都会被映射到同一台后端服务器进行访问。

public static String getServerByConsumerHash(String remoteIp) {
    Map<String, Integer> serverMap = Maps.newHashMap();
    serverMap.putAll(serverWeightMap);
    Set<String> keySet = serverMap.keySet();
    List<String> keyList = Lists.newArrayList();
    keyList.addAll(keySet);

    String server = null;

    int hashCode = remoteIp.hashCode();
    int serverListSize = keyList.size();
    int serverPos = hashCode % serverListSize;
    server = keyList.get(serverPos);

    return server;
}

通过参数传入的客户端remoteip参数,取得它的哈希值,对服务器列表的大小取模,结果便是选用的服务器在服务器列表中的索引值。该算法保证了相同的客户端ip地址将会被“哈希”到同一台后端服务器,直到后端服务器列表变更。根据此特性可以在服务消费者和服务提供者之间建立有状态的session会话。

加权轮询法

不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此他们的抗压能力也不尽相同,给配置高,负载低的机器配置更高的权重,让其处理更过的请求,而地配置,负载高的机器,则给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

public static String getByWeightRoundRobin() {
    Map<String, Integer> serverMap = Maps.newHashMap();
    serverMap.putAll(serverWeightMap);
    Set<String> keySet = serverMap.keySet();
    Iterator<String> it = keySet.iterator();
    List<String> serverList = Lists.newArrayList();

    while (it.hasNext()) {
        String server = it.next();
        Integer weight = serverMap.get(server);
        for (int i = 0; i < weight; i++) {
            serverList.add(server);
        }
    }
    String server = null;
    synchronized (pos) {
        if (pos >= serverList.size()) {
            pos = 0;
        }

        server = serverList.get(pos);
    pos ++;    }

    return server;
}

与轮询算法类似,只是在获取服务器地址之前增加了一段权重计算的代码,根据权重的大小,将地址重复增加到服务器地址列表中,权重越大,该服务器每轮所获得的请求数量越多。

加权随机法

public static String getByWeightRandom() {
    Map<String, Integer> serverMap = Maps.newHashMap();
    serverMap.putAll(serverWeightMap);
    Set<String> keySet = serverMap.keySet();
    Iterator<String> it = keySet.iterator();
    List<String> serverList = Lists.newArrayList();

    while (it.hasNext()) {
        String server = it.next();
        Integer weight = serverMap.get(server);
        for (int i = 0; i < weight; i++) {
            serverList.add(server);
        }
    }
    String server = null;
    Random random = new Random();
    int randomPos = random.nextInt(serverList.size());
    server = serverList.get(randomPos);
    return server;
}

最小连接数算法

最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它正是根据后端服务器当前的链接情况,动态的选取其中积压连接数最少的一台服务器来处理当前请求,尽可能的提高后端服务器的利用效率,将负载合理的分流到每一台机器。

sticky

保证始终只在一台处理,如果这台服务器挂了,会自动切换到下一台上。

一致性hash

主要用于分布式缓存集群,避免扩容或宕机带来的命中率急剧下降.

先构造一个长度为2^32的整数环(这个环被称作一致性hash环)根据节点名称的hash值(其分布范围为[0,0^32 -1]),将缓存服务器节点放置在这个Hash环上,然后根据需要缓存的数据的Key值计算得到其Hash值(其 分布范围也同样为[0,2^32-1]),然后在Hash环上顺时针查找距离这个Key的Hash值最近的缓存服务器节点,完成Key到服务器的Hash映射查找

这个长度为2^32的一致性Hash环通常使用二叉查找树实现,Hash查找过程实际上是在二叉查找树中查找小于查找树的最小数值,当然这个二叉树的最右边叶子节点和最左边的叶子节点相连接,构成环.

还有问题,新加入节点后,节点之间的数据负载是不一样的,所以又引入虚拟节点的概念,就是将物理缓存服务器虚拟为一组虚拟缓存服务器,分散在整个环上.

   public class ConsistentHash<T> {

       private final HashFunction hashFunction;
       private final int numberOfReplicas;
       private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();

       public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
                             Collection<T> nodes) {
           this.hashFunction = hashFunction;
           this.numberOfReplicas = numberOfReplicas;

           for (T node : nodes) {
               add(node);
           }
       }

       public void add(T node) {
           for (int i = 0; i < numberOfReplicas; i++) {
               circle.put(hashFunction.hash(node.toString() + i), node);
           }
       }

       public void remove(T node) {
           for (int i = 0; i < numberOfReplicas; i++) {
               circle.remove(hashFunction.hash(node.toString() + i));
           }
       }

       public T get(Object key) {
           if (circle.isEmpty()) {
               return null;
           }
           int hash = hashFunction.hash(key);
           if (!circle.containsKey(hash)) {
               SortedMap<Integer, T> tailMap = circle.tailMap(hash);
               hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
           }
           return circle.get(hash);
       }

   }

参考

[Consistent Hashing Blog]https://community.oracle.com/blogs/tomwhite/2007/11/27/consistent-hashing