一致性Hash算法

一致性Hash算法

为什么会出现一致性hash

一致性哈希是分布式系统组件负载均衡的首选算法,比如分库分表时要考虑数据怎么均匀分布,它既可以在客户端实现,也可以在中间件上实现。其应用有:
  1. 分布式散列表(DHT)的设计;
  2. 分布式关系数据库(MySQL):分库分表时,计算数据与节点的映射关系;
  3. 分布式缓存:Memcached 的客户端实现了一致性哈希,还可以使用中间件 twemproxy 管理 redis/memcache 集群;
  4. RPC 框架 Dubbo:用来选择服务提供者;
  5. 亚马逊的云存储系统 Dynamo;
  6. 分布式 Web 缓存;
  7. Bittorrent DHT;
  8. LVS。

传统hash算法的弊端

资源数据分布通常有哈希分区和顺序分区两种方式
  1. 顺序分布:数据分散度易倾斜、键值业务相关、可顺序访问、不支持批量操作。
  2. 哈希分布:数据分散度高、键值分布业务无关、无法顺序访问、支持批量操作。
  1. 优点
这种方式的突出优点就是简单,且常用于数据库的分库分表。如京东某系统中采用shardingjdbc,用这种方式进行分库分表路由处理。
  1. 缺点
节点数量发生变化时,如扩容或收缩节点(没有遇到过),数据节点关系需要重新计算,会导致数据的重新迁移。所以扩容通常采用翻倍扩容,避免数据映射全部打乱而全部迁移,翻倍迁移只发生50%的数据迁移。如果不翻倍缩扩容,如某一台机器宕机,那么应该落在该机器的请求就无法得到正确的处理,这时需要将宕掉的服务器使用算法去除,此时候会有(N-1)/N的服务器的缓存数据需要重新进行计算;如果新增一台机器,会有N /(N+1)的服务器的缓存数据需要进行重新计算。对于系统而言,这通常是不可接受的颠簸(因为这意味着大量缓存的失效或者数据需要转移)。
传统求余做负载均衡算法,缓存节点数由3个变成4个(3/(3+1)=75%),缓存不命中率为75%。计算方法:穷举hash值为1-12的12个数字分别对3和4取模,然后比较发现只有前3个缓存节点对应结果和之前相同,所以有75%的节点缓存会失效,可能会引起缓存雪崩。
大量缓存在同一时间失效,造成缓存的雪崩,进而导致整个缓存系统的不可用,这基本上是不能接受的;
为了解决优化上述情况,一致性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环上的位置代表了一个服务器,依次将node0、node1、node2三个缓存服务器映射到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映射,转变为了在一个环上的某个片段上的映射!
 
在前面我们已经说过:如果使用简单的取模方法,当新添加服务器时可能会导致大部分缓存失效,而使用一致性哈希算法后,这种情况得到了较大的改善,因为只有少部分对象需要重新分配!

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

 
引出问题
 
在上面给出的例子中,各个服务器几乎是平均被均摊到Hash环上;
但是在实际场景中很难选取到一个Hash函数这么完美的将各个服务器散列到Hash环上;
此时,在服务器节点数量太少的情况下,很容易因为节点分布不均匀而造成数据倾斜问题;
 
同时,还有另一个问题:
在上面新增服务器 CS4 时,CS4 只分担了 CS1 服务器的负载,服务器 CS2 和 CS3 并没有因为 CS4 服务器的加入而减少负载压力;如果 CS4 服务器的性能与原有服务器的性能一致甚至可能更高,那么这种结果并不是我们所期望的;
 

虚拟节点

针对上面的问题,我们可以通过:引入虚拟节点来解决负载不均衡的问题
 
即将每台物理服务器虚拟为一组虚拟服务器,将虚拟服务器放置到哈希环上,如果要确定对象的服务器,需先确定对象的虚拟服务器,再由虚拟服务器确定物理服务器
 

原理总结:

 

原理理解了,实现并不难,主要是一些细节:

    1. hash算法的选择
      Java代码不要使用hashcode函数,这个函数结果不够散列,而且会有负值需要处理。
      这种计算Hash值的算法有很多,比如CRC32HASH、FNV132HASH、KETAMAHASH等,其中KETAMAHASH是默认的MemCache推荐的一致性Hash算法,用别的Hash算法也可以,比如FNV132_HASH算法的计算效率就会高一些。
    2. 数据结构的选择。根据算法原理,我们的算法有几个要求:
      要能根据hash值排序存储
      排序存储要被快速查找 (List不行)
      排序查找还要能方便变更 (Array不行)
      另外,由于二叉树可能极度不平衡。所以采用红黑树是最稳妥的实现方法。Java中直接使用TreeMap(自动根据key排序的,而LinkedHashMap维护插入顺序)即可。

使用案例

例如:Jedis下的ShardedJedis (分布式)分片机制 采用了一致性哈希的算法,至少160个虚拟节点,来决定每个key的保存位置,不同的key返回的redis连接可能是不同的。

 

 默认权重是1,虚拟节点是权重*160,即一个物理节点至少160个虚拟节点虚拟节点hash计算:物理节点(名字或索引序号) 加 数字编号

 

 

 

 

 hash算法(MurmurHash算法) 和 数据结构(划分虚拟节点采用 TreeMap):

 

 



posted @ 2022-11-21 11:04  好Wu赖  阅读(166)  评论(0编辑  收藏  举报