分布式系统基础-一致性哈希

Posted by AlstonWilliams on February 17, 2019

注意: 本文是读者在理解了一致性哈希算法之后,写的一些感悟.由于参考的大多数资料都是英文文献,所以可能会有部分理解的与原文有差异.在文末附上了一些参考文献,其中便有作者读的Standford CS168的一篇文档,请读者在阅读时适当结合英文原文.当然,也有的中文资料写的也不错,我看过一篇.但是考虑到自己动手记录下才能更加加深印象,所以写成此文.文笔拙劣,当读者们见笑了.

很早就对分布式系统感兴趣,但是一直没有深入研究,一是因为忙着做其他事情,二是并没有什么书籍能够全面的介绍分布式系统中常用的算法.

Google了一番之后,发现有人说** Amazon的Dynamo**论文中,比较详细的介绍了分布式系统算法.于是就打算读这篇论文,把这篇论文中的算法学习一下.

我们先了解一下什么是** Dynamo**.从Wikipedia上我们可以看到其定义为:

通俗一点的讲,就是一个Key/Value存储系统,与其类似的产品还有Consul, Etcd, Zookeeper.

论文中第一个看到的算法就是** Consistent Hash**,中文译法便是一致性哈希.

那么这个算法的作用是什么呢?它能容忍集群的变动,使得即使集群中新增或者删除了节点,我们能够通过最小的代价来进行调整.** Dynamo**的论文中指出,它们使用这个算法来实现数据的分区和备份.

一致性哈希的使用场景

最开始,一致性哈希是被用在设计分布式缓存系统中.关于缓存,我们已经了解的够多了.这里就不详细介绍了.

当然,其使用场景远远不止于缓存系统.实际上,其在大量分布式系统中都有使用.

传统算法的不足

当设计一个分布式缓存系统时,关于数据的分布问题,最容易想到的解决方案便是,对key取Hash,转换成一个32位的数字,然后将其mod节点的数量,决定放在哪个节点中.这种方案在集群拓扑比较稳定时,是一个不错的方案.但是,如果我们新增一个节点,便会是一个灾难性的变化.比如,我们采用md5(key).toUInt32().mod(node_size)这个算法来计算节点的位置,原先有100个节点,key为AAAAAAA的那个数据,原先是放在node1上的.当我们想要读取这个key对应的数据时,我们可以通过md5(“AAAAAAA”).toUInt32.mod(100)这个函数来得到存在node1上,然后去对应的node1上找.现在问题来了,如果集群中新增了一个节点,现在节点数变为了101,那我们通过md5(“AAAAAAA”).toUInt32.mod(101)这个函数得到的结果就可能是node2,这当然会造成miss.然而,实际上这个数据还是缓存在整个集群中的.

我们可以看到,上面的算法的主要不足是,当新增了节点时,由于计算存储的节点依赖于节点的数量,所以在读缓存时,会造成明明数据存在却miss的情况.

一致性哈希的基本思想

我们知道,在一个集群中,可用节点的数量是一直在变得,这就导致传统算法的弊端被放大.基于此,Karger和其他专家提出了一致性哈希算法.那么它是如何解决出现传统的算法中的不足呢?

在我们考试的时候,考场的门上都贴着这个考场中考生的考号的范围.我们在进考场时,就得找到包含我们的考号的那个考场.

其实一致性哈希的实现方法就跟这个类似.

在一致性哈希中,我们除了需要计算要存储的数据的key的hash之外,还要计算节点的hash,然后在存储时,选择一个跟key的hash最接近的节点,存储进去.

比如说,AAAAAAA这个key的hash是100,我们还是有100个节点,其中第一个节点的hash值是10000, 第二个节点的hash值是20000,依次类推.那么要存储AAAAAAA这个key的节点就是第一个节点.

我们可以看到,第一个节点能够存放从hash值为0-10000 **的key,第二个节点能够存放hash值为10001-20000**的key.依次类推.

如果我们想要读取key为AAAAAAA的数据,经过计算,从节点1中读取即可.即使我们新增了第101个节点,还是从节点1中读取.

这张图片来自斯坦福大学cs168课程的一份文档中.

上图中的前缀为s的节点,表示集群中的一个节点,前缀为x的节点,表示要被缓存的数据.从第一张圆圈中,我们可以看到,由于x1, x2的hash值里s0最近,所以被存到s0这台节点中了,同理,x0被存到了s2这个节点中了.

在第二个圆圈中,由于多了一台节点,s3, 而x2现在又离它最近,所以,x2需要从s0节点迁移到s3中.

从上图中,我们也能看到,尽管一致性哈希解决了由于集群变动而导致的数据不命中率增加的问题,但是又引入了另一个复杂的问题,就是每个节点的负载不相同,因为每个节点的hash是根据IP计算出来的.

那一致性哈希是如何解决这个问题的呢?

虚节点

一致性哈希中,是通过虚节点来解决这个问题的.所谓的虚节点,顾名思义,就是虚拟的节点.那它到底是什么呢?之前我们在介绍一致性哈希的时候,是将物理节点直接通过哈希运算得到其hash值,而后数据的key计算出来之后,与节点的哈希进行比较,决定存放在哪个节点中.而现在,我们用几个虚拟节点代表一个物理节点.不同虚拟节点的hash是通过不同的哈希函数计算出来的.比如,上图中的三个物理节点,如果每个物理节点有四个虚拟节点的话,经过不同的哈希函数计算,可能得到的结果就是这样了:

这样,通过添加虚节点的方式,就能在一定程度上平衡各个节点的负载.

参考资料

Distributed Systems Part-1: A peek into consistent hashing!

CASSANDRADATABASELABS Consistent Hashing in Cassandra

Standford CS168 Consistency Hash