在了解一致性哈希算法之前,最好先了解一下缓存中的一个应用场景,了解了这个应用场景之后,再来理解一致性哈希算法,就容易多了,也更能体现出一致性哈希算法的优点,那么,我们先来描述一下这个经典的分布式缓存的应用场景。
一、场景描述
假设,我们有三台缓存服务器,用于缓存图片,分别为 0 号、1 号、2 号,现在,有 3 万张图片需要缓存,我们希望这些图片被均匀的缓存到这3台服务器上,以便它们能够分摊缓存的压力。也就是说,我们希望每台服务器能够缓存 1 万张左右的图片,那么,我们应该怎样做呢?
如果我们没有任何规律的将 3 万张图片平均的缓存在 3 台服务器上,可以满足我们的要求吗?可以!但是如果这样做,当我们需要访问某个缓存项时,则需要遍历3台缓存服务器,从 3 万个缓存项中找到我们需要访问的缓存,遍历的过程效率太低,时间太长,当我们找到需要访问的缓存项时,时长可能是不能被接受的,也就失去了缓存的意义,缓存的目的就是提高速度,改善用户体验,减轻后端服务器压力,如果每次访问一个缓存项都需要遍历所有缓存服务器的所有缓存项,想想就觉得很累,那么,我们该怎么办呢?
原始的做法是对缓存项的键进行哈希,将 Hash 后的结果对缓存服务器的数量进行取模操作,通过取模后的结果,决定缓存项将会缓存在哪一台服务器上。
举个例子,假设我们使用图片名称作为访问图片的 Key,假设图片名称是不重复的,那么,我们可以使用 hash(图片名称) % N
,计算出图片应该存放在哪台服务器上。
如果我们有 3 台服务器,使用哈希后的结果对 3 求余,那么余数一定是 0、1、2,正好与我们之前的服务器编号相同,如果求余的结果为 0, 就把当前名称对应的图片缓存在 0 号服务器上,如果余数为 1,就缓存在 1 号服务器上,以此类推。
当我们访问任意一个图片的时候,只要再次对图片名称进行上述运算,即可得出对应的图片应该存放在哪一台缓存服务器上。
但是,使用上述 Hash 算法进行缓存时,会出现一些缺陷,试想一下,如果 3 台缓存服务器已经不能满足我们的缓存需求,那么我们应该怎么做呢?
很简单,多增加两台缓存服务器不就行了,假设,我们增加了一台缓存服务器,那么缓存服务器的数量就由 3 台变成了 4 台,此时,如果仍然使用上述方法对同一张图片进行缓存,那么这张图片所在的服务器编号必定与原来 3 台服务器时所在的服务器编号不同,因为除数由 3 变为了 4,并且,之前缓存的图片也需要全部调整,这种情况带来的结果就是当服务器数量变动时,全部缓存图片的位置都要发生改变。
我们来回顾一下使用上述算法会出现的问题。
问题1:当缓存服务器数量发生变化时,会引起缓存的雪崩,可能会引起整体系统压力过大而崩溃(大量缓存同一时间失效)。
问题2:当缓存服务器数量发生变化时,几乎所有缓存的位置都会发生改变,怎样才能尽量减少受影响的缓存呢?
为了解决这些问题,一致性哈希算法诞生了。
二、一致性哈希算法的基本概念
其实,一致性哈希算法也是使用取模的方法,只是,刚才描述的取模法是对服务器的数量进行取模,而一致性哈希算法是对 2^32
取模,什么意思呢?我们慢慢聊。
首先,我们把 232 想象成一个圆,就像钟表一样,钟表的圆可以理解成由 60 个点组成的圆,而此处我们把这个圆想象成由 232 个点组成的圆,示意图如下:
圆环的正上方的点代表 0,0 点右侧的第一个点代表 1,以此类推,2、3、4、5、6……直到 232-1,也就是说 0 点左侧的第一个点代表 232-1,我们把这个由 232 个点组成的圆环称为 Hash 环。
那么,一致性哈希算法与上图中的圆环有什么关系呢?
假设我们有 3 台缓存服务器,服务器 A、服务器 B、服务器 C,那么,这三台服务器肯定有自己的 IP 地址,我们使用它们各自的 IP 地址进行 Hash 计算,使用 Hash 后的结果对 232 取模,即:hash(A的IP地址) % 2^32
。
通过上述公式算出的结果一定是一个 0 到 232-1 之间的一个整数,我们就用算出的这个整数,代表服务器 A,并且上图中的 Hash 环上必定有一个点与这个整数对应,那么,服务器 A 就可以映射到这个环上。
同理,服务器 B 与服务器 C 也可以通过相同的方法映射到上图中的 Hash 环中:
hash(B的IP地址) % 2^32
hash(C的IP地址) % 2^32
示意图如下,当然,这是理想的情况:
到目前为止,我们已经把服务器与 Hash 环联系在了一起,那么使用同样的方法,我们也可以将需要缓存的图片映射到 Hash 环上。
假设,我们仍然使用图片的名称作为找到图片的 Key,那么我们使用
hash(图片名称Key) % 2^32
公式可以将图片映射到上图中的hash环上。
现在服务器与图片都被映射到了 Hash 环上,那么这个图片到底应该被缓存到哪一台服务器上呢?
图片将会被缓存到服务器A上,为什么呢?因为从图片的位置开始,沿顺时针方向遇到的第一个服务器就是A服务器,所以,上图中的图片将会被缓存到服务器A上,如下图所示,图中橘黄色代表需缓存的图片:
一致性哈希算法就是通过这种方法,判断一个对象应该被缓存到哪台服务器上的,将缓存服务器与被缓存对象都映射到 Hash 环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要缓存于的服务器。
由于被缓存对象与服务器 Hash 后的值是固定的,所以,在服务器不变的情况下,一张图片必定会被缓存到固定的服务器上,那么,当下次想要访问这张图片时,只要再次使用相同的算法进行计算,即可算出这个图片被缓存在哪个服务器上,直接去对应的服务器查找对应的图片即可。
刚才的示例只使用了一张图片进行演示,假设有四张图片需要缓存,示意图如下:
1 号、2 号图片将会被缓存到服务器 A 上,3 号图片将会被缓存到服务器 B 上,4 号图片将会被缓存到服务器 C 上。
三、一致性哈希算法的优点
经过上述描述,应该已经明白了一致性哈希算法的原理了,但是话说回来,一致性哈希算法能够解决之前出现的问题吗,我们说过,如果简单的对服务器数量进行取模,那么当服务器数量发生变化时,所有数据全部需要调整。那么使用一致性哈希算法,能够避免这个问题吗?
在服务器 B 未移除时,图片 3 应该被缓存到服务器 B 中,假设,服务器 B 出现了故障,我们现在需要将服务器 B 移除,那么,我们将上图中的服务器 B 从 Hash 环上移除。
可是当服务器 B 移除以后,按照之前描述的一致性哈希算法的规则,图片 3 应该被缓存到服务器 C 中,因为从图片 3 的位置出发,沿顺时针方向遇到的第一个缓存服务器节点就是服务器 C,也就是说,如果服务器B出现故障被移除时,图片3的缓存位置会发生改变:
但是,图片 4 仍然会被缓存到服务器 C 中,图片 1 与图片 2 仍然会被缓存到服务器 A 中,这与服务器 B 移除之前并没有任何区别,这就是一致性哈希算法的优点。
如果使用之前的 Hash 算法,服务器数量发生改变时,所有服务器的所有缓存在同一时间失效了。而使用一致性哈希算法时,并不是所有缓存都会失效,而是只有部分缓存会失效。在这个例子中就只会影响服务器 B 至服务器 A 之间的数据,尽可能的提供有损的服务。
这就是一致性哈希算法所体现出的优点。
四、Hash环的偏斜
在介绍一致性哈希的概念时,我们理想化的将 3 台服务器均匀的映射到了 Hash 环上。但实际我们想象的与实际情况往往不一样。在实际的映射中,服务器可能会被映射成如下模样:
如果服务器被映射成上图中的模样,那么被缓存的对象很有可能大部分集中缓存在某一台服务器上,如下图所示:
图中,1号、2号、3号、4号、6号图片均被缓存在了服务器 A 上,只有 5 号图片被缓存在了服务器 B 上,服务器 C 上甚至没有缓存任何图片,如果出现上图中的情况,A、B、C 三台服务器并没有被合理的平均的充分利用,缓存分布的极度不均匀。并且,如果此时服务器 A 出现故障,那么失效缓存的数量也将达到最大值,在极端情况下,仍然有可能引起系统的崩溃,上图中的情况则被称之为 Hash 环的偏斜,那么,我们应该怎样防止 Hash 环的偏斜呢?
一致性 Hash 算法中使用"虚拟节点"解决了这个问题。
六、虚拟节点
由于我们只有 3 台服务器,当我们把服务器映射到 Hash 环上的时候,很有可能出现 Hash 环偏斜的情况,当 Hash 环偏斜以后,缓存往往会极度不均衡的分布在各服务器上。如果想要均衡的将缓存分布到3台服务器上,最好能让这 3 台服务器尽量多的、均匀的出现在 Hash 环上,但是,真实的服务器资源只有 3 台,我们怎样凭空的让它们多起来呢,没错,就是凭空的让服务器节点多起来。
既然没有多余的真正的物理服务器节点,我们就只能将现有的物理节点通过虚拟的方法复制出来,这些由实际节点虚拟复制而来的节点被称为“虚拟节点”。加入虚拟节点以后的hash环如下:
“虚拟节点”是实际节点在 Hash 环上的复制品,一个实际节点可以对应多个虚拟节点。
从上图可以看出,A、B、C 三台服务器分别虚拟出了一个虚拟节点,当然,如果你需要,也可以虚拟出更多的虚拟节点。引入虚拟节点的概念后,缓存的分布就均衡多了,上图中,1号、3号图片被缓存在服务器A中,5号、4号图片被缓存在服务器B中,6号、2号图片被缓存在服务器C中,如果你还不放心,可以虚拟出更多的虚拟节点,以便减小hash环偏斜所带来的影响,虚拟节点越多,hash环上的节点就越多,缓存被均匀分布的概率就越大。
以上内容参考:http://www.zsythink.net/archives/1182