首页 > 编程语言 >一致性hash算法

一致性hash算法

时间:2025-01-08 17:47:25浏览次数:1  
标签:node hash Hash 算法 key 一致性 服务器 节点

一、一致性Hash算法背景

我们有三台缓存服务器编号node0node1node2,现在有3000万个key,希望可以将这些个key均匀的缓存到三台机器上,你会想到什么方案呢?

我们可能首先想到的方案是:取模算法hash(key)% N,即:对key进行hash运算后取模,N是机器的数量;

这样,对key进行hash后的结果对3取模,得到的结果一定是0、1或者2,正好对应服务器node0node1node2,存取数据直接找对应的服务器即可,简单粗暴,完全可以解决上述的问题;

 

 

取模算法虽然使用简单,但对机器数量取模,在集群扩容和收缩时却有一定的局限性:因为在生产环境中根据业务量的大小,调整服务器数量是常有的事;

而服务器数量N发生变化后hash(key)% N计算的结果也会随之变化!

比如:一个服务器节点挂了,计算公式从hash(key)% 3变成了hash(key)% 2,结果会发生变化,此时想要访问一个key,这个key的缓存位置大概率会发生改变,那么之前缓存key的数据也会失去作用与意义;

大量缓存在同一时间失效,造成缓存的雪崩,进而导致整个缓存系统的不可用,这基本上是不能接受的;

为了解决优化上述情况,一致性hash算法应运而生~

 

二、一致性Hash算法原理

 

算法原理

一致性哈希算法在 1997 年由麻省理工学院提出,是一种特殊的哈希算法,在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系;
一致性哈希解决了简单哈希算法在分布式哈希表(Distributed Hash Table,DHT)中存在的动态伸缩等问题;

一致性hash算法本质上也是一种取模算法;

不过,不同于上边按服务器数量取模,一致性hash是对固定值2^32取模;

IPv4的地址是4组8位2进制数组成,所以用2^32可以保证每个IP地址会有唯一的映射;

 

① hash环

我们可以将这2^32个值抽象成一个圆环⭕️,圆环的正上方的点代表0,顺时针排列,以此类推:1、2、3…直到2^32-1,而这个由2的32次方个点组成的圆环统称为hash环

② 服务器映射到hash环

在对服务器进行映射时,使用hash(服务器ip)% 2^32,即:

使用服务器IP地址进行hash计算,用哈希后的结果对2^32取模,结果一定是一个0到2^32-1之间的整数;

而这个整数映射在hash环上的位置代表了一个服务器,依次将node0node1node2三个缓存服务器映射到hash环上;

③ 对象key映射到服务器

在对对应的Key映射到具体的服务器时,需要首先计算Key的Hash值:hash(key)% 2^32

注:此处的Hash函数可以和之前计算服务器映射至Hash环的函数不同,只要保证取值范围和Hash环的范围相同即可(即:2^32);

将Key映射至服务器遵循下面的逻辑:

从缓存对象key的位置开始,沿顺时针方向遇到的第一个服务器,便是当前对象将要缓存到的服务器;

假设我们有 "semlinker"、"kakuqo"、"lolo"、"fer" 四个对象,分别简写为 o1、o2、o3 和 o4;

首先,使用哈希函数计算这个对象的 hash 值,值的范围是 [0, 2^32-1]:

图中对象的映射关系如下:

hash(o1) = k1; hash(o2) = k2;
hash(o3) = k3; hash(o4) = k4;

同时 3 台缓存服务器,分别为 CS1、CS2 和 CS3:

则可知,各对象和服务器的映射关系如下:

K1 => CS1
K4 => CS3
K2 => CS2
K3 => CS1

即:

以上便是一致性Hash的工作原理;

可以看到,一致性Hash就是:将原本单个点的Hash映射,转变为了在一个环上的某个片段上的映射!

下面我们来看几种服务器扩缩容的场景;

服务器扩缩容场景

① 服务器减少

假设 CS3 服务器出现故障导致服务下线,这时原本存储于 CS3 服务器的对象 o4,需要被重新分配至 CS2 服务器,其它对象仍存储在原有的机器上:

此时受影响的数据只有 CS2 和 CS3 服务器之间的部分数据!

② 服务器增加

假如业务量激增,我们需要增加一台服务器 CS4,经过同样的 hash 运算,该服务器最终落于 t1 和 t2 服务器之间,具体如下图所示:

此时,只有 t1 和 t2 服务器之间的部分对象需要重新分配;

在以上示例中只有 o3 对象需要重新分配,即它被重新到 CS4 服务器;

在前面我们已经说过:如果使用简单的取模方法,当新添加服务器时可能会导致大部分缓存失效,而使用一致性哈希算法后,这种情况得到了较大的改善,因为只有少部分对象需要重新分配!

数据偏斜&服务器性能平衡问题

引出问题

在上面给出的例子中,各个服务器几乎是平均被均摊到Hash环上;

但是在实际场景中很难选取到一个Hash函数这么完美的将各个服务器散列到Hash环上;

此时,在服务器节点数量太少的情况下,很容易因为节点分布不均匀而造成数据倾斜问题;

如下图被缓存的对象大部分缓存在node-4服务器上,导致其他节点资源浪费,系统压力大部分集中在node-4节点上,这样的集群是非常不健康的:

同时,还有另一个问题:

在上面新增服务器 CS4 时,CS4 只分担了 CS1 服务器的负载,服务器 CS2 和 CS3 并没有因为 CS4 服务器的加入而减少负载压力;如果 CS4 服务器的性能与原有服务器的性能一致甚至可能更高,那么这种结果并不是我们所期望的;

虚拟节点

针对上面的问题,我们可以通过:引入虚拟节点来解决负载不均衡的问题:

即将每台物理服务器虚拟为一组虚拟服务器,将虚拟服务器放置到哈希环上,如果要确定对象的服务器,需先确定对象的虚拟服务器,再由虚拟服务器确定物理服务器;

如下图所示:

在图中:o1 和 o2 表示对象,v1 ~ v6 表示虚拟服务器,s1 ~ s3 表示实际的物理服务器;

虚拟节点的计算

虚拟节点的hash计算通常可以采用:对应节点的IP地址加数字编号后缀 hash(10.24.23.227#1) 的方式;

举个例子,node-1节点IP为10.24.23.227,正常计算node-1hash值

  • hash(10.24.23.227#1)% 2^32

假设我们给node-1设置三个虚拟节点,node-1#1node-1#2node-1#3,对它们进行hash后取模:

  • hash(10.24.23.227#1)% 2^32
  • hash(10.24.23.227#2)% 2^32
  • hash(10.24.23.227#3)% 2^32
注意:
  • 分配的虚拟节点个数越多,映射在hash环上才会越趋于均匀,节点太少的话很难看出效果;
  • 引入虚拟节点的同时也增加了新的问题,要做虚拟节点和真实节点间的映射,对象key->虚拟节点->实际节点之间的转换;

使用场景

一致性hash在分布式系统中应该是实现负载均衡的首选算法,它的实现比较灵活,既可以在客户端实现,也可以在中间件上实现,比如日常使用较多的缓存中间件memcachedredis集群都有用到它;

memcached的集群比较特殊,严格来说它只能算是伪集群,因为它的服务器之间不能通信,请求的分发路由完全靠客户端来的计算出缓存对象应该落在哪个服务器上,而它的路由算法用的就是一致性hash;

还有redis集群中hash槽的概念,虽然实现不尽相同,但思想万变不离其宗,看完本篇的一致性hash,你再去理解redis槽位就轻松多了;

其它的应用场景还有很多:

  • RPC框架Dubbo用来选择服务提供者
  • 分布式关系数据库分库分表:数据与节点的映射关系
  • LVS负载均衡调度器
  • ……

 


 
 

三、一致性Hash算法java实现

 

一致性Hash算法实现

下面我们根据上面的讲述,使用Golang实现一个一致性Hash算法,这个算法具有一些下面的功能特性:

  • 一致性Hash核心算法;
  • 支持自定义Hash算法;
  • 支持自定义虚拟节点个数;
package com.job.hash;


import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
/**
 * 一致性hash算法
 */
public class HashUtil {


    private static Map<Integer,String>  nodeMap = new HashMap<Integer, String>();

    //1024个虚拟节点
    private static int V_NODES = 1024;

    private static TreeMap<Integer,String> vNodeMap = new TreeMap<Integer, String>();
    private static final int REAL_NODE_COUNT = 8;
    static {
        nodeMap.put(0,"node_0");
        nodeMap.put(1,"node_1");
        nodeMap.put(2,"node_2");
        nodeMap.put(3,"node_3");
        nodeMap.put(4,"node_4");
        nodeMap.put(5,"node_5");
        nodeMap.put(6,"node_6");
        nodeMap.put(7,"node_7");

        /**
         * 虚拟节点和真实节点的映射
         */
        for(int i=0;i<V_NODES;i++){
            vNodeMap.put(i,nodeMap.get(i % REAL_NODE_COUNT));
        }
    }

    /**
     * 获取真实节点
     * @param value
     * @return
     */
    public static String getRealServerNode(String value){
        Integer code = value.hashCode() % 1024;
        return vNodeMap.ceilingEntry(code).getValue();
    }


    /**
     * 删除一个节点
     * @param nodeName
     */
    public static void dropNode(String nodeName){

        int node = -1;

        for(Map.Entry<Integer,String> entry : nodeMap.entrySet()){
            if(entry.getValue().equalsIgnoreCase(nodeName)){
                node = entry.getKey();
                break;
            }
        }
        if(node == -1){
            System.out.println("未找到节点:"+nodeName);
            return;
        }


        Iterator<Map.Entry<Integer,String>> iter = vNodeMap.entrySet().iterator();
        while (iter.hasNext()){
           Map.Entry<Integer,String> entry =  iter.next();
           Integer key = entry.getKey();

           if(key % REAL_NODE_COUNT == node){
               System.out.println("删除节点:"+key+",value:"+entry.getValue());
               iter.remove();;
           }
        }

    }

    public static void main(String[] args) {
        String requestValue = "hello jason";
        System.out.println("映射关系"+vNodeMap);
        System.out.println(getRealServerNode(requestValue));
        System.out.println("删除节点");
        dropNode("node_3");
        System.out.println("映射关系"+vNodeMap);
        System.out.println(getRealServerNode(requestValue));
    }

}

  

 

标签:node,hash,Hash,算法,key,一致性,服务器,节点
From: https://www.cnblogs.com/json-92/p/18660144

相关文章

  • 简化芯片设计传统,AI训练的新型算法正改变芯片研发范式
    编辑丨&自1971年第一个商用微处理器的草图面世以来,芯片设计已经取得了长足的进步。但是,随着芯片变得越来越复杂,设计人员必须解决的问题也越来越复杂。而我们目前的工具并不总是能胜任这项任务。当今使用的自动化工具通常无法解决设计过程中的现实问题,这意味着必须人工介入,......
  • 接口项目uuid算法开发及验证-thinkphp6-rabbitmq
    一、uuid算法开发if(!function_exists('uuid')){/***生成uuid*User:龙哥·三年风水*Date:2024/6/7*Time:11:08*@paramstring$prefix*@returnstring*/functionuuid($prefix=''){$s=......
  • Java HashMap 深度解析:底层原理、源码剖析与面试必备知识
    1.HashMap概述HashMap是Java集合框架中最常用的数据结构之一,基于哈希表(HashTable)实现。它以键值对(Key-Value)存储数据,允许null键和null值,且无序。1.1HashMap的特性基于哈希表(HashTable)实现允许null键和null值非线程安全默认初始容量16,负载因子0.75JDK1......
  • 【机器学习与数据挖掘实战】案例08:基于Apriori算法的商品零售购物篮分析
    【作者主页】FrancekChen【专栏介绍】⌈⌈⌈机器学习与数据挖掘实战⌋......
  • 数据结构与算法之二叉树: LeetCode 107. 二叉树的层序遍历 II (Ts版)
    二叉树的层序遍历IIhttps://leetcode.cn/problems/binary-tree-level-order-traversal-ii/description/描述给你二叉树的根节点root,返回其节点值自底向上的层序遍历。(即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)示例1输入:root=[3,9,20,null,nul......
  • 应用质数和模算法
    生成RSA加密密钥密钥生成时先选择两个素数p和q,计算他们的乘积n=p*q,RSA的安全性是基于从n推导出p和q是很困难的,p和q越大,在给定n推到p和q的值越难,简单逻辑如下:1、选择两个大的素数2、计算n和phi(欧拉商函数)3、选择一个公共指数e4、计算私有指数d5、使用公钥加密信息6、使用私......
  • 25/1/7 算法笔记<强化学习> sac_learn代码拆解
    昨天我们看了V-REP中一个github项目的环境代码,今天我们来分析下他的强化学习代码。git链接:https://github.com/deep-reinforcement-learning-book/Chapter16-Robot-Learning-in-Simulation.首先导入了库importmathimportrandomimportgymimportnumpyasnpimport......
  • 代码随想录算法训练营第1天 | 数组理论基础,704. 二分查找,27. 移除元素,977.有序数组的
    1.刷题部分1.1数组基础理论原文链接:代码随想录1.1.1题目内容知识性讲解,点击链接查看原文。1.1.2初见想法是一些很基本的知识,看看有么有什么生疏的。1.1.3看录后想法原来有的语言的二维数组元素地址是可以行与行之间不连续的。1.1.4遇到的困难暂未遇到困难。1.......
  • 大语言模型中常用的tokenizer算法
    大语言模型中常用的tokenizer算法对于自然语言处理(NLP)任务至关重要。它们将文本分解为更小的单元(token),这些单元可以是单词、子词或字符,进而用于模型训练和推理。以下是几种常用的tokenizer算法及其详细介绍。常用的Tokenizer算法1.基于规则的Tokenizer1.1空格分词空格分词是......
  • 数据结构与算法学习笔记----扩展欧几里得算法
    数据结构与算法学习笔记----扩展欧几里得算法@@author:明月清了个风@@firstpublishtime:2025.1.8ps⭐️涉及裴蜀定理和欧几里得算法(辗转相除法)讲解,扩展欧几里得算法的推导及其应用——线性同余方程的求解Acwing877.扩展欧几里得算法[原题链接](877.扩展欧几......