微信红包业务,为什么接纳轮询算法?

藏宝库编辑 2024-9-10 19:26:38 93 0 来自 中国
目次

  • 媒介
  • 根本的负载算法
  • 平滑加权轮询算法
  • 同等性哈希算法
  • 最小生动数算法
  • 最优相应算法
  • 总结
媒介
负载平衡这个概念,险些在所有支持高可用的技能栈中都存在,比方微服务、分库分表、各大中心件(MQ、Redis、MyCat、Nginx、ES)等,也包罗云盘算、云调理、大数据中也是炙手可热的词汇。
负载平衡战略重要分为静态与动态两大类:

  • 静态调理算法:指设置后只会依据设置好的战略举行哀求分发的算法。
  • 动态调理算法:指设置后会根据线上情况(网络/CPU 负载/磁盘 IO 等)来分发哀求。
但负载平衡算法数量并不少,本篇重要对于一些常用且高效的负载战略举行分析。
根本的负载算法
假如聊到最根本的负载平衡算法,那么信托各人多少都有相识,比方:轮询、随机、权重等这类算法。特点就在于实现简单,先来快速过一遍根本的算法实现。
| 轮询算法

轮询算法是最为简单、也最为常见的算法,也是大多数集群情况下的默认调理算法,这种算法会按照设置的服务器列表,按照次序依次分发哀求,所有服务器都分发一遍后,又会回到第一台服务器循环该步调。
Java 代码实现如下:
// 服务类:重要用于生存设置的所有节点public class Servers {// 模拟设置的集群节点public static List<String> SERVERS = Arrays.asList("44.120.110.001:8080","44.120.110.002:8081","44.120.110.003:8082","44.120.110.004:8083","44.120.110.005:8084");}// 轮询战略类:实现根本的轮询算法public class RoundRobin{// 用于记载当前哀求的序列号private static AtomicInteger requestIndex = new AtomicInteger(0);// 从集群节点中选取一个节点处理惩罚哀求public static String getServer(){// 用哀求序列号取余集群节点数量,求得本次处理惩罚哀求的节点下标int index = requestIndex.get() % Servers.SERVERS.size();// 从服务器列表中获取详细的节点IP地点信息String server = Servers.SERVERS.get(index);// 自增一次哀求序列号,方便下个哀求盘算requestIndex.incrementAndGet();// 返回获取到的服务器IP地点return server;}}// 测试类:测试轮询算法public class Test{public static void main(String[] args){// 利用for循环简单模拟10个客户端哀求for (int i = 1; i <= 10; i++){System.out.println("第"+ i + "个哀求:" + RoundRobin.getServer());}}}/******输出结果*******/第1个哀求:44.120.110.001:8080第2个哀求:44.120.110.002:8081第3个哀求:44.120.110.003:8082第4个哀求:44.120.110.004:8083第5个哀求:44.120.110.005:8084第6个哀求:44.120.110.001:8080第7个哀求:44.120.110.002:8081第8个哀求:44.120.110.003:8082第9个哀求:44.120.110.004:8083第10个哀求:44.120.110.005:8084上述案例中,整个算法的实现尤为简单,就是通过一个原子计数器记载当前哀求的序列号,然后直接通过 % 集群中的服务器节点总数,终极得到一个详细的下标值,再通过这个下标值,从服务器 IP 列表中获取一个详细的 IP 地点。
轮询算法的上风:

  • 算法实现简单,哀求分发服从够高。
  • 可以大概将所有哀求均派到集群中的每个节点上。
  • 易于后期弹性伸缩,业务增长时可以拓展节点,业务萎靡时可以缩减节点。
轮询算法的劣势:

  • 对于差别设置的服务器无法公道照顾,无法将高设置的服务器性能发挥出来。
  • 由于哀求分发时,是基于哀求序列号来实现的,以是无法包管同一客户端的哀求都是由同一节点处理惩罚的,因此须要通过 session 记载状态时,无法确保其同等性。
轮询算法的应用场景:

  • 集群中所有节点硬件设置都是雷同的情况。
  • 只读不写,无需保持状态的景象。
| 随机算法

随机算法的实现也非常简单,也就是当客户端哀求到来时,每次都会从已设置的服务器列表中随机抽取一个节点处理惩罚。
实现如下:
// 随机战略类:随机抽取集群中的一个节点处理惩罚哀求public class Random {// 随机数产生器,用于产生随机因子static java.util.Random random = new java.util.Random();public static String getServer(){// 从已设置的服务器列表中,随机抽取一个节点处理惩罚哀求return Servers.SERVERS.get(random.nextInt(Servers.SERVERS.size()));}}上述该算法的实现,非常明白,通过 java.util 包中自带的 Random 随机数产生器,从服务器列表中随机抽取一个节点处理惩罚哀求,该算法的结果也不测试了,各人估计一眼就能看明白。
随机算法的上风:个人看来该算法单独利用的意义并不大,一样平常会共同下面要讲的权重战略协同利用。
随机算法的劣势:

  • 无法公道地将哀求均派到每台服务器上。
  • 由于处理惩罚哀求的目的服务器不明白,因此也无法满足须要记载状态的哀求。
  • 可以大概在肯定程度上发挥出高设置的呆板性能,但充满不确定因素。
| 权重算法

权重算法是创建在其他基础算法之上推出的一种概念,权重算法并不能单独设置,由于权重算法无法做到哀求分发的调理,以是一样平常权重会共同其他基础算法联合利用。
如:轮询权重算法、随机权重算法等,如答应以让之前的两种基础调理算法更为“人性化”一些。
权重算法是指对于集群中的每个节点分配一个权重值,权重值越高,该节点被分发的哀求数也会越多,反之同理。
如许做的利益非常显着,也就是可以大概充实思量呆板的硬件设置,从而分配差别权重值,做到“能者多劳”。
那怎样实现呢,先来看看随机权重的实现:
public class Servers{// 在之前是Servers类中再参加一个权重服务列表public static Map<String, Integer> WEIGHT_SERVERS = new LinkedHashMap<>();static {// 设置集群的所有节点信息及权重值WEIGHT_SERVERS.put("44.120.110.001:8080",17);WEIGHT_SERVERS.put("44.120.110.002:8081",11);WEIGHT_SERVERS.put("44.120.110.003:8082",30);}}// 随机权重算法public class Randomweight {// 初始化随机数生产器static java.util.Random random = new java.util.Random();public static String getServer(){// 盘算总权重值int weightTotal = 0;for (Integer weight : Servers.WEIGHT_SERVERS.values()) {weightTotal += weight;}// 从总权重的范围内随机天生一个索引int index = random.nextInt(weightTotal);System.out.println(index);// 遍历整个权重集群的节点列表,选择节点处理惩罚哀求String targetServer = "";for (String server : Servers.WEIGHT_SERVERS.keySet()) {// 获取每个节点的权重值Integer weight = Servers.WEIGHT_SERVERS.get(server);// 假如权重值大于产生的随机数,则代表此次随机分配应该落入该节点if (weight > index){// 直接返回对应的节点行止理惩罚本次哀求并制止循环targetServer = server;break;}// 假如当前节点的权重值小于随机索引,则用随机索引减去当前节点的权重值,// 继承循环权重列表,与其他的权重值举行对比,// 终极该哀求总会落入到某个IP的权重值范围内index = index - weight;}// 返回选中的目的节点return targetServer;}public static void main(String[] args){// 利用for循环模拟10个客户端哀求测试for (int i = 1; i <= 10; i++){System.out.println("第"+ i + "个哀求:" + getServer());}}}/********运行结果********/第1个哀求:44.120.110.003:8082第2个哀求:44.120.110.001:8080第3个哀求:44.120.110.003:8082第4个哀求:44.120.110.003:8082第5个哀求:44.120.110.003:8082第6个哀求:44.120.110.003:8082第7个哀求:44.120.110.003:8082第8个哀求:44.120.110.001:8080第9个哀求:44.120.110.001:8080第10个哀求:44.120.110.002:8081上面这个算法对比之前的根本实现,大概略微有些复杂难明,我们先上个图:
1.png 细致观看上图后,逻辑应该会清楚许多,大要捋一下思绪:

  • 先求和所有的权重值,再随机天生一个总权重之内的索引。
  • 遍历之前设置的服务器列表,用随机索引与每个节点的权重值举行判断。假如小于,则代表当前哀求应该落入目前这个节点;假如大于,则代表随机索引超出了目前节点的权重范围,则减去当前权重,继承与其他节点判断。
  • 终极随机出的索引总会落入到一个节点的权重范围内,末了返回对应的节点 IP。
如许一分析下来,估摸着各位小伙伴应该都明白了,接着再来看看轮询权重算法的实现:
// 轮询权重算法public class RoundRobinweight {private static AtomicInteger requestCount = new AtomicInteger(0);public static String getServer(){int weightTotal = 0;for (Integer weight : Servers.WEIGHT_SERVERS.values()) {weightTotal += weight;}String targetServer = "";int index = requestCount.get() % weightTotal;requestCount.incrementAndGet();for (String server : Servers.WEIGHT_SERVERS.keySet()) {Integer weight = Servers.WEIGHT_SERVERS.get(server);if (weight > index){targetServer = server;break;}index = index - weight;}return targetServer;}public static void main(String[] args){for (int i = 1; i <= 10; i++){System.out.println("第"+ i + "个哀求:" + getServer());}}}/********运行结果*********/第1个哀求:44.120.110.001:8080第2个哀求:44.120.110.001:8080第3个哀求:44.120.110.001:8080第4个哀求:44.120.110.001:8080第5个哀求:44.120.110.001:8080第6个哀求:44.120.110.001:8080第7个哀求:44.120.110.001:8080第8个哀求:44.120.110.001:8080第9个哀求:44.120.110.001:8080第10个哀求:44.120.110.001:8080观察上述中的案例,此刻会发现出端倪,代码实现过程雷同,但此刻的输出结果,竟然全部哀求都被分发到了 44.120.110.001:8080 这个节点,这是为什么呢?
由于此时是通过哀求序列号去举行判断的,以是终极结果会成为:

  • 前 17 个哀求会交给 44.120.110.001:8080 节点。
  • 后续 11 个哀求会交给 44.120.110.002:8081 节点。
  • 末了 30 个哀求会交给 44.120.110.003:8082 节点。
  • 然后持续重复该过程.....
此时似乎离我们预期的负载结果发生了偏离,假如接纳这种方案去实现轮询权重算法,终极会将一个集群变为单点服务,这显然并不是等待中的结果,因此须要一种新的方式去实现,那么又该怎样去做呢?
此时须要牵涉到一种哀求调理的高级算法:平滑加权轮询算法。
平滑加权轮询算法
平滑轮询加权算法的本质就是为了办理之前实现方式中所存在的标题,可以大概将哀求匀称的按照权重值分发到每台呆板。
这种算法计划的非常奥妙,实现过程也尤为风趣,我们一起来看看:
// 权重服务器的设置类public class Servers {public static Map<String, Integer> WEIGHT_SERVERS = new LinkedHashMap<>();static {// 权重值设置的略微小一点,方便后续明白算法WEIGHT_SERVERS.put("44.120.110.001:8080",3);WEIGHT_SERVERS.put("44.120.110.002:8081",2);WEIGHT_SERVERS.put("44.120.110.003:8082",1);}}// 权重类public class Weight {// 节点信息private String server;// 节点权重值private Integer weight;// 动态权重值private Integer currentWeight;// 构造方法public Weight() {}public Weight(String server, Integer weight, Integer currentWeight) {this.server = server;this.weight = weight;this.currentWeight = currentWeight;}// 封装方法public String getServer() {return server;}public void setServer(String server) {this.server = server;}public Integer getWeight() {return weight;}public void setWeight(Integer weight) {this.weight = weight;}public Integer getCurrentWeight() {return this.currentWeight;}public void setCurrentWeight(Integer currentWeight) {this.currentWeight = currentWeight;}}public class RoundRobinWeight {// 初始化存储每个节点的权重容器private static Map<String,Weight> weightMap = new HashMap<>();// 盘算总权重值,只须要盘算一次,因此放在静态代码块中实行private static int weightTotal = 0;static {sumWeightTotal();}// 求和总权重值,后续动态伸缩节点时,再次调用该方法即可。public static void sumWeightTotal(){for (Integer weight : Servers.WEIGHT_SERVERS.values()) {weightTotal += weight;}}// 获取处理惩罚本次哀求的详细服务器IPpublic static String getServer(){// 判断权重容器中是否有节点信息if (weightMap.isEmpty()){// 假如没有则将设置的权重服务器列表挨个载入容器Servers.WEIGHT_SERVERS.forEach((servers, weight) -> {// 初始化时,每个节点的动态权重值都为0weightMap.put(servers, new Weight(servers, weight, 0));});}// 每次哀求时,更改动态权重值for (Weight weight : weightMap.values()) {weight.setCurrentWeight(weight.getCurrentWeight()+ weight.getWeight());}// 判断权重容器中最大的动态权重值Weight maxCurrentWeight = null;for (Weight weight : weightMap.values()) {if (maxCurrentWeight == null || weight.getCurrentWeight()> maxCurrentWeight.getCurrentWeight()){maxCurrentWeight = weight;}}// 末了用最大的动态权重值减去所有节点的总权重值maxCurrentWeight.setCurrentWeight(maxCurrentWeight.getCurrentWeight()- weightTotal);// 返回最大的动态权重值对应的节点IPreturn maxCurrentWeight.getServer();}public static void main(String[] args){// 利用for循环模拟6次哀求for (int i = 1; i <= 6; i++){System.out.println("第"+ i + "个哀求:" + getServer());}}}/********输出结果********/第1个哀求:44.120.110.001:8080第2个哀求:44.120.110.002:8081第3个哀求:44.120.110.001:8080第4个哀求:44.120.110.003:8082第5个哀求:44.120.110.002:8081第6个哀求:44.120.110.001:8080先看结果,对比之前的实现方式而言,该算法在分发哀求时,确实匀称了许多许多。
而且哀求分发的数量与我们设置的权重值也碰巧符合合:

  • 44.120.110.001:8080:3 次
  • 44.120.110.002:8081:2 次
  • 44.120.110.003:8082:1 次
这是不是很神奇?怎样做到的呢,接下来简单聊一下该算法的核心头脑。
在之前的权重算法中,服务器列表中只有两个值:服务器 IP、对应的权重值,而在当前这种算法中,须要再引入一个动态权重值的概念。
以是我们再上述案例中,将服务器的列表抽象成了一个 Weight 类,在该类中除开本来的 servers、weight 之外,多添加了一个字段 currentWeight,用于记载每个节点的动态权重(该值是变化的)。
在该算法中,会先盘算已设置的权重值总和,然后第一次哀求,会初始化权重容器 weightMap,将每个设置的节点都封装成一个 Weight 对象,并将其动态权重值初始化为 0。
如下:

  • Weight("server":"44.120.110.001:8080","weight":3,"currentWeight":0)
  • Weight("server":"44.120.110.002:8081","weight":2,"currentWeight":0)
  • Weight("server":"44.120.110.003:8082","weight":1,"currentWeight":0)
OK,至此准备工作停当,接下来是算法的核心过程,重要分为三步:

  • 用本来的动态权重值加一次每个节点的静态权重值,盘算出新的动态权重值。
  • 遍历权重容器,找出动态权重值最大的节点,将其作为处理惩罚本次哀求的节点。
  • 用最大的动态权重值减去已设置的静态权重值总和,为一下轮分发做准备。
联合上述的算法过程和前面给出的案例,把整个过程摊开分析一次:
2.png 上表中列出了六次哀求的处理惩罚过程,整个过程到末了,动态权重值又会回归初始值:0,0,0,然后开启新的一轮盘算,周而复始之,格外的神奇_。
平滑加权轮询算法也是应用最为广泛的轮询算法,在 Dubbo、Robbin、Nginx、Zookeeper 等一些集群情况中,当你设置了权重时,默认接纳的就是该算法作为哀求分发的战略。
同等性哈希算法
实在平滑加权轮询算法对于哀求分发而言,是一种比力精良的战略了,不外前面分析的所有战略,都存在一个致命标题:不能确保同一客户端的所有哀求都分发在同一台服务器处理惩罚,因此无法实现有状态的哀求。
好比最简单的登录功能,客户端发送哀求登录乐成,然后将其登录的状态生存在 session 中,结果客户端的第二次哀求被分发到了别的一台呆板。
由于第二台服务器 session 中没有相干的登录信息,因此会要求客户端重新登录,这显然造成的用户体验感是极差的,那么对于这种标题又该怎样办理呢?
重要有两种方案:

  • 接纳外部中心件存储 session,比方 Redis,然后从 Redis 中获取登录状态。
  • 接纳特别的哀求分发战略,确保同一客户端的所有哀求都会去到同一台呆板上处理惩罚。
同等性哈希算法就是一种可以或答应以大概确保同一客户端的所有哀求都会被分发到同一台呆板的战略,不外同等性哈希算法仍旧会存在标题,就是当集群中某个节点下线,大概集群出现拓展时,那么也会影响终极分发的目的呆板。
以是一样平常同等性哈希算法并不能 100% 办理 session 同等性的标题,因此该算法一样平常很少用于网关层的哀求分发,更多的场景是应用在分布式缓存等情况,接下来一起来看看。
| 通过其他分发算法实现缓存

在解说同等性哈希算法之前,各人先来简单明白一下同等性哈希算法的产生配景。
先思考一个标题:假设目前单台缓存服务器无法负担外部的访问压力,此刻会怎样去做呢?
答案是增长新的缓存服务器节点,拓展出一个集群对外提供服务。
好的,那标题又来了,现在缓存服务器是一个集群情况,此刻来了一个哀求后该落入哪个节点呢?
假设接纳轮询战略,那么写入 xxx 缓存信息的哀求被分发到了第一个节点,客户端读取 xxx 时,哀求又被分发到了第三个节点上,那么显然是读不到之前的缓存。
而且最关键的是,一样平常的轮询战略都是须要基于集群的节点数量举行哀求分发的,因此集群中的节点一旦出现伸缩,终极会导致所有缓存内容全部失效。
就拿最根本的取模轮询来说,本来集群是 3 个节点,以是是基于取模 3 去分发哀求,结果有台节点宕机了,成为了取模 2,那末了整个缓存体系分发哀求完全乱套.....
假如接纳随机战略.....,更不靠谱.....
因此在这种需求配景下,台甫鼎鼎的同等性哈希算法问世了,同等性哈希算法实在也利用的取模方式,只是,刚才形貌的取模轮询法是对服务器的数量举行取模,而同等性哈希算法是对 2^32 取模,什么意思呢?我们一点点来讲。
| 同等性哈希核心-哈希环

实现同等性哈希算法的核心结构在于哈希环,前面讲到过同等性哈希是基于 2^32 做取模。
那么起首可以将二的三十二次方想象成一个圆,这个圆统共由 2^32 个点构成,如下:
圆环的正上方第一个点代表 0,0 右侧的点按照 1、2、3、4....的次序依此类推,直到 2^32-1,也就是说 0 左侧的第一个点代表着 2^32-1。
终极这个在逻辑上由 2^32 个点构成的圆,被称为哈希环。
联合之前的缓存案例,假设有四台缓存服务器 A、B、C、D,然后再通过每台服务器的 IP 哈希值取模 2^32,终极一定会得到一个 2^32 范围之内的整数,这个数在哈希环上定然也对应着一个点。
那么每台服务器的 IP 就可以映射到哈希环上,如下:
到此时,服务器已经和哈希环创建起了接洽,那么此时当客户端发送哀求时,又可以通过雷同的盘算方式,将客户端须要利用的缓存 Key 举行雷同的哈希取模,然后同样将其映射到哈希环上。
比方写入一条缓存 name=竹子,如下:
那么此时该缓存纠结要落入到哪台服务器呢?答案是 B,为什么?由于在哈希环结构中,沿着顺时针方向走,遇到的第一台服务器是 B,以是终极会落到 B 服务器上。
固然,假如同等性哈希算法被用于哀求分发,那么就以用户的 IP 作为哈希取模的条件,如许就能确保同一个客户端的所有哀求都会被分发到同一台服务器。
同等性哈希算法中,就利用哈希环结构+哈希取模判断每个哀求该落入的服务器,由于服务器 IP、客户端 IP 或缓存的 Key 都是雷同的,以是在服务器数量稳定的情况,雷同的哈希条件举行哈希取模,终极盘算出来的值永久都是雷同的。
然后再通过盘算出的值,在哈希环结构上举行顺时针查找,可以大概定位到的服务器也是雷同的,以是雷同属性的哀求永久会落入到同一服务器。
| 哈希环的映射偏移标题

经过上述分析后,似乎发现同等性哈希算法没啥大毛病,但上述中属于“理想状态”:
可偏偏理想很丰满,现实却很骨感,现实映射服务器 IP 的过程中,大概会出现如下情况:
由于服务器 IP 哈希取模后,无法确保哈希得到的数字可以大概匀称分布,因此就有大概造成如上情况,所有的服务器IP都被映射在“一块儿”,终极导致 A 服务器承载了 90% 以上的访问压力。
| 映射偏移造成的宕机连锁反应

接上述,假如服务器 IP 映射在哈希环上出现偏移,在大流量的冲击下,这种情况很轻易导致整个集群崩塌,起首是A扛不住并发冲击,宕机下线,紧接着流量交给 B,B 也扛不住,接着宕机,然后 C.....
因此哈希环映射偏移标题大概会造成的一系列连锁反应,以是在同等性哈希算法中,为了确保整个集群的坚固性,提出了一种捏造节点的概念来办理此标题。
捏造节点实在本质上就是真实服务器节点的复制品,捏造节点映射的 IP 都是指向于真实服务器的。
就类似平常 .EXE 软件的快捷方式,现在为 QQ 创建了一个快捷方式,然后拷贝到了十个差别的目次下,但本质上这十个快捷方式指向的启动文件都是雷同 exe 步调。
哈希环中的捏造节点也同理,如下:
从上图中可以看出,A、B、C、D 四台服务器分别都映射出了一个捏造节点,引入捏造节点后会显着感觉出来,本来 A 服务器须要承载 90% 以上的流量,但此刻映射出的捏造节点大大减轻了 A 的压力,将流量均派到了集群中的每个节点。
在同等性哈希算法的现实应用场景中,绝非只映射一个捏造节点,每每会为一个真实节点映射数十个捏造节点,以便于减小哈希环偏移所带来的影响。
同时,捏造节点的数量越多,哀求在分发时也能更匀称的分布,哈希环终极结构如下:
8.png | Java 实现同等性哈希算法

讲了这么多,那么同等性哈希算法究竟怎样实现呢?接下来一起看看:
public class Servers {public static List<String> SERVERS = Arrays.asList("44.120.110.001:8080","44.120.110.002:8081","44.120.110.003:8082","44.120.110.004:8083","44.120.110.005:8084");}public class ConsistentHash {// 利用有序的红黑树结构,用于实现哈希环结构private static TreeMap<Integer,String> virtualNodes = new TreeMap<>();// 每个真实节点的捏造节点数量private static final int VIRTUAL_NODES = 160;static {// 对每个真实节点添加捏造节点,捏造节点会根据哈希算法举行散列for (String serverIP : Servers.SERVERS) {// 将真实节点的IP映射到哈希环上virtualNodes.put(getHashCode(serverIP), serverIP);// 根据设定的捏造节点数量举行捏造节点映射for (int i = 0; i < VIRTUAL_NODES; i++){// 盘算出一个捏造节点的哈希值(只要差别即可)int hash = getHashCode(serverIP + i);// 将捏造节点添加到哈希环结构上virtualNodes.put(hash, serverIP);}}}public static String getServer(String IP){int hashCode = getHashCode(IP);// 得到大于该Hash值的子红黑树SortedMap<Integer, String> sortedMap = virtualNodes.tailMap(hashCode);// 得到该树的第一个元素,也就是最小的元素Integer treeNodeKey = sortedMap.firstKey();// 假如没有大于该元素的子树了,则取整棵树的第一个元素,相称于取哈希环中的最小元素if (sortedMap == null)treeNodeKey = virtualNodes.firstKey();// 返回对应的捏造节点名称return virtualNodes.get(treeNodeKey);}// 哈希方法:用于盘算一个IP的哈希值public static int getHashCode(String IP){final int p = 1904390101;int hash = (int)1901102097L;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;}public static void main(String[] args){// 用for循环模拟五个差别的IP访问for (int i = 1; i <= 5; i++){System.out.println("第"+ i + "个哀求:" + getServer("192.168.12.13"+i));}System.out.println("-----------------------------");// 用for循环模拟三个雷同的IP访问for (int i = 1; i <= 3; i++){System.out.println("第"+ i + "个哀求:" + getServer("192.168.12.131"));}}}/********输出结果*******/第1个哀求:44.120.110.002:8081第2个哀求:44.120.110.003:8082第3个哀求:44.120.110.004:8083第4个哀求:44.120.110.003:8082第5个哀求:44.120.110.004:8083-----------------------------第1个哀求:44.120.110.002:8081第2个哀求:44.120.110.002:8081第3个哀求:44.120.110.002:8081上述便是 Java 实现同等性哈希算法的全过程,实在并不难明白,内里用到了 TreeMap 实现了哈希环结构,而且指定了每个服务器节点的捏造节点数量,同时实现了一个简单的哈希方法,用于盘算入参的哈希值。
算法过程如下:

  • 启动时先根据指定的数量,映射对应的捏造节点数量在哈希环上。
  • 通过盘算客户端哈希值,然后在哈希环上取得大于该值的节点,然后返回对应的 IP。由于哈希环是取顺时针方向的第一个节点作为处理惩罚哀求的目的服务器,以是获取大于该哈希值的节点中的第一个节点即可。
  • 假如哈希环中没有大于客户端哈希值的节点,那么则将这些客户端的哀求分发到整个 Map 上的第一台服务器,以后实现哈希闭环。
同等性哈希算法由于其特性,因此一样平常多被用于分布式缓存中的集群分片,尤其是 MemCache 的缓存分片,就是接纳同等性哈希算法实现的。
而 Redis 自身推出的 RedisCluster 分片集群中,也借用了同等性哈希算法的头脑,不外举行了改版实现,内部接纳 CRC16+HashSolt 实现了缓存分片,但核心头脑也是雷同的。
固然,文中给出的算法过程都是较为简单的实现,如若想要参考完备的实现,可以参考 :

  • Dubbo 的 com.alibaba.dubbo.rpc.cluster.loadbalance 包
  • 或参考 SpringCloudRibbon 的 com.netflix.loadbalancer 包下的实现
最小生动数算法
上述分析的根本算法、平滑轮询加权、同等性哈希等算法都属于静态算法,也就是说这些算法设置后,并不会根据线上的现实运行情况举行调解,只会根据已设置的规则举行哀求分发。
最小生动数算法则会根据线上的现实情况举行分发,可以大概机动的检测出集群中各个节点的状态,可以大概自动探求并调用生动度最低的节点处理惩罚哀求。
Java 实现如下:
// 节点类:用于封装集群中的每个节点public class Server {private String IP;private AtomicInteger active;// private Integer weight;public Server(){}public Server(String IP,int active) {this.IP = IP;// 将外部转达的生动数作为默认生动数this.active = new AtomicInteger(active);}public String getIP() {// 每分发一个哀求时自增一次生动数active.incrementAndGet();return IP;}public AtomicInteger getActive() {return active;}}// 集群类:用于模拟集群节点列表public class Servers {// 生动度衰减器public static void attenuator(){new Thread(()->{// 遍历集群中的所有节点for (Server server : Servers.SERVERS) {// 假如生动度不为0if (server.getActive().get() != 0){// 则自减一个生动度server.getActive().getAndDecrement();}}try {// 每隔 2 秒中衰减一次生动度Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}).start();}// 模拟的集群节点信息,生动数最开始默以为0public static List<Server> SERVERS = Arrays.asList(new Server("44.120.110.001:8080",0),new Server("44.120.110.002:8081",0),new Server("44.120.110.003:8082",0));}// 最小生动数算法实现类public class LeastActive {public static String getServer(){// 初始化最小生动数和最小生动数的节点int leastActive = Integer.MAX_VALUE;Server leastServer = new Server();// 遍历集群中的所有节点for (Server server : Servers.SERVERS) {// 找出生动数最小的节点if (leastActive > server.getActive().get()){leastActive = server.getActive().get();leastServer = server;}}// 返回生动数最小的节点IPreturn leastServer.getIP();}public static void main(String[] args){Servers.attenuator();for (int i = 1; i <= 10; i++){System.out.println("第"+ i + "个哀求:" + getServer());}}}/********运行结果*********/第1个哀求:44.120.110.001:8080第2个哀求:44.120.110.002:8081第3个哀求:44.120.110.003:8082第4个哀求:44.120.110.001:8080第5个哀求:44.120.110.002:8081第6个哀求:44.120.110.003:8082第7个哀求:44.120.110.001:8080第8个哀求:44.120.110.002:8081第9个哀求:44.120.110.003:8082第10个哀求:44.120.110.001:8080观察如上案例的运行结果,似乎结果似乎是轮询的结果呀?确实是的,这是由于在最开始,所有节点的生动数都为 0,三个节点的生动数都雷同。
以是默认会先取集群中的第一个生动数为 0 的节点处理惩罚哀求,第一个节点的生动数会酿成 1,第二次哀求时最小生动数也为 0,然后取第二个节点处理惩罚哀求,依此类推......
在线上情况下,不会出现轮询的结果,由于每台服务器随着运行时间的增长,生动数一定会差别,因此该算法总会取生动数最小的节点提供服务。
固然,上述案例中实现的最小生动数,是比力浅易的版本,对于完满的实现可以参考 Dubbo 框架中的 com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance 类,此中也实现了权重机制。
简单叙述一下此中的原理实现:

  • 先从注册中心中拉取所有的服务实例,然后找出生动数最小的节点。
  • 假如只有一个,那么则直接返回对应的实例节点处理惩罚本次哀求。
  • 假如存在多个,则根据每个节点设置的权重值来决定本次处理惩罚哀求的详细节点。
  • 假如权重值差别,优先选取权重值最大的实例,作为处理惩罚本次哀求的节点。
  • 假如存在雷同的最大权重值,那么则通过随机的方式选择一个节点提供服务。
固然,由于须要对每个节点去实现生动数监听,以是在 Dubbo 框架中,想要设置最小生动数战略,那么须要起首启用 ActiveLimitFilter 记载每个节点的生动数。
大概也可以参考 Ribbon 框架 com.netflix.loadbalancer 包下面的 BestAvailableRule 最小生动数算法实现类。
从最小生动数算法特性不难过知,该算法带来的上风极为显着,永久都能选取节点列表中最空闲的那台服务器处理惩罚哀求,从而制止某些负载过高的节点,还仍旧负担须要负担新的流量访问,造成更大的压力。
最优相应算法
与前面分析的最小生动数算法一样,最优相应算法也是一种动态算法,但它比最小生动数算法更加智能,由于最小生动数算法中,假如一台节点存在故障,导致它自身处理惩罚的哀求数比力少,那么它会遭受最大的访问压力,这显然是并不公道的。
最小生动数算法就类似于平常的搬砖工作,谁事变做的最少谁留下来加班,在正常情况下,这种算法都可以大概找到“摸鱼”最锋利的员工留下来加班。
但假如有一天,某个员工由于身材出标题了,导致本身做的工作量比力少,但按照这种算法的逻辑,仍旧会判断为该员工本日最闲,以是留下来加班。
从上述这个案例中,各人略微可以大概感受出来最小生动数算法的不公道性。
而最优相应算法则更加智能,该算法在开始前,会对服务列表中的各节点发出一个探测哀求(比方 Ping 或心跳包检测),然后根据各节点的相应时间来决定由哪台服务器处理惩罚客户端哀求,该算法能较好根据节点列表中每台呆板的当前运行状态分发哀求。
Java 实现如下:
public class Servers {// 模拟的集群节点信息,生动数最开始默以为0public static List<Server> SERVERS = Arrays.asList(new Server("44.120.110.001:8080"),new Server("44.120.110.002:8081"),new Server("44.120.110.003:8082"));}public class Server {private String IP;public Server(){}public Server(String IP) {this.IP = IP;}public String getIP() {return IP;}public void setIP(String IP){this.IP = IP;}public String ping(){// 天生一个1000~3000之间的随机数int random = ThreadLocalRandom.current().nextInt(1000, 2000);try {// 随机休眠一段时间,模拟差别的相应速率Thread.sleep(random);} catch (InterruptedException e) {e.printStackTrace();}// 末了返回自身的IPreturn this.IP;}}public class ResponseTime {// 创建一个定长的线程池,用于去实行ping任务static ExecutorService pingServerPool = Executors.newFixedThreadPool(Servers.SERVERS.size());public static String getServer() throws InterruptedException {// 创建一个CompletableFuture用于拼接任务CompletableFuture cfAnyOf;// 创建一个汲取结果返回的server节点对象final Server resultServer = new Server();// 根据集群节点数量初始化一个异步任务数组CompletableFuture[] cfs = new CompletableFuture[Servers.SERVERS.size()];// 遍历整个服务器列表,为每个节点创建一个ping任务for (Server server : Servers.SERVERS) {// 获取当前节点在集群列表中的下标int index = Servers.SERVERS.indexOf(server);// 为每个节点创建一个ping任务,并交给pingServerPool线程池实行CompletableFuture<String> cf =CompletableFuture.supplyAsync(server::ping,pingServerPool);// 将创建好的异步任务参加数组中cfs[index] = cf;}// 将创建好的多个Ping任务组合成一个聚合任务并实行cfAnyOf = CompletableFuture.anyOf(cfs);// 监听实行完成后的回调,谁先实行完成则返回谁cfAnyOf.thenAccept(resultIP -> {System.out.println("开始相应检测哀求的节点为:" + resultIP);resultServer.setIP((String) resultIP);});// 壅闭主线程一段时间,防止CompletableFuture退出Thread.sleep(3000);// 返回开始相应检测哀求(ping)的节点作为本次处理惩罚客户端哀求的节点return resultServer.getIP();}public static void main(String[] args) throws InterruptedException {for (int i = 1; i <= 5; i++){System.out.println("第"+ i + "个哀求:" + getServer());}}}/******运行结果:******/开始相应检测哀求的节点为:44.120.110.002:8081第1个哀求:44.120.110.002:8081开始相应检测哀求的节点为:44.120.110.002:8081第2个哀求:44.120.110.002:8081开始相应检测哀求的节点为:44.120.110.003:8082第3个哀求:44.120.110.003:8082开始相应检测哀求的节点为:44.120.110.003:8080第4个哀求:44.120.110.001:8080开始相应检测哀求的节点为:44.120.110.002:8081第5个哀求:44.120.110.002:8081在该案例中,实在现过程对比之前的算法略微复杂一些,起首在 Server 实例类中界说了一个 Ping() 方法,该方法中利用随机数+线程休眠的方式简单模拟了一下节点的差别的相应速率。
然后在算法实现类中,利用 CompletableFuture 分别对每一个节点都创建了对应的 Ping 任务,然后同时实行,又通过 thenAccept() 回调方法监听了实行结果,谁开始相应,则取其作为处理惩罚本次哀求的节点。
这个算法的实现过程中,唯一难明白的就是 CompletableFuture,它是 JDK8 中推出的一种异步任务。
这里只是举例实现,以是通过 CompletableFuture 实现了检测哀求,但现实过程中假如要选择这种算法,那么基于 Netty 会更为符合。
从上述案例的运行结果中也可以得知:最优相应算法无论在何种情况下,都能从集群中选取性能最好的节点对外服务,Nginx 中也支持设置这种算法,但须要先安装对应的 nginx-upstream-fair 模块。
总结
在本文中,对于比力常用的哀求分发算法举行了分析及手写实践,此中提到了较为传统的静态调理算法:轮询、随机、加权、同等性哈希等,也谈到了一些较为智能的动态算法:最小生动数、最优相应等。
但须要牢记的一点是:并非越智能的算法越好,越是并发高、流量大的场景下,反而选用最根本的算法更符合,比方微信的红包业务,就是接纳最根本的轮询算法举行集群调理。
那这又是为何呢?由于越智能的调理算法,举行节点选择时的开销会更大,假如你对于文中给出的调理算法实现都逐一运行过,那么各人会显着感知出:越到背面的算法,分发哀求的速率越慢。
因此在面对巨大访问压力的景象中,选择最简单的算法反而带来的收益更高,但条件是须要集群中所有的节点硬件设置都同等,所有节点分配的资源都雷同,轮询算法则是最佳的调理算法。
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-19 02:26, Processed in 0.161768 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表