一致性哈希

一致性哈希是指将存储节点和数据都映射到一个首尾相连的哈希环上,存储节点可以根据 IP 地址进行哈希,数据通常通过顺时针方向寻找的方式,来确定自己所属的存储节点,即从数据映射在环上的位置开始,顺时针方向找到的第一个存储节点。

如图所示,假设数据 D0~D7 按照 ID 进行等值映射,即映射值与 ID 值相等,比如数据 D0 映射到哈希环上的值为 100,数据 D1 映射到哈希环上的值为 200······;同时,假设存储节点 Node1、Node2 和 Node3 映射到哈希环上的值分别为 400、600、900。

按照规则,D0,D1,D2 和 D3 顺时针方向的下一个存储节点为 Node1,因此 Node1 将存储数据 D0(id = 100)、D1(id = 200)、D2(id = 300)和 D3(id = 400);同理,Node2 将存取数据 D4(id = 500)和 D5(id = 600),Node3 将存取数据 D6(id = 700)。

400|400

一致性哈希是对哈希方法的改进,在数据存储时采用哈希方式确定存储位置的基础上,又增加了一层哈希,也就是在数据存储前,对存储节点预先进行了哈希。

这种改进可以很好地解决哈希方法存在的稳定性问题。当节点加入或退出时,仅影响该节点在哈希环上顺时针相邻的后继节点。比如,当 Node2 发生故障需要移除时,由于 Node3 是 Node2 顺时针方向的后继节点,本应存储到 Node2 的数据就会存储到 Node3 中,其他节点不会受到影响,因此不会发生大规模的数据迁移。

带有限负载的一致性哈希

带有限负载的一致性哈希方法的核心原理是,给每个存储节点设置了一个存储上限值来控制存储节点添加或移除造成的数据不均匀。当数据按照一致性哈希算法找到相应的存储节点时,要先判断该存储节点是否达到了存储上限;如果已经达到了上限,则需要继续寻找该存储节点顺时针方向之后的节点进行存储。

带有限负载的一致性哈希方法比较适合同类型节点、节点规模会发生变化的场景。目前,在 Google Cloud Pub/Sub、HAProxy 中已经实现该方法,应用于 Google、Vimeo 等公司的负载均衡项目中。

带虚拟节点的一致性哈希

带虚拟节点的一致性哈希方法,核心思想是根据每个节点的性能,为每个节点划分不同数量的虚拟节点,并将这些虚拟节点映射到哈希环中,然后再按照一致性哈希算法进行数据映射和存储。

假设,Node1 性能最差,Node2 性能一般,Node3 性能最好。以 Node1 的性能作为参考基准,Node2 是 Node1 的 2 倍,Node3 是 Node1 的 3 倍。

因此,Node1 对应一个虚拟节点 Node1_1,Node2 对应 2 个虚拟节点 Node2_1 和 Node2_2,Node3 对应 3 个虚拟节点 Node3_1、Node3_2 和 Node3_3。

假设,虚拟节点 Node1_1、Node2_1、Node2_2、Node3_1、Node3_2、Node3_3 的哈希值,分别为 100、200、300、400、500、600。

那么,按照带虚拟节点的哈希一致性方法,数据 D0 和 D6 按顺时针方向的下一个虚拟存储节点为 Node 1-1,因此节点 Node1 将会存储数据 D0(id = 100)和 D6(id = 700);同理,Node2 将会存储数据 D1(id = 200)和 D2(id = 300),Node3 将会存储数据 D3(id = 400)、D4(id = 500)和 D5(id = 600)。

400|400

带虚拟节点的一致性哈希方法比较适合异构节点、节点规模会发生变化的场景。目前 Memcached 缓存系统实现了该方法。

这种方法不仅解决了节点异构性问题,还提高了系统的稳定性。当节点变化时,会有多个节点共同分担系统的变化,因此稳定性更高。

比如,当某个节点被移除时,对应该节点的多个虚拟节点均会移除,而这些虚拟节点按顺时针方向的下一个虚拟节点,可能会对应不同的物理节点,即这些不同的物理节点共同分担了节点变化导致的压力。

当然,这种方法引入了虚拟节点,增加了节点规模,从而增加了节点的维护和管理的复杂度,比如新增一个节点或一个节点故障时,对应到虚拟节点构建的哈希环上需要新增和删除多个节点,数据的迁移等操作相应地也会很复杂。

补充问题

带有限负载的一致性 hash,查询时怎么确定数据存储在哪个节点上呢?

带有限负载的一致性 hash 在存储时实际是分两步走,第一步通过一致性哈希计算出一个命中节点,如果命中节点有足够的空间,则存储在命中节点上,否则按顺时针方向,依次遍历下一个节点,直到找到下一个节点为止。

如果查询数据不存在的话,需要遍历完整个哈希环上的所有节点。这种遍历的开销就太大了,因为这是网络上的遍历,可不是单机内存内的遍历。

一种思路是,在哈希命中节点上额外添加一个索引表,用以记录命中到该节点上的数据存储在哈希环上的位置。这样在通过一致性哈希计算出命中的节点后,只需要再查询这个节点上的索引表就可以找到实际的存储节点了。

其实这个本质上就是一个解决哈希冲突的过程。

参考与扩展链接