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

本文涉及的产品
应用型负载均衡 ALB,每月750个小时 15LCU
传统型负载均衡 CLB,每月750个小时 15LCU
云原生网关 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不是本案例的重点, 细节暂不展开讲了.

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
相关文章
|
2月前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
319 35
|
7月前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
2月前
|
存储 算法 搜索推荐
《数据之美》:Java数据结构与算法精要
本系列深入探讨数据结构与算法的核心原理及Java实现,涵盖线性与非线性结构、常用算法分类、复杂度分析及集合框架应用,助你提升程序效率,掌握编程底层逻辑。
|
7月前
|
存储 缓存 监控
上网行为监控系统剖析:基于 Java LinkedHashMap 算法的时间序列追踪机制探究
数字化办公蓬勃发展的背景下,上网行为监控系统已成为企业维护信息安全、提升工作效能的关键手段。该系统需实时记录并深入分析员工的网络访问行为,如何高效存储和管理这些处于动态变化中的数据,便成为亟待解决的核心问题。Java 语言中的LinkedHashMap数据结构,凭借其独有的有序性特征以及可灵活配置的淘汰策略,为上网行为监控系统提供了一种兼顾性能与功能需求的数据管理方案。本文将对LinkedHashMap在上网行为监控系统中的应用原理、实现路径及其应用价值展开深入探究。
188 3
|
2月前
|
存储 人工智能 算法
从零掌握贪心算法Java版:LeetCode 10题实战解析(上)
在算法世界里,有一种思想如同生活中的"见好就收"——每次做出当前看来最优的选择,寄希望于通过局部最优达成全局最优。这种思想就是贪心算法,它以其简洁高效的特点,成为解决最优问题的利器。今天我们就来系统学习贪心算法的核心思想,并通过10道LeetCode经典题目实战演练,带你掌握这种"步步为营"的解题思维。
|
7月前
|
人工智能 算法 NoSQL
LRU算法的Java实现
LRU(Least Recently Used)算法用于淘汰最近最少使用的数据,常应用于内存管理策略中。在Redis中,通过`maxmemory-policy`配置实现不同淘汰策略,如`allkeys-lru`和`volatile-lru`等,采用采样方式近似LRU以优化性能。Java中可通过`LinkedHashMap`轻松实现LRUCache,利用其`accessOrder`特性和`removeEldestEntry`方法完成缓存淘汰逻辑,代码简洁高效。
308 0
|
6月前
|
Java 应用服务中间件 Linux
在Java 12环境中配置和部署Apache Tomcat的步骤。
这段部署Tomcat的冒险旅程充满技术挑战,但同时也像游戏一样充满乐趣。它需要你提前准备,仔细执行,并随时准备解决意外情况。成功后,你就可以在这匹强壮的网络野马上,带着你的Java应用,冲向Web开发的璀璨星空。
216 56
|
6月前
|
存储 算法 安全
Java中的对称加密算法的原理与实现
本文详细解析了Java中三种常用对称加密算法(AES、DES、3DES)的实现原理及应用。对称加密使用相同密钥进行加解密,适合数据安全传输与存储。AES作为现代标准,支持128/192/256位密钥,安全性高;DES采用56位密钥,现已不够安全;3DES通过三重加密增强安全性,但性能较低。文章提供了各算法的具体Java代码示例,便于快速上手实现加密解密操作,帮助用户根据需求选择合适的加密方案保护数据安全。
423 58
|
5月前
|
机器学习/深度学习 算法 Java
Java实现林火蔓延路径算法
记录正在进行的森林防火项目中林火蔓延功能,本篇文章可以较好的实现森林防火蔓延,但还存在很多不足,如:很多参数只能使用默认值,所以蔓延范围仅供参考。(如果底层设备获取的数据充足,那当我没说)。注:因林火蔓延涉及因素太多,如静可燃物载量、矿质阻尼系数等存在估值,所以得出的结果仅供参考。
98 4