vlambda博客
学习文章列表

一文理解一致性哈希算法

对于最近看到的哈希算法,然后还有一致性哈希算法,本文针对网上搜集到的资料做一个整理,方便后面回顾一致性哈希算法的知识,这就是本篇文章《一文彻底读懂一致性哈希算法》的由来;

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

简介

一致性哈希算法是1997年在论文《Consistent hashing and random trees》中被提出的,在分布式系统中应用非常广泛。一致性哈希是一种哈希算法,简单的说就是在移除或者添加一个服务器时,此算法能够尽可能小的改变已存在的服务请求与处理请求服务器之间的映射关系,尽可能满足单调性的要求,在普通分布式集群中,服务请求鱼处理请求服务器之间可以一一对应,也就是说固定服务请求与处理服务器之间的映射关系,某个请求由固定的服务器去处理。这种方式无法对整个系统进行负载均衡,可能会造成某些服务器过于繁忙一直无法处理新来的请求,而另一些服务器则过于空闲,整体系统的资源利用率低,并且当分布式集群中的某个服务器宕机,会直接导致某些服务请求无法处理。

进一步的改进可以利用hash算法对服务请求进行处理服务器之间的关系进行映射,以达到动态分配的目的。通过hash算法对服务请求进行转换,转换后的结果对服务器节点值进行取模计算,取模后的值就是服务请求对应的请求处理服务器。这种方法可以应对节点失效的情况,当某个分布式集群节点宕机,服务请求可以通过hash算法重新分配到其他可用的服务器上。避免了无法处理请求的情况出现。

但这种方法的缺陷也很明显,如果服务器中保存有服务请求对应的数据,那么如果重新计算请求的hash值,会造成大量的请求被重定位到不同的服务器而造成请求所要的数据无效,这种情况在分布式系统中是非常糟糕的。一个设计良好的分布式系统应该具有良好的单调性,即服务器的添加与移除不会造成大量的哈希重定位,而一致性哈希恰好可以解决这个问题。

一致性哈希算法将整个哈希值空间映射一个虚拟的圆环,整个哈希空间的取值范围为0~2^32-1.整个空间按顺时针方向组织。0~2^32-1在零点方向重合。接下来使用如下算法对请求进行映射,将服务请求使用哈希算法算出对应的hash值,然后根据hash值的位置沿圆环顺时针查找,第一台遇到的服务器就是所对应的处理请求服务器。当增加一台新的服务器,受影响的数据仅仅是新添加的服务器到其环空间中前一台的服务器(也就是顺着逆时针方向遇到的第一台服务器)之间的数据,其它都不会受到影响。综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

场景描述

假设我们有三台缓存服务器,用于缓存图片,我们为这三台缓存服务器设置编号为0,1,2号服务器,现在对请求过来的图片均匀的缓存在这三台服务器上,以便它们均摊缓存图片的请求,假设有三万张图片需要缓存,也就是每台服务器平均缓存一万张左右。如果我们以没有任何规律的把三万张图片平均的缓存在三台服务器上,也能满足我们的需求,但是如果这样做的话,当我们访问某一张图片时,则最差需要遍历三台服务器,从三万个缓存中找到我们所需要的缓存,这样的话其实也就失去了缓存的意义,并没有改善用户的体验。那么我们可以怎么做呢,原始的做法就是对图片的名称进行哈希(假设图片名称是唯一的),将hash后的结果进行与服务器数量的取模操作,通过取模后的结果来决定缓存该存在哪台服务器上,那么我们可以使用如下公式来决定图片应该存在哪台服务器上

hash(图片名称)%N

因为图片的名称是不重复,所以当我们对一个图片名称做相同的哈希计算时,得到的结果应该是不变的,如果我们有三台服务器,使用哈希后的结果就是对三进行取余,那么余数一定是0,1,2三个中的一个,没错,正好与我们的服务器编号一样了,所以如果求余的结果是0,那么我们就可以把图片缓存在0号服务器上,相反,如果取余的结果是1,那就是缓存在1号服务器上。那么当我们访问任意一个图片时,只需要对图片名称进行上述的操作即可知道图片缓存在哪个服务器上,如果图片在对应的服务器上不存在,则证明对应的图片没缓存,也就不需要再去其它的服务器上查询了,通过这样的方法,就可以平均的把三万张图片分散的放在三台服务器上,而且当下次访问某张图片时,直接就能判断出当前图片所在的服务器,这样就能满足我们的需求了,过程可以参考如下图所示

image-20220210222533380

此时使用了上面的哈希算法之后还会有什么问题呢,试想一下,如果三台缓存服务器已经不能满足我们的缓存需求,那么我们是不是要增加一台,那么缓存服务器的数量就由3台变成4台了,此时如果仍然采用上述方法对同一张图片进行缓存,那么这张图片所在的服务器编号必定与原来三台服务器时产生的结果不一致,这种情况带来的后果就是当缓存服务器数量发生变动时,所有缓存的位置都要发生改变,换句话说,当服务器数量发生改变时,所有缓存在一定时间内是失效的,当请求无法从缓存中读取到数据时,则会向后端服务器发送请求,同理,假设三台服务器其中有一台服务器发生了故障,无法进行缓存,那么我们就需要将故障机器移除,此时响应的缓存服务器数量也是发生了变化,以前缓存的图片也就失去了意义,此时大量图片缓存失效,造成缓存雪崩,此时缓存无法在分担压力,后端服务器将承受压力,整个系统也就有可能被压垮,所以我们要想办法不让这种情况发生,但是由于上述哈希算法本身的缘故,使用取模算法进行缓存时,这种情况是不可避免的,为了解决这些问题,一致性哈希算法也就应运而生了。

所以我们来回顾下使用上述哈希算法会产生的问题:

  • 当缓存服务器数量发生变化时,引起缓存大量失效,缓存雪崩,可能会使后端服务器压力变大而崩溃
  • 当缓存服务器数量发生变化时,几乎所有的缓存位置都会发生变化,那我们怎样才能减少受影响的缓存呢

其实,这两个问题都可以使用一致性哈希算法解决,现在我们继续深入了解一下一致性哈希算法

概念

首先,一致性哈希算法是采用取模的方法,只是,刚才的取模算法是对服务器数量进行取模,那么我们只要不对服务器数量进行取模,而对一个常量进行取模不就可以了吗,所以一致性哈希算法是对2^32进行取模,我们把0-2的32次方想象成一个圆,就像钟表一样的圆,钟表的圆可以想象是圆上有60个点,而我们的这个圆是由2^32个点组成的圆,示意图如下

一文理解一致性哈希算法
image-20220211222050815

圆的正上方的点代表0,0点右侧第一个点代表1,左侧第一个点代表2^32-1,我们把0-2^32-1之间的圆环称之为hash环

下面,跟我一起看下一致性哈希算法是怎么在这个圆上来体现的

hash(服务器IP地址)% 2^32

通过这个公式计算的结果肯定是0-2^32-1之间的一个数,我们就用这个数代表服务器A

一文理解一致性哈希算法
image-20220211222626806

然后我们用同样的方法,计算出服务器B,和服务器C的位置,表示如下

一文理解一致性哈希算法
image-20220211222811458

假设三台服务器经过计算之后的位置如上图所示(理想情况下是这个样子,但是现实往往很无情)

好了,到这里,我们已经把服务器与hash环绑定到了一起,下面就是把数据绑定到hash环上,那么我们下面就用同样的方法把需要缓存的对象放到hash环上

还是按照刚才的例子,对三万张图片进行缓存,此时我们还是使用如下公式

hash(图片名称)% 2^32

第一张图片映射的结果如下图所示(假设)

一文理解一致性哈希算法
image-20220211230153473

那么它是如何决定到哪台服务器上去呢,上面的图片会被缓存到服务器A上,为什么会这样呢?因为从图片的位置开始,沿顺时针方向遇到的第一个服务器就是服务器A,所以上图会被缓存到服务器A上,如下图所示

一文理解一致性哈希算法
image-20220211230415546

所以一致性哈希算法就是通过这种方法来判断对象该存储在哪台服务器上的,也就是将缓存服务器与缓存的对象绑定到hash环上之后,从被缓存的对象开始,顺时针方向遇到的第一台服务器就是缓存对象要存储的服务器,由于缓存对象对服务器hash后的值是固定的,所以在服务器不变的情况下,一张图片必定会缓存在固定的服务器上,所以当下次方式该图片时,只要在次使用同样的哈希算法计算,即可获取出该图片在哪台服务器上,然后直接去对应的服务器上获取即可。

刚才的示例是一张图片,下面演示一下多张图片的,

一文理解一致性哈希算法
image-20220211231156905

图片1,图片2会缓存在服务器A上,图片3缓存在服务器B上,图片4缓存在服务器C上

一致性哈希的优点

经过上面的描述,我想大家应该能够知道一致性哈希的原理了,所以大家考虑一下,一致性哈希可以解决上面的两个问题吗,首先还是第一个问题,当服务器数量发生变动时,缓存会大量同一时间失效造成缓存雪崩,从而有可能引发系统的崩溃,那么使用一致性哈希算法能够避免这个问题吗,下面我们来看一下

假设服务器B出现故障,现在我们需要把服务器B移除掉,那么我们从hash环上移除服务器B之后如下所示

一文理解一致性哈希算法
image-20220212084221136

在服务器B没有故障未被移除时,图片3应该是缓存在服务器B上,可是当服务器B有了故障被移除后,图片3按照顺时针方向遇到的第一个服务器就变成了服务器C,所以此时图片3就被缓存在服务器C中,也就是说,如果服务器B出现故障,图片3的缓存位置会发生变化

一文理解一致性哈希算法
image-20220212085907685

此时,图片4仍然会被缓存中服务器C中,图片1与图片2缓存在服务器A中,这与服务器B移除前后并不会有影响,这也就是哈希算法的优点,如果使用之前的哈希算法,服务器数量发生变化时,所有的缓存对象都会发生变化,而使用一致性哈希算法之后,服务器的数量如果发生变化,并不是所有的缓存都会失效,而只有部分缓存才会失效,所以这种情况下不至于所有的缓存失效,都在同一时间集中到后端服务器上。

hash环的偏斜

在介绍一致性哈希时,我们是非常理想化的假设3台服务器均匀的分布在hash环上,如下图所示

一文理解一致性哈希算法
image-20220211222811458

但是往往现实不会如此,在实际的使用过程中,服务器有可能会被映射为如下

一文理解一致性哈希算法
image-20220212092143944

聪明的你肯定想到如果发生了这种情况,那么缓存的对象可能会集中的缓存中一台服务器上,这也就是hash环的倾斜,如下如所示

image-20220212094553990

此时,如图所示,图片1,图片3,图片4,图片5都会缓存到服务器A上,图片2缓存中服务器B上,服务器C上没有图片,也就是说,服务器资源并没有平均的被使用,最坏的情况下,如果此时服务器A发生故障,那缓存失效的对象也就达到了最大值,极端情况下也有可能造成后端服务器的崩溃。此时这种情况称之为hash环的倾斜,那么怎么防止hash环的倾斜呢,一致性hash算法使用虚拟节点来解决了这个问题,下面我们来看看

虚拟节点

还是按照我们说的,假设我们只有三台服务器,当我们把服务器映射到hash环上的时候,很有可能发生hash环偏斜的情况,当hash环偏斜以后,缓存往往会极度不平衡的分布在各个服务器上,聪明的你肯定想到了,要想均衡的将缓存分布在三台服务器上,那么只要这三台服务器尽可能多的均匀的分布在hash环不就行了吗,但是真实的服务器资源只有3台,我们怎么凭空让它多起来呢,没错,就是凭空让服务器的节点多起来,既然没有多余的真正的物理服务器节点,我们只能将现有的物理节点虚拟出来,这些由实际节点虚拟复制出来的节点被称为“虚拟节点”,加入虚拟节点之后的hash环如下

image-20220212095838921

虚拟节点是实际节点在hash环上的复制品,一个实际节点可以复制多少虚拟节点

从上图可以看出,ABC三台服务器分别虚拟出来一个虚拟节点,当然,如果你需要的话也可以虚拟出更多的节点,此时为了画图演示,我们各只虚拟出一个节点。增加了虚拟节点的概念后,缓存的分布就就均匀了很多,上图中,图片1,图片3缓存在服务器A,图片2,图片4缓存中服务器B,图片5缓存中服务器C,如果你还不放心的话,可以虚拟出更多的虚拟节点,以便减少hash环倾斜带来的影响,虚拟节点越多,hash环的节点越多,缓存被均匀分布的可能就越大。

所以,此时缓存对象已经均衡的分布在hash环上,如果在发生服务器故障,那么受影响的缓存对象就是发生故障的服务器逆时针方向到遇到的第一台服务器之间的数据,受影响的缓存对象大大减少。

好了,一致性哈希算法的原理就总结了这里,如果错误,欢迎赐教

参考链接

  • 百度百科(https://baike.baidu.com/item/%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C/2460889?fr=aladdin#reference-[3]-1588037-wrap)
  • 维基百科(https://zh.wikipedia.org/wiki/%E4%B8%80%E8%87%B4%E5%93%88%E5%B8%8C)
  • 参考博客 (https://www.cnblogs.com/lpfuture/p/5796398.html)
  • 参考博客 (https://www.zsythink.net/archives/1182)
  • 参考链接 (https://zh.wikipedia.org/wiki/%E5%88%86%E6%95%A3%E5%BC%8F%E9%9B%9C%E6%B9%8A%E8%A1%A8)