一、背景
一个设计良好的分布式哈希方案应该具有良好的单调性,即服务节点的增减不会造成大量哈希重定位;常见的缓存情况:在分布式缓存集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了;具体案例如下:
object.hashCode()% 4。
Cache 0: object.hashCode() % 4 == 0 |
Cache 1: object.hashCode() % 4 == 1 |
Cache 2: object.hashCode() % 4 == 2 |
Cache 3: object.hashCode() % 4 == 2 |
看起来一切正常,考虑下面两种情况:
1:由于Cache3硬件损坏,所有Cache3上的缓存都失效了,需要把Cache3移除。
2:由于负载已经无法承担业务增涨,决定添加一台Cache服务器。
二、算法基本原则
平衡性(Balance):平衡性是指Hash的结果能够尽可能分布均匀,充分利用所有缓存空间;单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
分散性(Spread):分散性定义了分布式环境中,不同终端通过Hash过程将内容映射至缓存上时,因可见缓存不同,Hash结果不一致,相同的内容被映射至不同的缓冲区。
负载(Load):负载是对分散性要求的另一个纬度。既然不同的终端可以将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。
那么一致性哈希是如何解决上述问题的呢?基本思想:
使用一致性哈希算法解决上述问题
一致性哈希算法采用一种新的方式来解决问题,不再仅仅依赖object.hashCode()本身,而且将Cache的配置也进行哈希运算。具体步骤如下:
1. 首先求出每个Cache的哈希值,并将其配置到一个0~2^32的圆环区间上。
2. 使用同样的方法求出需要存储对象的哈希值,也将其配置到这个圆环上。
3. 从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个 Cache 节点上。如果超过 2^32 仍然找不到 Cache 节点,就会保存到第一个 Cache 节点上。for example:
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;
通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;
针对插入缓存服务器节点的处理:
假设在这个环形哈希空间中,Cache5被映射在Cache3和Cache4之间,那么受影响的将仅是沿Cache5逆时针遍历直到下一个Cache(Cache3)之间的对象(它们本来映射到Cache4上)。
针对移除服务器的场景
假设在这个环形哈希空间中,Cache3被移除,那么受影响的将仅是沿Cache3逆时针遍历直到下一个Cache(Cache2)之间的对象(它们本来映射到Cache3上)。
如何解决负载平衡问题?思考:负载不平衡产生的原因是什么?解决思路是不是缩小【间隔】,使得对象的分布比较均匀;
考虑到哈希算法并不是保证绝对的平衡,尤其Cache较少的话,对象并不能被均匀的映射到 Cache上。为了解决这种情况,Consistent Hashing引入了“虚拟节点”的概念: “虚拟节点”是实际节点在环形空间的复制品,一个实际节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在哈希空间中以哈希值排列。
总结:总体解决思路,不仅哈希缓存数据【缓存数据的目前主要存储方式,k,v】,同时哈希缓存节点【如通过服务器别名、iP地址等】;提升均衡度的方法是缩小间隔,平均分布。