既然稍微了解了一下spring-cloud IRule的算法,那我就去找那些大佬稍微学习了一下基础的几个负载均衡算法,现在来做一个记录,可能写的不是很好,毕竟算法这个东西有一种只可意会不可言传的感觉。
先来一个入门级算法吧
这个没啥好说的就只是简单随机一下就可以了。我们先编写一个ServerIps类来存放ip集合:
public class ServerIps { public static final List<String> IP_LIST = Arrays.asList( "192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5", "192.168.0.6", "192.168.0.7", "192.168.0.8", "192.168.0.9", "192.168.0.10" ); /** * key = ip * value = 权重占比 */ public static final Map<String,Integer> IP_MAP = new HashMap<String,Integer>(); static { IP_MAP.put("192.168.0.1",1); IP_MAP.put("192.168.0.2",8); IP_MAP.put("192.168.0.3",3); IP_MAP.put("192.168.0.4",6); IP_MAP.put("192.168.0.5",5); IP_MAP.put("192.168.0.6",5); IP_MAP.put("192.168.0.7",6); IP_MAP.put("192.168.0.8",7); IP_MAP.put("192.168.0.9",2); IP_MAP.put("192.168.0.10",9); } }新建一个RandomTest类
public class RandomTest { /** * 随机算法 * @return */ public static String getRandomIp(){ int ipAddres = new Random().nextInt(ServerIps.IP_LIST.size()); return ServerIps.IP_LIST.get(ipAddres); } public static void main(String[] args) { for(int i=0;i<10;i++){ System.out.println(getRandomIp()); } }运行一下main方法。简单是随机算法就完成了,是不是太简单了。不过呢,不要慌,这个只是最最最最简单的一个了,感觉都算不上算法,接下来我们稍微加一点难度吧。
我们正式环境的服务器可不一定是每一台都一致的,总有性能好的和性能稍微差一点,所以这就会涉及到一个服务器权重,所以接下来我们做一下权重随机算法: 先讲一下这里的思路,如图: 画图这个真不会,所以就简单的意思意思了一下,我们还是继续来写代码吧。看一下代码就知道咋回事儿了:
public class RandomTest { /** * 随机算法 * @return */ public static String getRandomIp(){ int ipAddres = new Random().nextInt(ServerIps.IP_LIST.size()); return ServerIps.IP_LIST.get(ipAddres); } /** * 加权随机算法 * @return */ public static String getWeightId(){ //将ip占比转换为对象数组,用于循环计算权重总数 Object[] ips = ServerIps.IP_MAP.values().toArray(); int ipTotal = 0;//权重总数 boolean flag=true;//用于判断权重是不是一样的 for(int i=0;i<ips.length;i++){ //计算权重总数 Integer ipWeight = (Integer) ips[i]; ipTotal += ipWeight; //判断所有ip权重是否一致 if(flag && i>0 && !ipWeight.equals(ips[i-1])){ flag = false; } } //权重不一致,就随机一个数来判断一下这个树落在哪里 if(!flag){ //产生一个随机数 int ipAddres = new Random().nextInt(ipTotal); for(String ipKey : ServerIps.IP_MAP.keySet()){ Integer ipValue = ServerIps.IP_MAP.get(ipKey); if(ipAddres<=ipValue){ return ipKey; } ipAddres = ipAddres - ipValue; } } //所有权重都一致,那就随机好啦 return getRandomIp(); } public static void main(String[] args) { for(int i=0;i<10;i++){ System.out.println(getWeightId()); } }运行一下,其实少量数据也看不出啥结果,这个还是一个概率问题,需要大量数据来统计分析的,但你数据越大就越会发现某个Ip出现的概率绝对跟他的权重占比相近。
一样的,我们先来一个简单的轮询算法,怎么理解这个轮询呢,就是一个接着一个,跟学生在食堂打饭一样,一个接一个(千万别说插队,那个你可以假装不知道)。还是来撸代码吧
public class PollingAlgorithm { //定义一个全局参数,轮询依据 private static Integer address = 0; /** * 简单轮询算法 * @return */ public static String getPollingIp(){ String ip = null; //这里加一个锁,防止并发影响轮询结果 synchronized (address){ if(address>= ServerIps.IP_LIST.size()){ address = 0; } ip = ServerIps.IP_LIST.get(address); address++; } return ip; } public static void main(String[] args) { for(int i=0;i<10;i++){ System.out.println(getPollingIp()); } } }运行结果,不用猜也知道了… 是吧,一个接着一个,故意加了一个锁,就是怕你们来插队。
好了,我们接下来在来看一下权重轮询算法吧,这里大概讲一下意思就不画图了,跟上面个随机权重有点像,不过一个是随机一个是轮询而已。 我们需要一个计数器,每次使用后就加1,然后我们根据计数器来除以权重总数得到余数,然后判断余数落在哪个点上就返回这个点上对应的IP就好。代码如下: 新建一个Sequence类来当计数器:
public class Sequence { public static Integer num = 0; public static Integer getIncrement(){ return ++num; } }改一下 PollingAlgorithm类:
public class PollingAlgorithm { //定义一个全局参数,轮询依据 private static Integer address = 0; /** * 简单轮询算法 * @return */ public static String getPollingIp(){ String ip = null; //这里加一个锁,防止并发影响轮询结果 synchronized (address){ if(address>= ServerIps.IP_LIST.size()){ address = 0; } ip = ServerIps.IP_LIST.get(address); address++; } return ip; } public static String getWeightPollingIp(){ //将ip占比转换为对象数组,用于循环计算权重总数 Object[] ips = ServerIps.IP_MAP.values().toArray(); int ipTotal = 0;//权重总数 boolean flag=true;//用于判断权重是不是一样的 for(int i=0;i<ips.length;i++){ //计算权重总数 Integer ipWeight = (Integer) ips[i]; ipTotal += ipWeight; //判断所有ip权重是否一致 if(flag && i>0 && !ipWeight.equals(ips[i-1])){ flag = false; } } //取得计数器 Integer sequenceNum = Sequence.getIncrement(); //取余数 Integer resultNum = sequenceNum%ipTotal; if(resultNum == 0){ resultNum = ipTotal; } if(!flag){ //循环判断当前这个点落在哪里 for(String ipKey : ServerIps.IP_MAP.keySet()){ Integer ipValue = ServerIps.IP_MAP.get(ipKey); if(resultNum <= ipValue){ return ipKey; } resultNum = resultNum - ipValue; } } //如果权重都相同,那就简单的轮询就行 return getPollingIp(); } public static void main(String[] args) { for(int i=0;i<10;i++){ System.out.println(getWeightPollingIp()); } } }运行一下,这个就会根据你定义的权重来一个一个的走了。举一个例子,你定义了ABC三个IP地址,然后A占5,B占1,C占1那么这里打印的就会是 这个算是完成了吧,是不是很开心哈哈哈哈。但是呢,你有没有想过,一周七天,作为老大哥的A也不能连续上七天的班呀,那样超级累的,那我们能不能做成AABACAA,这样老大哥A也能中途出去玩一玩放松一下,毕竟劳逸结合才能做出更好的事情嘛。好吧,还能真能做出这种效果。接着看吧!
首先我们来看一下大概的代码思想 好吧,不知道为啥这样做?或则不懂上面图片的意思?那就看这个吧: 现在是不是大概知道咋回事儿了?但是你不要问我为什么要这么操作。这个就像1+1为什么等于2不等于3。这个具体谁做出来的算法我也还没有找到根源,但是nginx的负载均衡算法好像就是这个,当然啦,比这个应该要复杂很多的。如果你还是不太懂呢,那就把下面代码多写几遍,这样你就知道是啥意思了。 新建一个IpWeight类
/** * ip就是保存ip咯 * weight 保存 初始权重(固定不变的) * crrentWeight 当前权重,随时改变 */ public class IpWeight { private String ip; private Integer weight; private Integer currentWeight; public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public Integer getWeight() { return weight; } public void setWeight(Integer weight) { this.weight = weight; } public Integer getCurrentWeight() { return currentWeight; } public void setCurrentWeight(Integer currentWeight) { this.currentWeight = currentWeight; } public IpWeight(String ip, Integer weight, Integer currentWeight) { this.ip = ip; this.weight = weight; this.currentWeight = currentWeight; } }再新建一个SmoothPollingAlgorithm类
public class SmoothPollingAlgorithm { private static Map<String,IpWeight> weightMap = new HashMap<String,IpWeight>(); public static String getIp(){ //将ip占比转换为对象数组,用于循环计算权重总数 Object[] ips = ServerIps.IP_MAP.values().toArray(); int ipTotal = 0;//权重总数 for(int i=0;i<ips.length;i++){ //计算权重总数 Integer ipWeight = (Integer) ips[i]; ipTotal += ipWeight; } //初始化 weightMap if(weightMap.isEmpty()){ for(String ip:ServerIps.IP_MAP.keySet()){ weightMap.put(ip,new IpWeight(ip,ServerIps.IP_MAP.get(ip),ServerIps.IP_MAP.get(ip))); } } //找出最大的IpWeight对象 根据currentWeight寻找 IpWeight maxWeight = null; for (IpWeight weight:weightMap.values()){ if(maxWeight == null || weight.getCurrentWeight()>maxWeight.getCurrentWeight()){ maxWeight = weight; } } //将最大的IpWeight对象减去总权重 maxWeight.setCurrentWeight(maxWeight.getCurrentWeight()- ipTotal); for (IpWeight weight:weightMap.values()){ weight.setCurrentWeight(weight.getCurrentWeight()+weight.getWeight()); } return maxWeight.getIp(); } public static void main(String[] args) { for(int i=0;i<10;i++){ System.out.println(getIp()); } } }运行一下,结果如下: 这个呢就叫做平滑加权轮询算法。是不是有点意思。
一致性hash算法通过一个叫作一致性hash环的数据结构实现。这个环的起点是0,终点是2^32 - 1,并且起点与终点连接,环的中间的整数按逆时针分布,故这个环的整数分布范围是[0, 2^32-1]。 我们用这个能干啥? 服务器集群接收到一次请求调用时,可以根据请求的信息,比如客户端Ip地址,或则请求路径以及请求参数等信息进行哈希,可以得出一个哈希值,特点是对于相同的ip地址,或请求路径和参数哈希出来的值是一样的。只要再增加一个算法,能够把这个哈希值映射成一个服务端ip地址,就可以相同的请求(相同的ip地址,或请求路径和请求参数)落到同一台服务器上。 因为客户端的请求情况是无穷尽的(ip不同,地址不同,参数不同),所以对于哈希值也是无穷尽的,所以我们不可能把所有的哈希值都进行映射到服务器ip上,所以这里需要一个哈希环,如下: 但是这种情况要是挂掉一个就不公平了,比如ip2服务器挂了,然后ip3的压力就会增大,所以我们需要加入虚拟节点。大概如下: 就是添加很多虚拟节点,然后大家均分,这样就比较平均了,既重男也重女是吧! 那我们要来看一下代码了:
public class HashAlgorithm { //定义一个TreeMap,基于红黑树的一个map private static SortedMap<Integer, String> virtualNodes = new TreeMap<>(); //每个真实节点对应的虚拟节点 private static final int VIRTUAL_NODES = 160; //给每个真实节点添加虚拟节点 static { for(String ip: ServerIps.IP_LIST){ for(int i=0;i<VIRTUAL_NODES;i++){ int hash = getHash(ip+"TE"+i); virtualNodes.put(hash,ip); } } } public static String getIp(String clint){ //获取hash值 int hash = getHash(clint); //得到大于这个hash值的map SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash); //大于该hash值的第一个元素位置 Integer resultIndex = subMap.firstKey(); //这是一个hash环,最后一个值就不能获取到了,需要给根节点 if(resultIndex == null){ resultIndex = virtualNodes.firstKey(); } return virtualNodes.get(resultIndex); } private static int getHash(String str) { final int p = 16777619; int hash = (int) 2166136261L; for (int i = 0; i < str.length(); i++) hash = (hash ^ str.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; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println(getIp("client" + i)); } } }这里可能有一个TreeMap是一个比较生疏的技术点,这个还请大家各自自己去了解一下这里我就不讲解了,他是基于红黑树实现的。
好了 运行一下,其实你就会发现分散还比较平均(我前面拿了好多数据来测做了统计看的)。
前面几种方法主要目的是使服务器分配到的调用次数尽量均衡。但实际情况呢,还需要根据调用活跃度来进行计算的。
这里呢稍微讲一下,就是首先获取服务器活跃度,拿到最小的活跃度的ip。如果只有一个,那么就直接调用这个ip。但是有多个咋办?那我们就来判断权重啦。权重大的优先,权重相同的就随机咯。哎,实在是不知道怎么说了,还是来看代码吧:
public class ServerIps { public static final List<String> IP_LIST = Arrays.asList( "192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.4", "192.168.0.5", "192.168.0.6", "192.168.0.7", "192.168.0.8", "192.168.0.9", "192.168.0.10" ); /** * key = ip * value = 权重占比 */ public static final Map<String,Integer> IP_MAP = new HashMap<String,Integer>(); static { IP_MAP.put("192.168.0.1",1); IP_MAP.put("192.168.0.2",8); IP_MAP.put("192.168.0.3",3); IP_MAP.put("192.168.0.4",6); IP_MAP.put("192.168.0.5",5); IP_MAP.put("192.168.0.6",5); IP_MAP.put("192.168.0.7",6); IP_MAP.put("192.168.0.8",7); IP_MAP.put("192.168.0.9",2); IP_MAP.put("192.168.0.10",9); /* IP_MAP.put("A",5); IP_MAP.put("B",1); IP_MAP.put("C",1);*/ } public static final Map<String,Integer>IP_ACTIVE = new HashMap<String,Integer>(); static{ IP_ACTIVE.put("192.168.0.1",2); IP_ACTIVE.put("192.168.0.2",0); IP_ACTIVE.put("192.168.0.3",1); IP_ACTIVE.put("192.168.0.4",3); IP_ACTIVE.put("192.168.0.5",0); IP_ACTIVE.put("192.168.0.6",1); IP_ACTIVE.put("192.168.0.7",4); IP_ACTIVE.put("192.168.0.8",2); IP_ACTIVE.put("192.168.0.9",7); IP_ACTIVE.put("192.168.0.10",0); } } public class MinActive { private static String getServer() { //找出数最小的服务器 Optional<Integer> minValue = ServerIps.IP_ACTIVE.values().stream().min(Comparator.naturalOrder()); if (minValue.isPresent()) { List<String> minActivityIps = new ArrayList<>(); ServerIps.IP_ACTIVE.forEach((ip, activity) -> { if (activity.equals(minValue.get())) { minActivityIps.add(ip); } }); //最下活跃数有多个的时候,根据权重来选择,权重大的优先 if (minActivityIps.size() > 1) {Map<String, Integer> weightList = new LinkedHashMap<String, Integer> (); //过滤出对应ip和权重 ServerIps.IP_MAP.forEach((ip, weight) -> { if (minActivityIps.contains(ip)) { weightList.put(ip, ServerIps.IP_MAP.get(ip)); } }); int totalWeight = 0;//权重总数 boolean sameWeight = true;//用于判断权重是不是一样的 //将ip占比转换为对象数组,用于循环计算权重总数 Object[] weights = weightList.values().toArray(); for (int i = 0; i < weights.length; i++) { Integer weight = (Integer) weights[i]; totalWeight += weight; //判断所有ip权重是否一致 if (sameWeight && i > 0 && !weight.equals(weights[i - 1])) { sameWeight = false; } } java.util.Random random = new java.util.Random(); int randomPos = random.nextInt(totalWeight); if (!sameWeight) { //循环判断当前这个点落在哪里 for (String ip : weightList.keySet()) { Integer value = weightList.get(ip); if (randomPos < value) { return ip; } randomPos = randomPos - value; } } return (String) weightList.keySet().toArray()[new java.util.Random().nextInt(weightList.size())]; } else { return minActivityIps.get(0); } } else { return (String) ServerIps.IP_MAP.keySet().toArray()[new java.util.Random().nextInt(ServerIps.IP_MAP.size())]; } } public static void main(String[] args) { for(int i=0;i<10;i++){ System.out.println(getServer()); } }好吧,这里是有问题的,就是正常情况呢是在调用的时候将活跃度加1,返回成功后再减1的。但是今天实在是太累了,就忘记写了。不过只要知道有这个就行了,如果后面我还记得我一定来改掉。 运行结果就会发现192.168.0.10这个调用的次数稍微多一点咯。
好了,到此结束了。以上就是一些简单的负载均衡算法。希望能给你一丝帮助!