Java实现一致性哈希算法,并搭建环境测试其负载均衡特性(一)

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
简介: 实现负载均衡是后端领域一个重要的话题,一致性哈希算法是实现服务器负载均衡的方法之一,你很可能已在一些远程服务框架中使用过它。下面我们尝试一下自己实现一致性哈希算法。

一. 简述一致性哈希算法

这里不详细介绍一致性哈希算法的起源了,网上能方便地搜到许多介绍一致性哈希算法的好文章。本文主要想动手实现一致性哈希算法,并搭建一个环境进行实战测试。

在开始之前先整理一下算法的思路:

一致性哈希算法通过把每台服务器的哈希值打在哈希环上,把哈希环分成不同的段,然后对到来的请求计算哈希值从而得知该请求所归属的服务器。这个办法解决了传统服务器增减机器时需要重新计算哈希的麻烦。

但如果服务器的数量较少,可能导致计算出的哈希值相差较小,在哈希环上分布不均匀,导致某台服务器过载。为了解决负载均衡问题,我们引入虚拟节点技术,为每台服务器分配一定数量的节点,通过节点的哈希值在哈希环上进行划分。这样一来,我们就可以根据机器的性能为其分配节点,性能好就多分配一点,差就少一点,从而达到负载均衡。

二. 实现一致性哈希算法

奠定了整体思路后我们开始考虑实现的细节

  1. 哈希算法的选择

选择能散列出32位整数的 FNV 算法, 由于该哈希函数可能产生负数, 需要作取绝对值处理.

  1. 请求节点在哈希环上寻找对应服务器的策略

策略为:新节点寻找最近比且它大的节点, 比如说现在已经有环[0, 5, 7, 10], 来了个哈希值为6的节点, 那么它应该由哈希值为7对应的服务器处理. 如果请求节点所计算的哈希值大于环上的所有节点, 那么就取第一个节点. 比如来了个11, 将分配到0所对应的节点.

  1. 哈希环的组织结构

开始的时候想过用顺序存储的结构存放,但是在一致性哈希中,最频繁的操作是在集合中查找最近且比目标大的数. 如果用顺序存储结构的话,时间复杂度是收敛于O(N)的,而树形结构则为更优的O(logN)。

但凡事有两面,采用树形结构存储的代价是数据初始化的效率较低,而且运行期间如果有节点插入删除的话效率也比较低。但是在现实中,服务器在一开始注册后基本上就不怎么变了,期间增减机器,宕机,机器修复等事件的频率相比起节点的查询简直是微不足道。所以本案例决定使用使用树形结构存储。

贴合上述要求,并且提供有序存储的,首先想到的是红黑树,而且Java中提供了红黑树的实现 TreeMap

  1. 虚拟节点与真实节点的映射关系

如何确定一个虚拟节点对应的真实节点也是个问题。理论上应该维护一张表记录真实节点与虚拟节点的映射关系。本案例为了演示,采用简单的字符串处理。

比方说服务器 192.168.0.1:8888分配了 1000 个虚拟节点, 那么它的虚拟节点名称从 192.168.0.1:8888@1一直到 192.168.0.1:8888@1000。通过这样的处理,我们在通过虚拟节点找真实节点时只需要裁剪字符串即可。

计划定制好后, 下面是具体代码:

  1. publicclassConsistentHashTest{
  2.    /**
  3.     * 服务器列表,一共有3台服务器提供服务, 将根据性能分配虚拟节点
  4.     */
  5.    publicstaticString[] servers ={
  6.            "192.168.0.1#100",//服务器1: 性能指数100, 将获得1000个虚拟节点
  7.            "192.168.0.2#100",//服务器2: 性能指数100, 将获得1000个虚拟节点
  8.            "192.168.0.3#30"   //服务器3: 性能指数30,  将获得300个虚拟节点
  9.    };
  10.    /**
  11.     * 真实服务器列表, 由于增加与删除的频率比遍历高, 用链表存储比较划算
  12.     */
  13.    privatestaticList<String> realNodes =newLinkedList<>();
  14.    /**
  15.     * 虚拟节点列表
  16.     */
  17.    privatestaticTreeMap<Integer,String> virtualNodes =newTreeMap<>();

  18.    static{
  19.        for(String s : servers){
  20.            //把服务器加入真实服务器列表中
  21.            realNodes.add(s);
  22.            String[] strs = s.split("#");
  23.            //服务器名称, 省略端口号
  24.            String name = strs[0];
  25.            //根据服务器性能给每台真实服务器分配虚拟节点, 并把虚拟节点放到虚拟节点列表中.
  26.            int virtualNodeNum =Integer.parseInt(strs[1])*10;
  27.            for(int i =1; i <= virtualNodeNum; i++){
  28.                virtualNodes.put(FVNHash(name +"@"+ i), name +"@"+ i);
  29.            }
  30.        }
  31.    }

  32.    publicstaticvoid main(String[] args){
  33.        newThread(newRequestProcess()).start();
  34.    }

  35.    staticclassRequestProcessimplementsRunnable{
  36.        @Override
  37.        publicvoid run(){
  38.            String client =null;
  39.            while(true){
  40.                //模拟产生一个请求
  41.                client = getN()+"."+ getN()+"."+ getN()+"."+ getN()+":"+(1000+(int)(Math.random()*9000));
  42.                //计算请求的哈希值
  43.                int hash =FVNHash(client);
  44.                //判断请求将由哪台服务器处理
  45.                System.out.println(client +" 的请求将由 "+ getServer(client)+" 处理");
  46.                try{
  47.                    Thread.sleep(500);
  48.                }catch(InterruptedException e){
  49.                    e.printStackTrace();
  50.                }
  51.            }

  52.        }
  53.    }

  54.    privatestaticString getServer(String client){
  55.        //计算客户端请求的哈希值
  56.        int hash =FVNHash(client);
  57.        //得到大于该哈希值的所有map集合
  58.        SortedMap<Integer,String> subMap = virtualNodes.tailMap(hash);
  59.        //找到比该值大的第一个虚拟节点, 如果没有比它大的虚拟节点, 根据哈希环, 则返回第一个节点.
  60.        Integer targetKey = subMap.size()==0? virtualNodes.firstKey(): subMap.firstKey();
  61.        //通过该虚拟节点获得真实节点的名称
  62.        String virtualNodeName = virtualNodes.get(targetKey);
  63.        String realNodeName = virtualNodeName.split("@")[0];
  64.        return realNodeName;
  65.    }

  66.    publicstaticint getN(){
  67.        return(int)(Math.random()*128);
  68.    }

  69.    publicstaticintFVNHash(String data){
  70.        finalint p =16777619;
  71.        int hash =(int)2166136261L;
  72.        for(int i =0; i < data.length(); i++)
  73.            hash =(hash ^ data.charAt(i))* p;
  74.        hash += hash <<13;
  75.        hash ^= hash >>7;
  76.        hash += hash <<3;
  77.        hash ^= hash >>17;
  78.        hash += hash <<5;
  79.        return hash <0?Math.abs(hash): hash;
  80.    }
  81. }

  82. /* 运行结果片段
  83. 55.1.13.47:6240     的请求将由 192.168.0.1 处理
  84. 5.49.56.126:1105    的请求将由 192.168.0.1 处理
  85. 90.41.8.88:6884     的请求将由 192.168.0.2 处理
  86. 26.107.104.81:2989  的请求将由 192.168.0.2 处理
  87. 114.66.6.56:8233    的请求将由 192.168.0.1 处理
  88. 123.74.52.94:5523   的请求将由 192.168.0.1 处理
  89. 104.59.60.2:7502    的请求将由 192.168.0.2 处理
  90. 4.94.30.79:1299     的请求将由 192.168.0.1 处理
  91. 10.44.37.73:9332    的请求将由 192.168.0.2 处理
  92. 115.93.93.82:6333   的请求将由 192.168.0.2 处理
  93. 15.24.97.66:9177    的请求将由 192.168.0.2 处理
  94. 100.39.98.10:1023   的请求将由 192.168.0.2 处理
  95. 61.118.87.26:5108   的请求将由 192.168.0.2 处理
  96. 17.79.104.35:3901   的请求将由 192.168.0.1 处理
  97. 95.36.5.25:8020     的请求将由 192.168.0.2 处理
  98. 126.74.56.71:7792   的请求将由 192.168.0.2 处理
  99. 14.63.56.45:8275    的请求将由 192.168.0.1 处理
  100. 58.53.44.71:2089    的请求将由 192.168.0.3 处理
  101. 80.64.57.43:6144    的请求将由 192.168.0.2 处理
  102. 46.65.4.18:7649     的请求将由 192.168.0.2 处理
  103. 57.35.27.62:9607    的请求将由 192.168.0.2 处理
  104. 81.114.72.3:3444    的请求将由 192.168.0.1 处理
  105. 38.18.61.26:6295    的请求将由 192.168.0.2 处理
  106. 71.75.18.82:9686    的请求将由 192.168.0.2 处理
  107. 26.11.98.111:3781   的请求将由 192.168.0.1 处理
  108. 62.86.23.37:8570    的请求将由 192.168.0.3 处理
  109. */

经过上面的测试我们可以看到性能较好的服务器1和服务器2分担了大部分的请求,只有少部分请求落到了性能较差的服务器3上,已经初步实现了负载均衡。

下面我们将结合zookeeper,搭建一个更加逼真的服务器集群,看看在部分服务器上线下线的过程中,一致性哈希算法是否仍能够实现负载均衡。

三. 结合zookeeper搭建环境

环境介绍

首先会通过启动多台虚拟机模拟服务器集群,各台服务器都提供一个相同的接口供消费者消费。

同时会有一个消费者线程不断地向服务器集群发起请求,这些请求会经过一致性哈希算法均衡负载到各个服务器。

为了能够模拟上述场景, 我们必须在客户端维护一个服务器列表, 使得客户端能够通过一致性哈希算法选择服务器发送。(现实中可能会把一致性哈希算法实现在前端服务器, 客户先访问前端服务器, 再路由到后端服务器集群)。

但是我们的重点是模拟服务器的宕机和上线,看看一致性哈希算法是否仍能实现负载均衡。所以客户端必须能够感知服务器端的变化并动态地调整它的服务器列表。

为了完成这项工作, 我们引入 zookeeper, zookeeper的数据一致性算法保证数据实时, 准确, 客户端能够通过 zookeeper得知实时的服务器情况。

具体操作是这样的: 服务器集群先以临时节点的方式连接到 zookeeper, 并在 zookeeper上注册自己的接口服务(注册节点). 客户端连接上 zookeeper后, 把已注册的节点(服务器)添加到自己的服务器列表中。

如果有服务器宕机的话, 由于当初注册的是瞬时节点的原因, 该台服务器节点会从 zookeeper中注销。客户端监听到服务器节点有变时, 也会动态调整自己的服务器列表, 把当宕机的服务器从服务器列表中删除, 因此不会再向该服务器发送请求, 负载均衡的任务将交到剩余的机器身上。

当有服务器从新连接上集群后, 客户端的服务器列表也会更新, 哈希环也将做出相应的变化以提供负载均衡。

具体操作:

I. 搭建 zookeeper集群环境:

  1. 创建3个 zookeeper服务, 构成集群. 在各自的 data文件夹中添加一个 myid文件, 各个id分别为 1,2,3.
    5.jpg

2.
重新复制一份配置文件, 在配置文件中配置各个 zookeeper的端口号. 本案例中三台 zookeeper分别在 2181,2182,2183端口
6.jpg


  1. 启动 zookeeper集群

由于zookeeper不是本案例的重点, 细节暂不展开讲了.

相关实践学习
SLB负载均衡实践
本场景通过使用阿里云负载均衡 SLB 以及对负载均衡 SLB 后端服务器 ECS 的权重进行修改,快速解决服务器响应速度慢的问题
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
7天前
|
监控 算法 网络协议
Java 实现局域网电脑屏幕监控算法揭秘
在数字化办公环境中,局域网电脑屏幕监控至关重要。本文介绍用Java实现这一功能的算法,涵盖图像采集、数据传输和监控端显示三个关键环节。通过Java的AWT/Swing库和Robot类抓取屏幕图像,使用Socket进行TCP/IP通信传输图像数据,并利用ImageIO类在监控端展示图像。整个过程确保高效、实时和准确,为提升数字化管理提供了技术基础。
40 15
|
3月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
101 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
3月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
64 3
|
1月前
|
域名解析 弹性计算 监控
slb测试基本配置检查
slb测试基本配置检查
94 60
|
13天前
|
缓存 算法 搜索推荐
Java中的算法优化与复杂度分析
在Java开发中,理解和优化算法的时间复杂度和空间复杂度是提升程序性能的关键。通过合理选择数据结构、避免重复计算、应用分治法等策略,可以显著提高算法效率。在实际开发中,应该根据具体需求和场景,选择合适的优化方法,从而编写出高效、可靠的代码。
25 6
|
1月前
|
监控 测试技术
slb测试会话保持功能
slb测试会话保持功能
37 6
|
1月前
|
弹性计算 负载均衡 监控
slb测试健康检查
slb测试健康检查
40 4
|
1月前
|
监控 负载均衡 容灾
slb测试配置
slb测试配置
33 5
|
2月前
|
Web App开发 定位技术 iOS开发
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
126 1
|
3月前
|
XML Java Maven
在 Cucumber 测试中自动将 Cucumber 数据表映射到 Java 对象
在 Cucumber 测试中自动将 Cucumber 数据表映射到 Java 对象
71 7