分布式负载均衡算法的实现
Posted lgjlife
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式负载均衡算法的实现相关的知识,希望对你有一定的参考价值。
在分布式项目中,为了提高系统的可用性,服务提供者一般都会做集群处理,当其中一个服务出现宕机的时候,集群的其他服务仍然能够提供服务,从而提高系统的可靠性。
常用的负载均衡算法有:
- 随机算法
- 加权随机算法
- 轮询算法
- 加权轮询算法
- 最小时延算法
- 一致性hash算法
负载均衡追求的是每个服务提供者的负载一致,不会出现负载不均衡的情况。
准备
这是一个服务提供者的POJO,包含了服务的host和port等信息
@Slf4j @Data @AllArgsConstructor @NoArgsConstructor public class ProviderConfig implements Serializable{ private static final long serialVersionUID = 1; //通信host private String host; //通信端口 private Integer port; //请求接口名称 private String interfaceName; //请求方法 private String[] methods; //应用名称 private String application; //权重 private int weight; //调用时间 private int callTime; }
定义负载均衡策略接口
public interface LoadbalanceStrategy { //object为扩展参数 public ProviderConfig select(List<ProviderConfig> configs, Object object); }
随机算法
随机算法,也就是从服务列表中随机选择一个,如果随机数产生算法不好,那么就会导致出现偏向性,导致有些服务命中概率高,有的服务命中概率低,甚至有的服务命中率为0。最后会导致命中率高的时延很严重。
随机算法的优点是其实现简单。
实现
也就是产生一个随机数,从服务List选择一个,很简单的算法。
public class RandomLoadbalanceStrategy implements LoadbalanceStrategy{ @Override public ProviderConfig select(List<ProviderConfig> configs, Object object) { int index = new Random().nextInt(configs.size()); return configs.get(index); } }
测试
注:后面多处会使用这个方法进行测试
//strategy 负载均衡策略:随机,加权随机,轮询,加权轮询
//configNum 生产者个数
//testCount 测试次数
public void loadbalace(LoadbalanceStrategy strategy ,int configNum,int testCount ){
List<ProviderConfig> configs = new ArrayList<>();
int[] counts = new int[configNum];
for(int i = 0; i< configNum; i++){
ProviderConfig config = new ProviderConfig();
config.setInterfaceName("com.serviceImpl");
config.setHost("127.0.0.1");
config.setPort(i);
config.setWeight(new Random().nextInt(100));
configs.add(config);
}
//System.out.println(configs);
for(int i = 0; i< testCount ; i++){
ProviderConfig config = strategy.select(configs,null);
// System.out.println("选中的:"+config);
Integer count = counts[config.getPort()];
counts[config.getPort()] = ++count;
}
for(int i = 0; i< configNum; i++){
System.out.println("序号:" + i + " 权重:" + configs.get(i).getWeight() + "--次数:" + counts[i]);
}
}
执行测试
LoadbalanceStrategy strategy1 = new RandomLoadbalanceStrategy(); loadbalace(strategy1,10,1000);
输出
随机负载均衡.... 序号:0--次数:98 序号:1--次数:97 序号:2--次数:86 序号:3--次数:99 序号:4--次数:116 序号:5--次数:98 序号:6--次数:96 序号:7--次数:102 序号:8--次数:101 序号:9--次数:107
从测试结果来看,Jdk的随机算法还是比较均匀的。
加权随机算法
加权随机就是在随机算法的基础上,给每个服务增加一个权重,权重越大,概率越大。
在应用进行分布式部署时,机器硬件性能和环境的差异会导致服务性能出现不一致。
为了解决这个问题,可以给性能差的服务降低权重,给性能好的服务增加权重,以尽可能达到负载均衡的效果。
加权随机算法
实现
public class WeightRandomLoadbalanceStrategy implements LoadbalanceStrategy{ @Override public ProviderConfig select(List<ProviderConfig> configs, Object object) { List<ProviderConfig> newConfigs = new ArrayList<>(); for(ProviderConfig config:configs){ for(int i = 0; i< config.getWeight(); i++){ newConfigs.add(config); } } int index = new Random().nextInt(newConfigs.size()-1); return newConfigs.get(index); } }
还是使用上面的测试代码loadbalace(LoadbalanceStrategy strategy ,int configNum,int testCount )进行测试。
System.out.println("加权随机负载均衡...."); LoadbalanceStrategy strategy2 = new WeightRandomLoadbalanceStrategy(); loadbalace(strategy1,10,1000);
测试结果:
加权随机负载均衡.... 序号:0 权重:44--次数:101 序号:1 权重:27--次数:63 序号:2 权重:22--次数:47 序号:3 权重:61--次数:134 序号:4 权重:97--次数:214 序号:5 权重:38--次数:72 序号:6 权重:42--次数:79 序号:7 权重:51--次数:113 序号:8 权重:16--次数:28 序号:9 权重:67--次数:149
可以看到,权重越大,命中概率页越大。
轮询算法
轮询算法就是,轮询所有的服务,每个服务命中的概率都是一样的,缺点还是和随机算法一样,还是无法解决机器性能差异的问题。
实现
public class PollingLoadbalanceStrategy implements LoadbalanceStrategy { //使用一个Map来缓存每类应用的轮询索引 private Map<String,Integer> indexMap = new ConcurrentHashMap<>(); public ProviderConfig select(List<ProviderConfig> configs, Object object){ Integer index = indexMap.get(getKey(configs.get(0))); if(index == null){ indexMap.put(getKey(configs.get(0)),0); return configs.get(0); } else { index++; if(index >= configs.size()){ index = 0; } indexMap.put(getKey(configs.get(0)),index); return configs.get(index); } } public String getKey(ProviderConfig config){ return config.getInterfaceName(); } }
测试
还是使用上面的方法
System.out.println(" 轮询负载均衡....."); LoadbalanceStrategy strategy3 = new PollingLoadbalanceStrategy(); loadbalace(strategy3,10,1000);
测试结果
轮询负载均衡..... 序号:0 权重:88--次数:100 序号:1 权重:82--次数:100 序号:2 权重:58--次数:100 序号:3 权重:68--次数:100 序号:4 权重:67--次数:100 序号:5 权重:57--次数:100 序号:6 权重:19--次数:100 序号:7 权重:43--次数:100 序号:8 权重:4--次数:100 序号:9 权重:35--次数:100
可以看到,测试1000次,每个应用命中的概率都是一样的。
加权轮询算法
原理和上面的加权随机算法一样
实现
public class WeightPollingLoadbalanceStrategy implements LoadbalanceStrategy { private Map<String,Integer> indexMap = new ConcurrentHashMap<>(); public ProviderConfig select(List<ProviderConfig> configs, Object object){ Integer index = indexMap.get(getKey(configs.get(0))); if(index == null){ indexMap.put(getKey(configs.get(0)),0); return configs.get(0); } else { List<ProviderConfig> newConfigs = new ArrayList<>(); for(ProviderConfig config:configs){ for(int i = 0; i< config.getWeight(); i++){ newConfigs.add(config); } } index++; if(index >= newConfigs.size()){ index = 0; } indexMap.put(getKey(configs.get(0)),index); return newConfigs.get(index); } } public String getKey(ProviderConfig config){ return config.getInterfaceName(); } }
测试
System.out.println(" 加权轮询负载均衡....."); LoadbalanceStrategy strategy4 = new WeightPollingLoadbalanceStrategy(); loadbalace(strategy4,10,1000);
输出
加权轮询负载均衡..... 序号:0 权重:77--次数:182 序号:1 权重:75--次数:150 序号:2 权重:22--次数:44 序号:3 权重:43--次数:86 序号:4 权重:59--次数:118 序号:5 权重:10--次数:20 序号:6 权重:1--次数:2 序号:7 权重:25--次数:50 序号:8 权重:85--次数:170 序号:9 权重:89--次数:178
可以看到,权重越大,命中的概率越大。
最小时延算法
由于机器性能的差异以及网络传输等原因,会导致集群中不同的应用调用时长不一样。
如果能降低调用耗时长的应用的命中率,提高调用耗时短的命中率,达到动态调整,从而实现最终的负载均衡,那么便可以解决以上性能差异的问题。
缺点是实现起来比较复杂,因为要计算启动之后平均调用耗时。
实现
public class LeastActiveLoadbalanceStrategy implements LoadbalanceStrategy{ public ProviderConfig select(List<ProviderConfig> configs, Object object){ ProviderConfig[] registryConfigs= new ProviderConfig[configs.size()]; configs.toArray(registryConfigs); Arrays.sort(registryConfigs, new Comparator<ProviderConfig>() { @Override public int compare(ProviderConfig o1, ProviderConfig o2) { if(o1.getCallTime() < o2.getCallTime()){ return -1; } else if(o1.getCallTime() == o2.getCallTime()){ return 0; } else { return 1; } } }); return registryConfigs[0]; } }
这里使用Arrays.sort()来实现耗时排序。
测试
public void leastActiveLoadbalance(LoadbalanceStrategy strategy ,int configNum){ List<ProviderConfig> configs = new ArrayList<>(); for(int i = 0; i< configNum; i++){ ProviderConfig config = new ProviderConfig(); config.setInterfaceName("com.serviceImpl"); config.setHost("127.0.0.1"); config.setPort(i); config.setWeight(i);
//这里使用随机数来模拟调用耗时。 config.setCallTime(new Random().nextInt(100)); configs.add(config); } for(ProviderConfig c:configs){ System.out.println("序号:" + c.getPort() +"--时延:" + c.getCallTime() ); } System.out.println("--------------"); ProviderConfig config = strategy.select(configs,null); System.out.println("最终选择 序号:" + config.getPort() +"--时延:" + config.getCallTime() ); }
这里使用随机数来模拟调用耗时。
System.out.println(" 最小时延负载均衡....."); LoadbalanceStrategy strategy5 = new LeastActiveLoadbalanceStrategy(); leastActiveLoadbalance(strategy5,10);
测试结果
最小时延负载均衡..... 序号:0--时延:83 序号:1--时延:3 序号:2--时延:60 序号:3--时延:52 序号:4--时延:73 序号:5--时延:74 序号:6--时延:37 序号:7--时延:59 序号:8--时延:83 序号:9--时延:2 -------------- 最终选择 序号:9--时延:2
可以看到命中了耗时最小的应用。
一致性hash算法
先构造一个长度为232的整数环(这个环被称为一致性Hash环),根据节点名称的Hash值(其分布为[0, 232-1])将服务器节点放置在这个Hash环上,
然后根据数据的Key值计算得到其Hash值(其分布也为[0, 232-1]),接着在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。
一致性hash算法还可以实现一个消费者一直命中一个服务提供者。
如下图,一共有四个服务提供者
provider-1: 127.0.0.1:8001
provider-2: 127.0.5.2:8145
provider-3: 127.0.1.2:8123
provider-4: 127.1.3.2:8256
通过hash计算后,四个节点分布在hash环的不同位置上
当有一个消费者(127.0.0.1:8011)通过hash计算后,定位到如图中所示位置,它会顺时针查找下一个节点,选择第一个查找到的节点。
这里存在几个关键问题:
1. hash算法的影响
如果hash算法计算结果过于集中,如下图,节点分布再很小的范围内,如果消费者大部分命中范围之外,就会导致node1负载异常的大,出现负载不均衡的问题。
所以需要一个比较好的hash算法。
解决这个问题的办法是需要选择一个好的hashcode算法,hash算法比较
2. 增加或者删除节点时会导致负载不均衡
如下图:
正常情况下每个节点都是25%的命中概率
节点node2失效时,之前节点2的所有命中全部加到节点3,导致节点3的负载变大
当增加节点5时,之前节点3的命中全部给了节点5,也还是出现了负载不均衡。
解决这个问题的办法是增加虚拟节点
如下图,为每个节点都增加了虚拟节点,增加虚拟节点,可以使整个hash环分布的更加均匀,但有个问题是,节点越多,维护的性能越大,因此,需要增加多少个虚拟节点,需要根据实际需要进行测试。
实现
虚拟节点的格式为 127.0.0.1:8001&&node1
分别使用jdk 的hashcode算法和FNV1_32_HASH算法进行比较。 .
public class UniformityHashLoadbalanceStrategy implements LoadbalanceStrategy{ private static final int VIRTUAL_NODES = 5; //object为消费者的 host:port public ProviderConfig select(List<ProviderConfig> configs, Object object){ SortedMap<Integer, ProviderConfig> sortedMap = new TreeMap(); for(ProviderConfig config:configs){ for(int j = 0; j < VIRTUAL_NODES; j++){ sortedMap.put(caculHash(getKey(config.getHost(),config.getPort(),"&&node"+j)),config); } } System.out.println(sortedMap); Integer requestHashcCode = caculHash((String)object); sortedMap.forEach((k,v)->{ System.out.println("hashcode: " + k + " " + v.getHost()+":"+v.getPort()); }); System.out.println("------------------请求的hashcode:"+requestHashcCode); SortedMap<Integer, ProviderConfig> subMap = sortedMap.tailMap(requestHashcCode); Integer index = subMap.firstKey(); return subMap.get(index); } private String getKey(String host,int port,String node){ return new StringBuilder().append(host).append(":").append(port).append(node).toString(); }
//计算hash值 private int caculHash(String str){ //jdk 的 hashcode /*int hashCode = str.hashCode(); hashCode = (hashCode<0)?(-hashCode):hashCode; return hashCode;*/
//FNV1_32_HASH 算法 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 void uniformityHashLoadbalanceStrategyTest(LoadbalanceStrategy strategy ,int configNum){ List<ProviderConfig> configs = new ArrayList<>(); for(int i = 0; i< configNum; i++){ ProviderConfig config = new ProviderConfig(); config.setInterfaceName("com.serviceImpl"); config.setHost("127.0.0.1"); config.setPort(new Random().nextInt(9999)); config.setWeight(i); config.setCallTime(new Random().nextInt(100)); configs.add(config); } ProviderConfig config = strategy.select(configs,"127.0.0.1:1234"); System.out.println("选择结果:" + config.getHost() + ":" + config.getPort()); }
System.out.println(" 一致性hash负载均衡....."); uniformityHashLoadbalanceStrategyTest(new UniformityHashLoadbalanceStrategy(),10);
1. jdk 的 hashcode 算法
hashcode: 441720772 127.0.0.1:1280 hashcode: 441720773 127.0.0.1:1280 hashcode: 441720774 127.0.0.1:1280 hashcode: 441720775 127.0.0.1:1280 hashcode: 441720776 127.0.0.1:1280 hashcode: 1307619854 127.0.0.1:3501 hashcode: 1307619855 127.0.0.1:3501 hashcode: 1307619856 127.0.0.1:3501 hashcode: 1307619857 127.0.0.1:3501 hashcode: 1307619858 127.0.0.1:3501 hashcode: 1363372970 127.0.0.1:779 hashcode: 1363372971 127.0.0.1:779 hashcode: 1363372972 127.0.0.1:779 hashcode: 1363372973 127.0.0.1:779 hashcode: 1363372974 127.0.0.1:779 hashcode: 1397780469 127.0.0.1:5928 hashcode: 1397780470 127.0.0.1:5928 hashcode: 1397780471 127.0.0.1:5928 hashcode: 1397780472 127.0.0.1:5928 hashcode: 1397780473 127.0.0.1:5928 hashcode: 1700521830 127.0.0.1:4065 hashcode: 1700521831 127.0.0.1:4065 hashcode: 1700521832 127.0.0.1:4065 hashcode: 1700521833 127.0.0.1:4065 hashcode: 1700521834 127.0.0.1:4065 hashcode: 1774961903 127.0.0.1:5931 hashcode: 1774961904 127.0.0.1:5931 hashcode: 1774961905 127.0.0.1:5931 hashcode: 1774961906 127.0.0.1:5931 hashcode: 1774961907 127.0.0.1:5931 hashcode: 1814135809 127.0.0.1:5050 hashcode: 1814135810 127.0.0.1:5050 hashcode: 1814135811 127.0.0.1:5050 hashcode: 1814135812 127.0.0.1:5050 hashcode: 1814135813 127.0.0.1:5050 hashcode: 1881959435 127.0.0.1:1991 hashcode: 1881959436 127.0.0.1:1991 hashcode: 1881959437 127.0.0.1:1991 hashcode: 1881959438 127.0.0.1:1991 hashcode: 1881959439 127.0.0.1:1991 hashcode: 1889283041 127.0.0.1:4071 hashcode: 1889283042 127.0.0.1:4071 hashcode: 1889283043 127.0.0.1:4071 hashcode: 1889283044 127.0.0.1:4071 hashcode: 1889283045 127.0.0.1:4071 hashcode: 2118931362 127.0.0.1:7152 hashcode: 2118931363 127.0.0.1:7152 hashcode: 2118931364 127.0.0.1:7152 hashcode: 2118931365 127.0.0.1:7152 hashcode: 2118931366 127.0.0.1:7152 ------------------请求的hashcode:35943393 选择结果:127.0.0.1:1280
可以看到JDK默认的hashcode方法的问题,各个虚拟节点都是比较集中,会出现很严重的负载不均衡问题。
2.使用 FNV1_32_HASH算法
hashcode: 23525275 127.0.0.1:1340 hashcode: 28411340 127.0.0.1:2589 hashcode: 43226263 127.0.0.1:1340 hashcode: 117776908 127.0.0.1:2848 hashcode: 190736195 127.0.0.1:6316 hashcode: 193232709 127.0.0.1:2848 hashcode: 238271827 127.0.0.1:1340 hashcode: 304277099 127.0.0.1:1245 hashcode: 323839980 127.0.0.1:1340 hashcode: 527125849 127.0.0.1:1704 hashcode: 571281671 127.0.0.1:1704 hashcode: 575621854 127.0.0.1:3295 hashcode: 595383578 127.0.0.1:8294 hashcode: 611098804 127.0.0.1:6316 hashcode: 635348472 127.0.0.1:1245 hashcode: 645589927 127.0.0.1:2923 hashcode: 724716685 127.0.0.1:2589 hashcode: 751044093 127.0.0.1:6316 hashcode: 770120156 127.0.0.1:6316 hashcode: 826884147 127.0.0.1:1340 hashcode: 828747447 127.0.0.1:3295 hashcode: 845151891 127.0.0.1:2923 hashcode: 1009576302 127.0.0.1:8294 hashcode: 1012854319 127.0.0.1:3295 hashcode: 1128338730 127.0.0.1:2848 hashcode: 1193699998 127.0.0.1:1245 hashcode: 1256565279 127.0.0.1:5227 hashcode: 1262478155 127.0.0.1:2848 hashcode: 1313976900 127.0.0.1:1704 hashcode: 1316234142 127.0.0.1:8294 hashcode: 1329044824 127.0.0.1:2589 hashcode: 1338628917 127.0.0.1:8294 hashcode: 1376780501 127.0.0.1:2923 hashcode: 1400591219 127.0.0.1:8294 hashcode: 1464238680 127.0.0.1:1704 hashcode: 1477774526 127.0.0.1:3295 hashcode: 1508307156 127.0.0.1:5227 hashcode: 1519292522 127.0.0.1:1245 hashcode: 1624736872 127.0.0.1:1704 hashcode: 1734567634 127.0.0.1:5227 hashcode: 1742088861 127.0.0.1:6316 hashcode: 1786711526 127.0.0.1:2923 hashcode: 1799136540 127.0.0.1:3295 hashcode: 1871586245 127.0.0.1:2589 hashcode: 1879550454 127.0.0.1:2923 hashcode: 1882602635 127.0.0.1:5227 hashcode: 1921744078 127.0.0.1:2848 hashcode: 1925433450 127.0.0.1:2589 hashcode: 2087729269 127.0.0.1:5227 hashcode: 2138998633 127.0.0.1:1245 ------------------请求的hashcode:2064286659 选择结果:127.0.0.1:5227
可以看到,各个虚拟节点分布相对较散,能够达到较好的效果。
总结
以上给了各个负载均衡算法的实现思路和代码实现,测试结果。
现总结如下:
随机算法:
好的随机算法可以使选择比较均衡,但还是会出现机器性能差异导致的调用耗时不一样。优点是实现简单。
加权随机算法:
可以根据不同的机器性能调整不同的权重比,从而降低机器性能差异带来的问题。
轮询算法:
可以使每个节点的选中概率一致,但也会出现随机算法的问题。
加权轮询:
可以根据不同的机器性能调整不同的权重比,从而降低机器性能差异带来的问题。
最小时延算法:
根据服务调用耗时动态调整,可以达到比较好的负载均衡。缺点是实现比较复杂。
一致性hash算法:
可以使消费者始终对应一个服务提供者。缺点是实现相对复杂。同时通过优化hashcode算法和增加虚拟节点解决分布不均的问题。
后话
以上的实现仅提供一种思路,实际使用时应该根据实际情况进行性能测试和优化。
以上是关于分布式负载均衡算法的实现的主要内容,如果未能解决你的问题,请参考以下文章