一致性哈希指南
Posted java达人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一致性哈希指南相关的知识,希望对你有一定的参考价值。
作者: Juan Pablo Carzolio
译者: java达人
来源: https://www.toptal.com/big-data/consistent-hashing
近年来,随着云计算、大数据等概念的出现,分布式系统得到了普及和应用。
其中一种类型的系统是分布式缓存,它支持许多高流量动态网站和web应用程序,通常由一种特定的分布式哈希实现,一种一致性哈希算法。
什么是一致性哈希?它背后的动机是什么?你为什么要关注它?
在本文中,我将首先回顾哈希的一般概念及其用途,然后描述分布式哈希及其所涉及的问题,引出了我们的主题。
什么是哈希
哈希是什么意思?韦氏词典将“hash”一词定义为“肉丁和土豆混在一起烧成棕色”,而动词则是“将东西(如肉和土豆)切成碎片”。所以,撇开烹饪细节不谈,hash大致的意思是“切碎和混合”—这正是这个技术术语的来源。
哈希函数是将一段数据(常是描述某种类型的对象,通常是任意大小的)映射到另一段数据(通常是一个整数,称为哈希码,或者简称哈希)。
例如,一些哈希函数设计用于哈希字符串,输出范围为0 .. 100的数、可以将字符串 Hello 映射到数字57, Hasta la vista, baby映射到数字 33,将其他任何可能的字符串映射到该范围内的某个数字。由于可能的输入比输出多得多,任何给定的数字都会有许多不同的字符串映射到它,这就是所谓的碰撞现象。好的哈希函数应该以某种方式“切碎并混合”输入的数据,以便不同输入值的输出尽可能均匀地分布在输出值范围内。
哈希函数有许多用途,对于每种用途,可能需要不同的属性。有一种叫加密哈希函数,它必须严格满足一组属性,用于安全目的,如密码保护、消息的完整性检查和指纹识别,数据损坏检测等,这些超出了本文讨论范围。
非加密哈希函数也有几种用途,最常见的是它们在哈希表中的使用,这是我们关心的问题,我们将更详细地进行探讨。
介绍Hash Table(Hash Map)
假设我们需要保存某个俱乐部所有成员的列表,同时能够搜索任意特定的成员。我们可以通过将列表保存在数组(或链表)中来处理它,并执行搜索,迭代元素直到找到所需元素(例如,我们可能根据它们的名称进行搜索)。在最坏的情况下,这意味着检查所有成员(如果我们正在搜索的是最后一个成员,或者这个成员根本不存在),或者检索一半(这是平均情况)。在复杂度理论中,搜索的复杂度为O(n),对于一个小列表,它的速度相当快,但是它会随着成员数量的增加而变得越来越慢。
如何改进呢?假设所有这些俱乐部成员都有一个成员ID,它恰好是一个反映其加入俱乐部顺序序列号。
假设可以接受按 ID进行搜索,我们可以将所有成员放入一个数组中,其索引与ID匹配(例如,ID=10的成员位于数组索引10处)。这将允许我们直接访问每个成员,根本不需要搜索。这将是非常高效的,事实上,越是高效,对应于越小的复杂度,O(1)也被称为常数时间。
但是,必须承认,俱乐部成员ID场景是有些人为的。如果id很大、非顺序的或是随机数,或者如果不能接受按ID进行搜索,而需要按名称(或其他字段)进行搜索,怎么办?需要保持我们的快速直接访问(或其他类似的访问),同时能够处理任意数据集和较少限制的搜索条件。
这就是哈希函数发挥作用的地方。一个合适的哈希函数可以将任意的数据映射到一个整数,这个整数将扮演类似于我们的俱乐部成员ID的角色,尽管有一些重要的区别。
首先,一个好的哈希函数通常有一个宽泛的输出范围(通常是32位或64位integer范围),所以为所有可能的索引构建一个数组要么不切实际,要么根本不可能,而且会浪费大量内存。为了克服这个问题,我们可以有一个合理大小的数组(比方说,只需要两倍于我们预期存储的元素的数量),并执行Hash取模操作来获得数组索引。索引为index = hash(object) mod N,其中N是数组的大小。
其次,对象哈希不是唯一的(除非我们使用固定的数据集和自定义的完美哈希函数,但我们不会在这里讨论这个)。它们会有冲突(取模操作进一步增加冲突),因此简单用索引直接访问将无法工作。有几种方法可以解决这个问题,一种典型的方法是将一个列表(通常称为bucket)附加到每个数组索引,以保存共享同一个索引的所有对象。
因此,我们有一个大小为N的数组,每个条目都指向一个对象桶。要添加一个新对象,我们需要计算它的hash modulo N,并检查结果索引处的bucket,如果还没有对象,则添加该对象。要搜索一个对象,我们也要做同样的事情,即查看桶中是否有对象。这样的结构称为哈希表,尽管桶内的搜索是线性的,但是哈希表大小适当的话每个桶应该有相当少的对象,从而产生几乎是常数时间的访问(平均复杂度为O(N/k,其中k是桶的数量)。
扩展:分布式哈希
既然我们已经讨论了哈希,现在我们准备研究分布式哈希。
在某些情况下,可能必须或者希望将哈希表拆分为由不同服务器承载的多个部分。这样做的主要动机之一是绕过只使用单台计算机的内存限制,允许构造任意大的哈希表(给定足够的服务器)。
在这种情况下,对象(及其key)分布在多个服务器中,因此称为分布式哈希。
这方面的一个典型用例是内存缓存(如Memcached)的实现。
这种配置由一个缓存服务器池组成,这些缓存服务器托管许多键/值对,用于提供对原来在其他地方存储(或计算)的数据的快速访问。例如,为了减少对数据库服务器的负载,同时提高性能,应用程序可以设计先从缓存服务器中获取数据,只有当数据不存在时—这种情况称为缓存未命中—才请求数据库,运行相关查询并使用适当的键缓存结果,以便下次需要时可以找到它。
那么,分布式是如何实现的呢?使用什么标准来确定要在哪些服务器上驻留哪些key?
让我们看一个例子。假设我们有三个服务器,A、B和C,我们有一些拥有哈希值的字符串键:
KEY |
HASH |
HASH mod 3 |
"john" |
1633428562 |
2 |
"bill" |
7594634739 |
0 |
"jane" |
5000799124 |
1 |
"steve" |
9787173343 |
0 |
"kate" |
3421657995 |
2 |
客户端想要检索key为 john的值。它 hash modulo 3是2,所以它必须与服务器C关联。key不存在,所以客户端从数据源获取数据并添加。服务器池结果如下:
A |
B |
C |
"john" |
接下来,另一个客户端(或同一客户端)希望检索key 为bill的值。它hash modulo 3结果是0,所以它必须与服务器A关联。key未找到,所以客户端从数据源获取数据并添加。服务器池结果如下:
A |
B |
C |
"bill" |
"john" |
在添加了其余的key之后,服务器池结果如下:
A |
B |
C |
"bill" |
"jane" |
"john" |
"steve" |
"kate" |
Rehash问题
这种分配方案简单、直观、工作良好,但是如果服务器的数量发生变化就不行了。如果其中一个服务器崩溃或变得不可用,会发生什么?当然需要重新分发key来弥补损失的服务器。如果将一台或多台新服务器添加到池中,情况也是如此;需要重新分发key以包含新服务器。任何分发方案都会有这种问题,但是我们这种简单的取模分发方案的问题是,当服务器的数量发生变化时,大多数hashes modulo N 结果将发生变化,因此大多数key将需要移动到不同的服务器。因此,即使删除或添加了单个服务器,也可能需要将所有key rehash到不同的服务器中。
从我们之前的例子中,如果我们删除了服务器C,我们将不得不根据 hash modulo 2而不是hash modulo 3对所有key进行rehash,key的新位置如下:
KEY |
HASH |
HASH mod 2 |
"john" |
1633428562 |
0 |
"bill" |
7594634739 |
1 |
"jane" |
5000799124 |
0 |
"steve" |
9787173343 |
1 |
"kate" |
3421657995 |
1 |
A |
B |
"john" |
"bill" |
"jane" |
"steve" |
"kate" |
注意,所有key位置都发生了变化,不仅仅是在服务器 C的key的位置。
在我们前面提到的典型用例(缓存)中,这意味着,key突然都找不到了,因为它们还没有被存放在新的位置。
因此,大多数查询将不会命中,并且可能需要重新从数据源检索原始数据来rehash,进而给源服务器(通常是数据库)带来沉重的负载。这可能会严重降低性能,导致源服务器崩溃。
解决方案:一致性哈希
那么,如何解决这个问题呢?我们需要一个不直接依赖于服务器数量的分发方案,这样,在添加或删除服务器时,需要重新定位的key的数量就可以最小化。有一个这样的方案—一个聪明的,却非常简单的方案—被称为一致性哈希,它首次由麻省理工学院的Karger等人在1997年的一篇学术论文中提出(根据维基百科)。
一致性哈希是一种分布式哈希方案,它在一个抽象的圆或哈希环上为服务器或对象分配一个位置,它的操作不依赖于分布式哈希表中服务器或对象的数量。这允许服务器和对象在不影响整个系统的情况下进行伸缩。
假设我们将哈希输出范围映射到圆环的边缘。这意味着最小散列值零将对应于零角,其最大可能值(我们称为INT_MAX的大整数)对应于2 以上是关于一致性哈希指南的主要内容,如果未能解决你的问题,请参考以下文章