分布式哈希表DHT和一致性哈希

分布式哈希(DHT)
两个key point:每个节点只维护一部分路由;每个节点只存储一部分数据。从而实现整个网络中的寻址和存储。
DHT
只是一个概念,提出了这样一种网络模型。并且说明它是对分布式存储很有好处的。但具体怎么实现,并不是DHT的范畴。

一致性哈希:
DHT
的一种实现。本质还是一个哈希算法。回想平时我们做负载均衡,按querystring签名对后端节点取模是最简单也是最常用的算法,但节点的增删所造成的问题显而易见:原有的请求几乎都落不到同一台机器上。优化一点的是carp算法,只让1/n的数据受到影响。
一致性哈希,似乎最早提出是在分布式缓存里面的,让节点震荡的时候,影响最小。不过现在已经应用在分布式存储和p2p系统里面。

一致性哈希也只是提出四个概念和原则,并没有提及具体实现:

1
balance:哈希结果尽可能的平均分散到各个节点上,使得每个节点都能得到充分利用。

2
Monotonicity:上面也说了,如果是用签名取模算法,节点变更会使得整个网络的映射关系更改。如果是carp,会使得1/n的映射关系更改。一致性哈希的目标,是节点变更,不会改变网络的映射关系。

3
spread:同一份数据,存储到不同的节点上,换言之就是系统冗余。一致性哈希致力于降低系统冗度。

4
load:负载分散,和balance其实是差不多的意思,不过这里更多是指数据存储的均衡,balance是指访的均衡。


Chord
算法:
一致性哈希有多种实现算法,最关键的问题在于如何定义数据分割策略和节点快速查询。

chord算是最为经典的实现。cassandra中的DHT,基本是chord的简化版。

网络中每个节点分配一个唯一id,可以通过机器的mac地址做sha1,是网络发现的基础。

假设整个网络有N 个节点,并且网络是呈环状。两个节点间的距离定义为每个节点会存储一张路由表(finger表),表内顺时针按照离本节点2、4、8、16、32.……2i的距离选定log2N个其他节点的ip信息来记录。

 

 

存储方面:数据被按一定规则切割,每一份数据也有一个独立id(查询key),并且和节点id的值域是一样的。然后查找节点,如果存在和数据id一样的节点id,则将这份数据存在该节点上;如果不存在,则存储到离该数据id距离最近的节点上。同时,为了保证数据的可靠性,会顺时针往下找K个冗余节点,存储这份数据。一般认为K=3是必须的。

查询方面:先从自己的路由表中,找一个和数据id距离最近、并且存活在网络中的节点next。如果该节点的id巧合和数据id相等,那么恭喜你。如果不相等,则到next进行递归查找。一般或需要经过多次查询才能找到数据所在的节点,而这个次数是可以被证明小于等于log2N的。

在这个查询的过程中就体现了路由表的选取优势了,其实是实现了一个二分查找,从每个节点来观察网络,都是将网络分成了log2N块,最大一块里面有N/2个节点。路由表里面其实是记录了每一块的第一个节点。这样每一次查询,最少排除了一半的节点。保证在log2N次内找到目标节点。

新增一个节点i,需要预先知道网络中已经存活的一个节点j,然后通过和节点j交互,更新自己和其他节点的路由表。并且,需要将离自己距离最近的节点中的数据copy过来,以提供数据服务。

损失一个节点,路由算法会自动跳过这个节点,并且依靠数据的冗余来持续提供服务。

KAD算法(Kademlia)
个人觉得,kad算法其实是在chord上做的优化。主要是两个点:
1
、用二进制(32/64/128)表示一个节点的id,两节点的id异或运算得到节点间的距离。
2
 每个节点保持的路由信息更丰富,同样是将整个网络按照划分成log2N份,在chord中,是保持log2N个路由节点,但在kad里面,是保存了 log2N个队列。每个队列长度为配置值K,记录网络中对应节点区域的多个节点,并且根据活跃时间对这些节点进行换入换出。
第一点是方便进行网络划分,节点按照二进制中每一bit01建成一棵二叉树。

第二点是使得节点查询更迅速。从分割情况我们就可以得知,最坏情况不会差于chord,但保存更多的节点使得命中概率更高。另外队列中根据活跃时间进行换入换出,更有利于在p2p这种节点变更频繁的网络中快速找到有效的节点。

关于kad的介绍,这篇文章讲的比较详细wenku.baidu.com/view/ee91580216fc700abb68fcae.html


 在大型web应用中,缓存可算是当今的一个标准开发配置了。在大规模的缓存应用中,应运而生了分布式缓存系统。分布式缓存系统的基本原理,大家也有所耳闻。key-value如何均匀的分散到集群中?说到此,最常规的方式莫过于hash取模的方式。比如集群中可用机器适量为N,那么key值为K的的数据请求很简单的应该路由到hash(K) mod N对应的机器。的确,这种结构是简单的,也是实用的。但是在一些高速发展的web系统中,这样的解决方案仍有些缺陷。随着系统访问压力的增长,缓存系统不得不通过增加机器节点的方式提高集群的相应速度和数据承载量。增加机器意味着按照hash取模的方式,在增加机器节点的这一时刻,大量的缓存命不中,缓存数据需要重新建立,甚至是进行整体的缓存数据迁移,瞬间会给DB带来极高的系统负载,设置导致DB服务器宕机。那么就没有办法解决hash取模的方式带来的诟病吗?看下文。

一致性哈希(Consistent Hashing):

      选择具体的机器节点不在只依赖需要缓存数据的key的hash本身了,而是机器节点本身也进行了hash运算。


(1) hash机器节点


首先求出机器节点的hash值(怎么算机器节点的hash?ip可以作为hash的参数吧。。当然还有其他的方法了),然后将其分布到0~2^32的一个圆环上(顺时针分布)。如下图所示:
 
 

集群中有机器:A , B, C, D, E五台机器,通过一定的hash算法我们将其分布到如上图所示的环上。


(2)访问方式

如果有一个写入缓存的请求,其中Key值为K,计算器hash值Hash(K), Hash(K) 对应于图 – 1环中的某一个点,如果该点对应没有映射到具体的某一个机器节点,那么顺时针查找,直到第一次找到有映射机器的节点,该节点就是确定的目标节点,如果超过了2^32仍然找不到节点,则命中第一个机器节点。比如 Hash(K) 的值介于A~B之间,那么命中的机器节点应该是B节点(如上图)。


(3)增加节点的处理

如上图 – 1,在原有集群的基础上欲增加一台机器F,增加过程如下:

计算机器节点的Hash值,将机器映射到环中的一个节点,如下图:

 

增加机器节点F之后,访问策略不改变,依然按照(2)中的方式访问,此时缓存命不中的情况依然不可避免,不能命中的数据是hash(K)在增加节点以前落在C~F之间的数据。尽管依然存在节点增加带来的命中问题,但是比较传统的 hash取模的方式,一致性hash已经将不命中的数据降到了最低。

 

Consistent Hashing最大限度地抑制了hash键的重新分布。另外要取得比较好的负载均衡的效果,往往在服务器数量比较少的时候需要增加虚拟节点来保证服务器能均匀的分布在圆环上。因为使用一般的hash方法,服务器的映射地点的分布非常不均匀。使用虚拟节点的思想,为每个物理节点(服务器)在圆上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。用户数据映射在虚拟节点上,就表示用户数据真正存储位置是在该虚拟节点代表的实际物理服务器上。
下面有一个图描述了需要为每台物理服务器增加的虚拟节点。

 


  

x轴表示的是需要为每台物理服务器扩展的虚拟节点倍数(scale),y轴是实际物理服务器数,可以看出,当物理服务器的数量很小时,需要更大的虚拟节点,反之则需要更少的节点,从图上可以看出,在物理服务器有10台时,差不多需要为每台服务器增加100~200个虚拟节点才能达到真正的负载均衡。
 


===

原文地址:http://blog.csdn.net/chen77716/archive/2010/10/18/5949166.aspx


 

 直到现在为止,一致性哈希也没有一个非常明确的定义,多数文献还是从其应用场景之上对一致性哈希进行描述。“哈希”想必大家都已经了解,问题是何为“一致性”?

  1. 一致性

  在讨论一致性哈希之前,先认识下“非一致性哈希”,显然HashMap属于此列。

  当使用HashMap时,key被均匀地映射到数组之上,映射方法就是利用key的hash与数组长度取模(通过&运算)。

  当put的数据超过负载因子loadFactor×2Len时,HashMap会按照2被的容量扩容。新put进来的数据会通过与新数组的长度取模的方式进行映射。那之前已经映射的数据该怎么办?通过查看HashMap代码的resize方法会发现,每次扩容都会把之前的key重新映射。

  所以对HashMap而言要想获得较好的性能必须要提前估计所放数据集合的大小,以设计合适的初始化容量和负载因子。

  2. 定义

 但不是每个场景都像HashMap这么简单,比如在大型的P2P网络中存在上百万台Server,资源与Server的关系是以Key的形式映射而成,也就是说是一个大的HashMap,维护着每个Key在哪个Server之上,如果有新的节点加入或退出P2P网络,跟HashMap一样,也会导致映射关系的变化,显然不可能把所有的Key与Server的映射关系都调整一遍。这就需要一种方法,在哈希项发生变化是,不需要调整所有的节点,而达到继续维护哈希映射的关系。因此一致性哈希定义为:

"Consistent hashing is a scheme that provides hash table functionality in a way that the addition or removal of one slot does not significantly change the mapping of keys to slots".(http://en.wikipedia.org/wiki/Consistent_hashing)

就是说,”一致性哈希,就是提供一个hashtable,它能在节点加入离开时不会导致映射关系的重大变化“。

3.实现

一致性哈希的定义除了描述一个定义或者一种想法并没有给出任何实现方面的描述,所有细化的问题都留给开发者去思考。但一般的实现思路如下:

  • 假定哈希的均匀Key分布在一个环上,比如所有节点都通过SHA-1或MD5进行哈希映射
  • 所有的节点也都分布在同一环上(比如Server的IP地址经过SHA-1)
  • 每个节点只负责一部分Key,当节点加入、退出时只影响加入退出的节点和其邻居节点或者其他节点只有少量的Key受影响

假如有n个节点,m个key,当节点增加时大约有O(m/n)的节点需要移动。但一般一致性哈希需要满足下面几个条件才对实际系统有意义:

  • 平衡性(Balance):就是指哈希算法要均匀分布,不能有明显的映射规律,这对一般的哈希实现也是必须的
  • 单调性(Monotonicity):就是指有新节点加入时,已经存在的映射关系不能发生变化
  • 分散性(Spread):就是避免不同的内容映射到相同的位置和相同的内容映射到不同的位置

其实一致性哈希(哈希)有个明显的优点就是负载均衡,只要哈希函数设计得当,每个点就是对等的可以均匀地分布系统负载。

4.Memcached

看了上面的定义和实现可能还是比较迷茫,那就举个实际例子。

Memcached对大家应该不陌生,通过把Key映射到Memcached Server上,实现快速读取。我们可以动态对其节点增加,并未影响之前已经映射到内存的Key与memcached Server之间的关系,这就是因为使用了一致性哈希。

因为Memcached的哈希策略是在其客户端实现的,因此不同的客户端实现也有区别,以Spymemcache、Xmemcache为例,都是使用了KETAMA作为其实现。

KETAMA实现方式如下:

  • 把Server的IP地址和端口进行MD5哈希,MD5的结果为一个160bit的数字,取其前32位作为一个Integer
  • 把缓存对象的Key做MD5哈希,同样得到一个整数
  • 可以设想,Server的整数会根据大小形成一个数字环,而Key的哈希则分布在这些数字上或中间
  • 如果Server的哈希等于Key的哈希,则把Key存放在该Server上;否则,寻找第一个大于Key哈希的Server,用于存放Key
  • 但有Server增加、删除时,只要变动周边的Server映射关系即可,不用全部重新哈希。之所以有这样优良的特性是因为,Server和Key采用了同样的值域

但是这样做的效果并不理想,原因是哈希虽然是随机的,但往往随机的不如人意,尤其是在Server节点数量上的情况下,Server不会均匀分布在哈希环上,这会导致哈希不均匀,某些Server会承担很多的Key,而另一些会很少,如图:

绝大多数Key会映射到Server1,因此KETAMA引入了虚节点的概念,就是假象每个Server映射到N个节点(根据测试N在100~200时较优化),但Key的哈希映射到这N个节点时实际都有该Server来托管。这样做的意义在于,使因为实际节点少而导致大片未被映射的区别有虚节点去填充,从而使实节点有了处理本不属于自己区间的Key。有虚节点后的环如下:

新增的同名节点即为虚节点。

还有最后一个问题,虚节点是如何产生的呢?也非常简单,就是在每个Server加个后缀,在做MD5哈希,取其32位。

具体请参考KETAMA的源码:http://www.chris.de/archives/288-libketama-a-consistent-hashing-algo-for-memcache-clients.html


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页