layout: post
title: "一致性hash算法"
category: Reading Notes
tags: ["读文章", "hash"]
{% include JB/setup %}
一致性hash算法
基本场景
比如有N个cache服务器,那么如何将一个对象object映射到N个cache上,你可能会采用下面的通用方法计算object的hash值,然后均匀的映射到N个cache
hash(object) % N
一切都运行正常,再考虑如下的两种情况:
一个cache服务器M挂掉,这样所有映射到cache M的对象都会失效。怎么办?需要把cache M从cache中移除,这时候cache是N-1台,映射公式变成了
hash(object) % (N-1)
由于访问加重,需要添加cache,这时候cache是N+1台,映射公式变成了
hash(object) % (N+1)
1和2意味着什么?
这意味着突然之间所有的cache都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器。
再考虑第三个问题,由于硬件能力越来越强,你可能会让后面添加的节点多做点活,显然上面的hash算法也做不到。
hash算法和单调性
hash算法的一个衡量指标是单调性(Monotonicity),定义如下:
单调性是指如果已经有一些内容通过哈希分派到了响应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区
当缓冲区大小变化时Consistent hashing尽量保护已分配的内容不会被重新映射到新缓冲区。
因此上面提到的 hash(object) % N 难以满足单调性要求
consistent hashing算法的原理
consistent hashing是一种hash算法,简单的说,在移除/添加一个cache时,它能够尽可能小的改变已存在key映射关系,尽可能的满足单调性的要求。
环形hash空间
考虑通常的hash算法都是将value映射到一个32位的key值,也就是 0~232-1 次方的数值空间;我们可以将这个空间想象成首(0)尾(232-1)相接的圆环,如下图所示:
把对象映射到hash空间
接下来考虑 4 个对象 object1~object4 ,通过 hash 函数计算出的 hash 值 key 在环上的分布如图 2 所示。
hash(object1) = key1;
… …
hash(object4) = key4;
把cache映射到hash空间
Consistent hashing 的基本思想就是将对象和 cache 都映射到同一个 hash 数值空间中,并且使用相同的 hash 算法。
假设当前有 A,B 和 C 共 3 个cache ,那么其映射结果将如图 3 所示,他们在 hash 空间中,以对应的 hash 值排列。
hash(cache A) = key A;
… …
hash(cache C) = key C;
顺便提一下 cache 的 hash 计算,一般的方法可以使用 cache 机器的 IP 地址或者机器名作为 hash 输入。
把对象映射到cache
现在cache和对象都已经通过同一个hash算法映射到hash数值空间中了,接下来要考虑如何将对象映射到cache上面
在这个环形空间中,如果沿着顺时针方向从对象的key值出发,直到遇见一个cache,那么就将该对象存储在这个cache上,因为对象和cache的hash值是固定的,因此这个cache必然是唯一和确定的。这样就找到了对象和cache的映射方法了。
依然继续上面的例子(参见图 3 ),那么根据上面的方法,对象 object1 将被存储到 cache A 上; object2 和 object3 对应到 cache C ; object4 对应到 cache B
考察cache的变动
分析consistent hashing算法
移除cache
假设cache B挂掉了,根据上面讲到的映射方法,这时受影响的将仅是那些沿 cache B 逆时针遍历直到下一个 cache ( cache C )之间的对象,也即是本来映射到 cache B 上的那些对象
添加cache
再考虑添加一台新的 cache D 的情况,假设在这个环形 hash 空间中, cache D 被映射在对象 object2 和 object3 之间。这时受影响的将仅是那些沿 cache D 逆时针遍历直到下一个 cache ( cache B )之间的对象(它们是也本来映射到 cache C 上对象的一部分),将这些对象重新映射到 cache D 上即可。
虚拟节点
考量hash算法的另一个指标是平衡性(balance),定义如下:
平衡性:
平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
hash算法并不是保证部署的绝对平衡,如果cache较少的话,对象并不能被均匀的映射到cache上,比如在上面的例子中,仅部署cacheA和cacheC的情况下,在 4 个对象中, cache A 仅存储了 object1 ,而 cache C 则存储了 object2 、 object3 和 object4 ;分布是很不均衡的。
为了解决这种情况, consistent hashing 引入了“虚拟节点”的概念,它可以如下定义:
“虚拟节点(virtual node)”是实际节点在hash空间的复制品(replica),一个实际节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在hash空间中以hash值排列
仍以仅部署 cache A 和 cache C 的情况为例,在图 4 中我们已经看到, cache 分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为 2 ,这就意味着一共会存在 4 个“虚拟节点”, cache A1, cache A2 代表了 cache A ; cache C1, cache C2 代表了 cache C ;
此时,对象到“虚拟节点”的映射关系为:
objec1->cache A2 ;
objec2->cache A1 ;
objec3->cache C1 ;
objec4->cache C2 ;
因此对象 object1 和 object2 都被映射到了 cache A 上,而 object3 和 object4 映射到了 cache C 上;平衡性有了很大提高
引入“虚拟节点”后,映射关系就从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 。查询物体所在 cache 时的映射关系如图 7 所示。
“虚拟节点”的 hash 计算可以采用对应节点的 IP 地址加数字后缀的方式。例如假设 cache A 的 IP 地址为 202.168.14.241 。
引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“202.168.14.241”);
引入“虚拟节点”后,计算“虚拟节”点 cache A1 和 cache A2 的 hash 值:
Hash(“202.168.14.241#1”); // cache A1
Hash(“202.168.14.241#2”); // cache A2
The circle is represented as a sorted map of integers, which represent the hash values, to caches (of type T here).
When a ConsistentHash object is created each node is added to the circle map a number of times (controlled by numberOfReplicas).
The location of each replica is chosen by hashing the node's name along with a numerical suffix, and the node is stored at each of these points in the map.
To find a node for an object (the get method), the hash value of the object is used to look in the map. Most of the time there will not be a node stored at this hash value (since the hash value space is typically much larger than the number of nodes, even with replicas), so the next node is found by looking for the first key in the tail map. If the tail map is empty then we wrap around the circle by getting the first key in the circle.