[译] 咱们是如何高效实现一致性哈希的

Ably 的实时平台分布在超过 14 个物理数据中心和 100 多个节点上。为了保证负载和数据都可以均匀而且一致的分布到全部的节点上,咱们采用了一致性哈希算法。

在这篇文章中,咱们将会理解一致性哈希究竟是怎么回事,为何它是可伸缩的分布式系统架构中的一个重要工具。而后更进一步,咱们会介绍能够用来高效率规模化实现一致性哈希算法的数据结构。最后,咱们也会带你们看一看用这个算法实现的一个可工做实例。前端

再谈哈希

还记得大学里学的那个古老而原始的哈希方法吗?经过使用哈希函数,咱们确保了计算机程序所须要的资源能够经过一种高效的方式存储在内存中,也确保了内存数据结构能被均匀加载。咱们也确保了这种资源存储策略使信息检索变得更高效,从而让程序运行得更快。android

经典的哈希方法用一个哈希函数来生成一个伪随机数,而后这个伪随机数被内存空间大小整除,从而将一个随机的数值标识转换成可用内存空间里的一个位置。就如同下面这个函数所示:ios

location = hash(key) mod sizegit

既然如此,咱们为何不能用一样的方法来处理网络请求呢?

在各类不一样的程序、计算机或者用户从多个服务器请求资源的场景里,咱们须要一种机制来将请求均匀地分布到可用的服务器上,从而保证负载均衡,而且保持稳定一致的性能。咱们能够将这些服务器节点看作是一个或多个请求能够被映射到的位置。github

如今让咱们先退一步。在传统的哈希方法中,咱们老是假设:算法

  • 内存位置的数量是已知的,而且
  • 这个数量从不改变

例如,在 Ably,咱们一成天里一般须要扩大或者缩减集群的大小,并且咱们也要处理一些意外的故障。可是,若是咱们考虑前面提到的这些场景的话,咱们就不能保证服务器数量是不变的。若是其中一个服务器发生意外故障了怎么办?若是继续使用最简单的哈希方法,结果就是咱们须要对每一个哈希键从新计算哈希值,由于新的映射彻底决定于服务器节点或者内存地址的数量,以下图所示:后端

节点变化以前数组

节点变化以后bash

在分布式系统中使用简单再哈希存在的问题 — 每一个哈希键的存放位置都会变化 — 就是由于每一个节点都存放了一个状态;哪怕只是集群数目的一个很是小的变化,均可能致使须要从新排列集群上的全部数据,从而产生巨大的工做量。随着集群的增加,从新哈希的方法是无法持续使用的,由于从新哈希所须要的工做量会随着集群的大小而线性地增加。这就是一致性哈希的概念被引入的场景。服务器

一致性哈希 — 它究竟是什么?

一致性哈希能够用下面的方式描述:

  • 它用虚拟环形的结构来表示资源请求者(为了叙述方便,后文将称之为“请求”)和服务器节点,这个环一般被称做一个 hashring
  • 存储位置的数量再也不是肯定的,可是咱们认为这个环上有无穷多个点而且服务器节点能够被放置到环上的任意位置。固然,咱们仍然可使用哈希函数来选择这个随机数,可是以前的第二个步骤,也就是除以存储位置数量的那一步被省略了,由于存储位置的数量再也不是一个有限的数值。
  • 请求,例如用户,计算机或者无服务(serverless)程序,这些就等同于传统哈希方法中的键,也使用一样的哈希函数被放置到一样的环上。

那么它究竟是如何决定请求被哪一个服务器所服务呢?若是咱们假设这个环是有序的,并且在环上进行顺时针遍历就对应着存储地址的增加顺序,每一个请求能够被顺时针遍历过程当中所遇到的第一个节点所服务;也就是说,第一个在环上的地址比请求的地址大的服务器会服务这个请求。若是请求的地址比节点中的最大地址还大,那它会反过来被拥有最小地址的那个服务器服务,由于在这个环上的遍历是以循环的方式进行的。方法用下图进行了阐明:

理论上,每一个服务器‘拥有’哈希环(hashring)上的一段区间范围,任何映射到这个范围里的请求都将被同一个服务器服务。如今好了,若是其中一个服务器出现故障了怎么办,就以节点 3 为例吧,这个时候下一个服务器节点在环上的地址范围就会扩大,而且映射到这个范围的任何请求会被分派给新的服务器。仅此而已。只有对应到故障节点的区间范围内的哈希须要被从新分配,而哈希环上其他的部分和请求 - 服务器的分配仍然不会受到影响。这跟传统的哈希技术正好是相反的,在传统的哈希中,哈希表大小的变化会影响 所有 的映射。由于有了 一致性哈希,只有一部分(这跟环的分布因子有关)请求会受已知的哈希环变化的影响。(节点增长或者删除会致使环的变化,从而引发一些请求 - 服务器之间的映射发生改变。)

一种高效的实现方法

如今咱们对什么是哈希环已经熟悉了...

咱们须要实现如下内容来让它工做:

  1. 一个从哈希空间到集群上全部服务器节点之间的映射,让咱们能找到能够服务指定请求的节点。
  2. 一个集群上每一个节点所服务的请求的集合。在后面,这个集合可让咱们找到哪些哈希由于节点的增长或者删除而受到了影响。

映射

要完成上述的第一个部分,咱们须要如下内容:

  • 一个哈希函数,用来计算已知请求的标识(ID)在环上对应的位置。
  • 一种方法,用来寻找转换为哈希值的请求标识所对应的节点。

为了找到与特定请求相对应的节点,咱们能够用一种简单的数据结构来阐释,它由如下内容组成:

  • 一个与环上的节点一一对应的哈希数组。
  • 一张图(哈希表),用来寻找与已知请求相对应的服务器节点。

这实际上就是一个有序图的原始表示。

为了能在以上数据结构中找到能够服务于已知哈希值的节点,咱们须要:

  • 执行修改过的二分搜索,在数组中查找到第一个等于或者大于(≥)你要查询的哈希值所对应的节点 — 哈希映射。
  • 查找在图中发现的节点 — 哈希映射所对应的那个节点。

节点的增长或者删除

在这篇文章的开头咱们已经看到了,当一个节点被添加,哈希环上的一部分区间范围,以及它所包括的各类请求,都必须被分配到这个新节点。反过来,当一个节点被删除,过去被分配到这个节点的请求都将须要被其余节点处理。

如何寻找到被哈希环的改变所影响的那些请求?

一种解决方法就是遍历分配到一个节点的全部请求。对每一个请求,咱们判断它是否处在环发生变化的区间范围内,若是有须要的话,把它转移到其余地方。

然而,这么作所须要的工做量会随着节点上请求数量的增长而增长。让状况变得更糟糕的是,随着节点数量的增长,环上发生变化的数量也可能会增长。最坏的状况是,因为环的变化一般与局部故障有关,与环变化相关联的瞬时负载也可能增长其余受影响节点发生故障的可能性,有可能致使整个系统发生级联故障。

考虑到这个因素,咱们但愿请求的重定位作到尽量高效。最理想的状况是,咱们能够将全部请求保存在一种数据结构里,这样咱们能找到环上任何地方发生哈希变化时受到影响的请求。

高效查找受影响的哈希值

在集群上增长或者删除一个节点将改变环上一部分请求的分配,咱们称之为 受影响范围affected range)。若是咱们知道受影响范围的边界,咱们就能够把请求转移到正确的位置。

为了寻找受影响范围的边界,咱们从增长或者删除掉的一个节点的哈希值 H 开始,从 H 开始绕着环向后移动(图中的逆时针方向),直到找到另一个节点。让咱们将这个节点的哈希值定义为 S(做为开始)。从这个节点开始逆时针方向上的请求会被指定给它(S),所以它们不会受到影响。

注意:这只是实际将发生的状况的一个简化描述;在实践中,数据结构和算法都更加复杂,由于咱们使用的复制因子(replication factors)数目大于 1,而且当任意给定的请求都只有一部分节点可用的状况下,咱们还会使用专门的复制策略。

那些哈希值在被找到的节点和增长(或者删除)的节点范围之间的请求就是须要被移动的。

高效查找受影响范围内的请求

一种解决方法就是简单的遍历对应于一个节点的全部请求,而且更新那些哈希值映射到此范围内的请求。

在 JavaScript 中相似这样:

for (const request of requests) {
  if (contains(S, H, request.hash)) {
    /* 这个请求受环变化的影响 */
    request.relocate();
  }
}
function contains(lowerBound, upperBound, hash) {
   const wrapsOver = upperBound < lowerBound;
   const aboveLower = hash >= lowerBound;
   const belowUpper = upperBound >= hash;
   if (wrapsOver) {
     return aboveLower || belowUpper;
   } else {
     return aboveLower && belowUpper;
   }
}
复制代码

因为哈希环是环状的,仅仅查找 S <= r < H 之间的请求是不够的,由于 S 可能比 H 大(代表这个区间范围包含了哈希环的最顶端的开始部分)。函数 contains() 能够处理这种状况。

只要请求数量相对较少,或者节点的增长或者删除的状况也相对较少出现,遍历一个给定节点的全部请求仍是可行的。

然而,随着节点上的请求数量的增长,所需的工做量也随之增长,更糟糕的是,随着节点的增长,环变化也可能发生得更频繁,不管是由于在自动节点伸缩(automated scaling)或者是故障转换(failover)的状况下为了从新均衡访问请求而触发的整个系统上的并发负载。

最糟的状况是,与这些变化相关的负载可能增长其它节点发生故障的可能性,有可能致使整个系统范围的级联故障。

为了减轻这种影响,咱们也能够将请求存储到相似于以前讨论过的一个单独的环状数据结构中,在这个环里,一个哈希值直接映射到这个哈希对应的请求。

这样咱们就能经过如下步骤来定位受影响范围内的全部请求:

  • 定位从 S 开始的第一个请求。
  • 顺时针遍历直到你找到了这个范围之外的一个哈希值。
  • 从新定位落在这个范围以内的请求。

当一个哈希更新时所须要遍历的请求数量平均是 R/N,R 是定位到这个节点范围内的请求数量,N 是环上哈希值的数量,这里咱们假设请求是均匀分布的。


让咱们经过一个可工做的例子将以上解释付诸实践:

假设咱们有一个包含节点 A 和 B 的集群。

让咱们随机的产生每一个节点的 ‘哈希分配’:(假设是32位的哈希),所以咱们获得了

A:0x5e6058e5

B:0xa2d65c0

在此咱们将节点放到一个虚拟的环上,数值 0x00x10x2... 是被连续放置到环上的直到 0xffffffff,就这样在环上绕一个圈后 0xffffffff 的后面正好跟着的就是 0x0

因为节点 A 的哈希是 0x5e6058e5,它负责的就是从 0xa2d65c0+10xffffffff,以及从 0x00x5e6058e5 范围里的任何请求,以下图所示:

另外一方面,B 负责的是从 0x5e6058e5+10xa2d65c0 的范围。如此,整个哈希空间都被划分了。

从节点到它们的哈希之间的映射在整个集群上是共享的,这样保证了每次环计算的结果老是一致的。所以,任何节点在须要服务请求的时候均可以判断请求放在哪里。

好比咱们须要寻找 (或者建立)一个新的请求,这个请求的标识符是 ‘bobs.blog@example.com’。

  1. 咱们计算这个标识的哈希 H ,好比获得的是 0x89e04a0a
  2. 咱们在环上寻找拥有比 H 大的哈希值的第一个节点。这里咱们找到了 B。

所以 B 是负责这个请求的节点。若是咱们再次须要这个请求,咱们将重复以上步骤而且又会获得一样的节点,它会包含咱们须要的的状态。

这个例子是过于简单了。在实际状况中,只给每一个节点一个哈希可能致使负载很是不均匀的分布。你可能已经注意到了,在这个例子中,B 负责环的 (0xa2d656c0-0x5e6058e5)/232 = 26.7%,同时 A 负责剩下的部分。理想的状况是,每一个节点能够负责环上同等大小的一部分。

让分布更均衡合理的一种方法是为每一个节点产生多个随机哈希,像下面这样:

事实上,咱们发现这样作的结果照样使人不满意,所以咱们将环分红 64 个一样大小的片断而且确保每一个节点都会被放到每一个片断中的某个位置;这个的细节就不是那么重要了。反正目的就是确保每一个节点能负责环上同等大小的一部分,所以保证负载是均匀分布的。(为每一个节点产生多个哈希的另外一个优点就是哈希能够在环上逐渐的被增长或者删除,这样就避免了负载的忽然间的变化。)

假设咱们如今在环上增长一个新节点叫作 C,咱们为 C 产生一个随机哈希值。

A:0x5e6058e5

B:0xa2d65c0

C:0xe12f751c

如今,0xa2d65c0 + 10xe12f751c (之前是属于A的部分)之间的环空间被分配给了 C。全部其余的请求像之前同样继续被哈希到一样的节点。为了处理节点职责的变化,这个范围内的已经分配给 A 的全部请求须要将它们的全部状态转移给 C。

如今你理解了为何在分布式系统中均衡负载是须要哈希的。然而咱们须要一致性哈希来确保在环发生任何变化的时候最小化集群上所须要的工做量。

另外,节点须要存在于环上的多个地方,这样能够从统计学的角度保证负载被均匀分布。每次环发生变化都遍历整个哈希环的效率是不高的,随着你的分布式系统的伸缩,有一种更高效的方法来决定什么发生了变化是很必要的,它能帮助你尽量的最小化环变化带来的性能上的影响。咱们须要新的索引和数据类型来解决这个问题。


构建分布式系统是很难的事情。可是咱们热爱它而且咱们喜欢谈论它。若是你须要依靠一种分布式系统的话,选择 Ably。若是你想跟咱们谈一谈的话,联系咱们!

在此特别感谢 Ably 的分布式系统工程师 John Diamond 对本文的贡献。


Srushtika 是 Ably Realtime的软件开发顾问

感谢 John DiamondMatthew O'Riordan

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索