我们在分布式系统常见负载均衡算法中对负载均衡及负载均衡算法进行了介绍,接下来我们用代码对常见的几种算法进行实现。

本文讲述的是”将外部发送来的请求均匀分配到对称结构中的某一台服务器上”的各种算法,并以Java代码演示每种算法的具体实现,OK,下面进入正题,在进入正题前,先写一个类来模拟Ip列表:

import java.util.HashMap;/**  * @author ashang.peng@aliyun.com  * @date 二月 07, 2017  */public class IpMap   {// 待路由的Ip列表,Key代表Ip,Value代表该Ip的权重public static HashMap<String, Integer> serverWeightMap =new HashMap<String, Integer>();static{serverWeightMap.put("192.168.1.100", 1);serverWeightMap.put("192.168.1.101", 1);// 权重为4serverWeightMap.put("192.168.1.102", 4);serverWeightMap.put("192.168.1.103", 1);serverWeightMap.put("192.168.1.104", 1);// 权重为3serverWeightMap.put("192.168.1.105", 3);serverWeightMap.put("192.168.1.106", 1);// 权重为2serverWeightMap.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);}
}

一、轮询(Round Robin)

轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。

其代码实现大致如下:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/**  * @author ashang.peng@aliyun.com  * @date 二月 07, 2017  */public class RoundRobin   {private static Integer pos = 0;public static String getServer(){// 重建一个Map,避免服务器的上下线导致的并发问题Map<String, Integer> serverMap =new HashMap<String, Integer>();serverMap.putAll(IpMap.serverWeightMap);// 取得Ip地址ListSet<String> keySet = serverMap.keySet();ArrayList<String> keyList = new ArrayList<String>();keyList.addAll(keySet);String server = null;synchronized (pos){if (pos > keySet.size())pos = 0;server = keyList.get(pos);pos ++;}return server;}
}

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

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

轮询法的优点在于:试图做到请求转移的绝对均衡。

轮询法的缺点在于:为了做到请求转移的绝对均衡,必须付出相当大的代价,因为为了保证pos变量修改的互斥性,需要引入重量级的悲观锁synchronized,这将会导致该段轮询代码的并发吞吐量发生明显的下降。

二、随机(Random)

通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,

其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。

随机法的代码实现大致如下:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/**  * @author ashang.peng@aliyun.com  * @date 二月 07, 2017  */class Random   {public static String getServer(){// 重建一个Map,避免服务器的上下线导致的并发问题   Map<String, Integer> serverMap =new HashMap<String, Integer>();serverMap.putAll(IpMap.serverWeightMap);// 取得Ip地址List   Set<String> keySet = serverMap.keySet();ArrayList<String> keyList = new ArrayList<String>();keyList.addAll(keySet);java.util.Random random = new java.util.Random();int randomPos = random.nextInt(keyList.size());return keyList.get(randomPos);}
}

整体代码思路和轮询法一致,先重建serverMap,再获取到server列表。在选取server的时候,通过Random的nextInt方法取0~keyList.size()区间的一个随机值,从而从服务器列表中随机获取到一台服务器地址进行返回。基于概率统计的理论,吞吐量越大,随机算法的效果越接近于轮询算法的效果。

三、源地址哈希(Hash)

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

源地址哈希算法的代码实现大致如下:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;/**  * @author ashang.peng@aliyun.com  * @date 二月 07, 2017  */class Hash      {public static String getServer(){// 重建一个Map,避免服务器的上下线导致的并发问题Map<String, Integer> serverMap =new HashMap<String, Integer>();serverMap.putAll(IpMap.serverWeightMap);// 取得Ip地址ListSet<String> keySet = serverMap.keySet();ArrayList<String> keyList = new ArrayList<String>();keyList.addAll(keySet);// 在Web应用中可通过HttpServlet的getRemoteIp方法获取String remoteIp = "127.0.0.1";int hashCode = remoteIp.hashCode();int serverListSize = keyList.size();int serverPos = hashCode % serverListSize;return keyList.get(serverPos);}
}

前两部分和轮询法、随机法一样就不说了,差别在于路由选择部分。通过客户端的ip也就是remoteIp,取得它的Hash值,对服务器列表的大小取模,结果便是选用的服务器在服务器列表中的索引值。

源地址哈希法的优点在于:保证了相同客户端IP地址将会被哈希到同一台后端服务器,直到后端服务器列表变更。根据此特性可以在服务消费者与服务提供者之间建立有状态的session会话。

源地址哈希算法的缺点在于:除非集群中服务器的非常稳定,基本不会上下线,否则一旦有服务器上线、下线,那么通过源地址哈希算法路由到的服务器是服务器上线、下线前路由到的服务器的概率非常低,如果是session则取不到session,如果是缓存则可能引发”雪崩”。

四、加权轮询(Weight Round Robin)

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

加权轮询法的代码实现大致如下:

import java.util.*;/**  * @author ashang.peng@aliyun.com  * @date 二月 07, 2017  */
class WeightRoundRobin   {private static Integer pos;public static String getServer(){// 重建一个Map,避免服务器的上下线导致的并发问题Map<String, Integer> serverMap =new HashMap<String, Integer>();serverMap.putAll(IpMap.serverWeightMap);// 取得Ip地址ListSet<String> keySet = serverMap.keySet();Iterator<String> iterator = keySet.iterator();List<String> serverList = new ArrayList<String>();while (iterator.hasNext()){String server = iterator.next();int weight = serverMap.get(server);for (int i = 0; i < weight; i++)serverList.add(server);}String server = null;synchronized (pos){if (pos > keySet.size())pos = 0;server = serverList.get(pos);pos ++;}return server;}
}

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

五、加权随机(Weight Random)

与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。

import java.util.*;/**  * @author ashang.peng@aliyun.com  * @date 二月 07, 2017  */class WeightRandom   {public static String getServer(){// 重建一个Map,避免服务器的上下线导致的并发问题Map<String, Integer> serverMap =new HashMap<String, Integer>();serverMap.putAll(IpMap.serverWeightMap);// 取得Ip地址ListSet<String> keySet = serverMap.keySet();Iterator<String> iterator = keySet.iterator();List<String> serverList = new ArrayList<String>();while (iterator.hasNext()){String server = iterator.next();int weight = serverMap.get(server);for (int i = 0; i < weight; i++)serverList.add(server);}java.util.Random random = new java.util.Random();int randomPos = random.nextInt(serverList.size());return serverList.get(randomPos);}
}

六、最小连接数法

前面我们费尽心思来实现服务消费者请求次数分配的均衡,我们知道这样做是没错的,可以为后端的多台服务器平均分配工作量,最大程度地提高服务器的利用率,但是,实际上,请求次数的均衡并不代表负载的均衡。因此我们需要介绍最小连接数法,最小连接数法比较灵活和智能,由于后台服务器的配置不尽相同,对请求的处理有快有慢,它正是根据后端服务器当前的连接情况,动态的选取其中当前积压连接数最少的一台服务器来处理当前请求,尽可能的提高后台服务器利用率,将负载合理的分流到每一台服务器。

import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;/*** @author zhibo* @date 2019/5/16 16:28*/
public class LeastBalance {public static String getServer() {Map<String, ConnectionsServer> serverMap = new TreeMap<>(ConnectionsServerManager.serverMap);Iterator<String> iterator = serverMap.keySet().iterator();ConnectionsServer minConnectionsServer = null;while (iterator.hasNext()){ConnectionsServer server = serverMap.get(iterator.next());if(minConnectionsServer == null){minConnectionsServer = server;}if(minConnectionsServer.getConnnections() > server.getConnnections()){minConnectionsServer = server;}}minConnectionsServer.setConnnections(minConnectionsServer.getConnnections() + 1);ConnectionsServerManager.serverMap.put(minConnectionsServer.getServer(), minConnectionsServer);System.out.println(String.format("ip=%s, connections=%s",minConnectionsServer.getServer(), minConnectionsServer.getConnnections()));return minConnectionsServer.getServer();}public static void main(String[] args) {for (int i = 0; i < 10; i++) {String server = getServer();}}
}

七、最小延迟(Latency-Aware)

与最小连接数类似,该方法也是为了让性能强的机器处理更多的请求,只不过最小连接数使用的指标是连接数,而该方法用的请求服务器的往返延迟(RTT),动态地选择延迟最低的节点处理当前请求。该方法的计算延迟的具体实现可以用EWMA算法来实现,它使用滑动窗口来计算移动平均耗时。具体代码如下:

public class EWMA {private static final long serialVersionUID = 2979391326784043002L;//时间类型枚举public static enum Time {MICROSECONDS(1),MILLISECONDS(1000),SECONDS(MILLISECONDS.getTime() * 1000),MINUTES(SECONDS.getTime() * 60),HOURS(MINUTES.getTime() * 60),DAYS(HOURS.getTime() * 24),WEEKS(DAYS.getTime() * 7);private long micros;private Time(long micros) {this.micros = micros;}public long getTime() {return this.micros;}}//三个alpha常量,这些值和Unix系统计算负载时使用的标准alpha值相同public static final double ONE_MINUTE_ALPHA = 1 - Math.exp(-5d / 60d / 1d);public static final double FIVE_MINUTE_ALPHA = 1 - Math.exp(-5d / 60d / 5d);public static final double FIFTEEN_MINUTE_ALPHA = 1 - Math.exp(-5d / 60d / 15d);private long window;private long alphaWindow;private long last;private double average;private double alpha = -1D;private boolean sliding = false;private long requests;//请求量private double weight;//权重public EWMA() {}public EWMA sliding(double count, Time time) {return this.sliding((long) (time.getTime() * count));}public EWMA sliding(long window) {this.sliding = true;this.window = window;return this;}public EWMA withAlpha(double alpha) {if (!(alpha > 0.0D && alpha <= 1.0D)) {throw new IllegalArgumentException("Alpha must be between 0.0 and 1.0");}this.alpha = alpha;return this;}public EWMA withAlphaWindow(long alphaWindow) {this.alpha = -1;this.alphaWindow = alphaWindow;return this;}public EWMA withAlphaWindow(double count, Time time) {return this.withAlphaWindow((long) (time.getTime() * count));}/*** 默认使用当前时间更新移动平均值*/public void mark(){mark(System.currentTimeMillis());}/*** 更新移动平均值* @param time*/public void mark(long time){if(this.sliding){//如果发生时间间隔大于窗口,则重置滑动窗口if(time-this.last > this.window){this.last = 0;}}if(this.last == 0){this.average = 0;this.requests = 0;this.last = time;}// 计算上一次和本次的时间差long diff = time - this.last;// 计算alphadouble alpha = this.alpha != -1.0 ? this.alpha : Math.exp(-1.0*((double)diff/this.alphaWindow));// 计算当前平均值this.average = (1.0-alpha)*diff + alpha*this.average;this.last = time;// 请求量加1this.requests++;// 计算权重值// this.weight = this.average != 0 ? this.requests/this.average : -1;}//返回mark()方法多次调用的平均值public double getAverage() {return this.average;}//按照特定的时间单位来返回平均值,单位详见Time枚举public double getAverageIn(Time time) {return this.average == 0.0 ? this.average : this.average / time.getTime();}//返回特定时间度量内调用mark()的频率public double getAverageRatePer(Time time) {return this.average == 0.0 ? this.average : time.getTime() / this.average;}//返回mark()方法多次调用的权重值public double getWeight() {return this.weight;}public long getRequests() {return this.requests;}public static   void main(String[] args) {//建立1分钟滑动窗口EWMA实例EWMA ewma = new EWMA().sliding(1.0, Time.MINUTES).withAlpha(EWMA.ONE_MINUTE_ALPHA).withAlphaWindow(1.0, EWMA.Time.MINUTES);int randomSleep = 0;long markVal = System.currentTimeMillis() * 1000;//单位为微秒try {ewma.mark(markVal);for (int i = 1; i <= 10000000; i++) {randomSleep = util.get_rand_32() % 1500;markVal += randomSleep;ewma.mark(markVal);if (i % 1000 == 0) {System.out.println("Round: " + i + ", Time: " + randomSleep+ ", Requests: " + ewma.getRequests()+ ", Average: " + ewma.getAverage()+ ", Weight:" + ewma.getWeight());}}}catch (Exception e) {e.printStackTrace();}}
}

Twitter的负载均衡算法基于这种思想,不过实现起来更加简单,即P2C算法。首先随机选取两个节点,在这两个节点中选择延迟低,或者连接数小的节点处理请求,这样兼顾了随机性,又兼顾了机器的性能,实现很简单。


我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!

参考资料:

  1. https://blog.csdn.net/weixin_34258782/article/details/90389607
  2. https://blog.csdn.net/okiwilldoit/article/details/81738782
  3. https://blog.csdn.net/claram/article/details/90290397

算法高级(13)-常见负载均衡算法Java代码实现相关推荐

  1. 算法高级(12)-分布式系统常见负载均衡算法

    负载均衡这个话题比较大,一篇估计说不完,今天先来第一篇. 一.负载均衡介绍 [百度百科]负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)进行平衡.分摊到多个操作单元上进行运 ...

  2. 分布式系统常见负载均衡算法

    一.概要 随着系统日益庞大.逻辑业务越来越复杂,系统架构由原来的单一系统到垂直系统,发展到现在的分布式系统.分布式系统中,可以做到公共业务模块的高可用,高容错性,高扩展性,然而,当系统越来越复杂时,需 ...

  3. 负载均衡轮询算法和服务器性能,负载均衡算法

    对于要实现高性能集群,选择好负载均衡器很重要,同时针对不同的业务场景选择合适的负载均衡算法也是非常重要的. 一.负载均衡算法分类 任务平分类 负载均衡系统将收到的任务平均分配给服务器进行处理,这里的& ...

  4. Java实现基于Socket的负载均衡代理服务器(含六种负载均衡算法)

    目录 前言 一.常见负载均衡算法 1.完全轮询算法 2.加权轮询算法 3.完全随机算法 4.加权随机算法 5.余数Hash算法 6.一致性Hash算法 二.代码实现 1.项目结构 2.代码实现 总结 ...

  5. 详解【负载均衡】(负载均衡算法、一致性hash、负载均衡架构分析)

    作者:duktig 博客:https://duktig.cn 优秀还努力.愿你付出甘之如饴,所得归于欢喜. 本文源码参看:https://github.com/duktig666/distribute ...

  6. 后宫佳丽三千,假如古代皇帝也懂负载均衡算法...

    古代皇帝,后宫佳丽三千,没法做到雨露均沾,但为了繁衍后代,子嗣繁盛,弱水三千,只取一瓢饮显然是不行的.不同的朝代有不同的宠幸妃子的方法,著名的有羊车望幸.掷筛侍寝.翻牌悬灯等等.如果皇帝懂得负载均衡算 ...

  7. 互联网研发中负载均衡算法一点探索

    负载均衡在线上服务中有着很重要作用,因为一台web服务比如tomcat,能够处理qps(每秒处理请求数) 是有限的.那么就需要有有前端负载均衡服务将大的流量分发为多个后端服务进行处理. 负载均衡产品有 ...

  8. 负载均衡算法及其Java代码实现

    负载均衡算法及其Java代码实现 什么是负载均衡 负载均衡,英文 名称为Load Balance,指由多台服务器以对称的方式组成一个服务器集合,每台服务器都具有等价的地位,都可以单独对外提供服务而无须 ...

  9. Java实现5种负载均衡算法

    Java实现5种负载均衡算法 1. 轮询算法 import com.google.common.collect.Lists;import java.util.List; import java.uti ...

最新文章

  1. RequireJS示例
  2. SAP S4HANA 使用BP创建供应商报错 - You cannot create a vendor with grouping G001 - 对策
  3. CCF发布2020-2021中国计算机科学技术发展报告
  4. Thymeleaf 学习笔记 (5)
  5. 精通python能干什么-转行做数据分析,是否需要精通python?
  6. android gpio操作
  7. Windows安装Python3
  8. 2021-11-28
  9. MapReduce案例-wordcount-Reduce阶段代码
  10. Opportunity creation case in Firebug
  11. 这届清华学生太难了!C++作业难到上热搜!
  12. hibernate 三种状态的转换
  13. 汇编语言0AH功能调用中显示字符出错
  14. Bootstrap--圆角图片`圆形图
  15. 无法从服务器获得响应,什么是java.io.EOFException的,消息:无法从服务器读取响应。 预期读4个字节,...
  16. idea快速搭建spring cloud-注册中心与注册
  17. vim 命令整理(自己常用)
  18. sql Sever的存储过程转换为mysql的
  19. 自动驾驶的“天眼”!聊一聊高精地图领域中所有主流的制作方案
  20. 网狐荣耀手机端内核源码

热门文章

  1. Egret 集成第三方库 记录
  2. PHP中两种包含文件方式、三种注释风格、四种标记风格
  3. eclipse项目中关于导入的项目里提示HttpServletRequest 不能引用的解决办法
  4. ubuntu 10.04   花屏   启动缓慢   处理办法
  5. Facebook热门应用被曝向第三方提供用户信息
  6. caffeine 弱引用key的实现
  7. java 某点 旋转_java-如何围绕某个点旋转顶点?
  8. Android开发中关于Fragments的内涵
  9. 认识计算机网络教案小学,初识计算机网络教案
  10. Raft -【go一致性算法】