问题
假设你有 N 个 缓存服务器(后面简称 cache),那么如何将一个对象 object 映射到 N 个 cache 上呢,你很可能会采用类似下面的通用方法计算 object 的 hash 值,然后均匀的映射到到 N 个 cache ;
hash(object) % N
一切都运行正常,再考虑如下的两种情况:
- 其中1个 cache 服务器 m down 掉了
映射到 cache m 的对象都会失效,怎么办,需要把 cache m 从 cache 中移除,
这时候 cache 是 N-1 台
映射公式变成了 hash(object) % (N-1)
- 由于访问加重,需要添加新的 cache
这时候 cache 是 N+1 台,
映射公式变成了 hash(object) % (N+1)
以上两种情况意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台 cache 服务器;
再来考虑第3个问题,随着业务数据的不断增长,你希望通过增加 cache 来分担压力,显然上面的 hash 算法做不到。
有什么方法可以改变这个状况呢,这就是一致性哈希 (consistent hashing)
一致性哈希 是一种 hash 算法,简单的说,就是在移除 / 添加一个 cache 时,最大限度地减小服务节点增减时的缓存重新分布。
概念
Consistent Hashing 算法早在 1997 年就在论文《Consistent hashing and random trees》中被提出,提出了在动态变化的Cache环境中,哈希算法应该满足的4个适应条件:
- 平衡性(Balance) :hash的结果尽量平均分配到各个节点
- 单调性(Monotonicity) :新增或删减节点,同一个Key访问到的值总是相同
- 分散性(Spread) :避免相同的数据被存储到不同的节点,hash的碰撞率
- 负载 (Load) :尽量平衡各个节点的负荷
一致性哈希原理
一致性哈希将 key 用 hash 函数进行映射,映射出来的所有节点能够分布到一个圆环内,实际上consistent hashing 是一种 hash 算法, 在改变映射内容的大小时,而不需要改变 hash 算法,而且能够尽可能小的改变已存在 key 映射关系,尽可能的满足单调性的要求。
算法简介
一致性哈希算法将 value 映射到一个 32 位的 key 值,也即是 0~2^32-1 次方的地址空间,我们可以将这个空间想象成一个首 (0) 尾 (2^32-1) 相接的圆环。
将数据计算出hash后存放至距离它顺时针最近的节点上。
- 如果环上某个节点宕掉,只需将该节点负责的数据转移至其顺时针方向的下一个节点
- 如果环上新增一个节点,只需将从添加位置到它的逆时针方向第一个节点这一段的数据移动即可。
这样就最大限度上抑制了数据的重分布。
不足之处
如果哈希函数对数据的散列效果不好或者由于节点位置的分布不均导致整个地址空间中的负载不均,有什么常用的解决方案?
- 虚拟节点
即地址空间上的每一个节点都是一个虚拟的节点,而一个物理节点可以根据自身的性能、参数等状况划分为多个虚拟节点,即环上的多个节点对应一台机器。Amazon Dynamo 模型就采用此种方法。
- 移动节点位置
参考 Chord 的模式,即分析环上的负载,根据负载信息来移动环上的节点从而减轻负载。Cassandra 偏向采用后一种方式,通过负载分析移动节点来减轻负载,更具备针对性。
代码实现
一致性哈希算法
import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistentHash<T> {
private final HashFunction hashFunction;
private final int numberOfReplicas;
private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<T> nodes) {
this.hashFunction = hashFunction;
this.numberOfReplicas = numberOfReplicas;
for(T node:nodes){
add(node);
}
}
public void add(T node) {
for(int i = 0; i < numberOfReplicas; i++){
circle.put(hashFunction.hash(node.toString() + i), node);
}
}
public void remove(T node) {
for(int i = 0; i < numberOfReplicas; i++){
circle.remove(hashFunction.hash(node.toString() + i));
}
}
public T get(Object key) {
if(circle.isEmpty()){
return null;
}
int hash = hashFunction.hash(key);
if(!circle.containsKey(hash)){
SortedMap<Integer, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
}
一致性哈希函数接口
public interface HashFunction {
int hash(Object obj);
}
参考资料
http://weblogs.java.net/blog/2007/11/27/consistent-hashing
http://blog.csdn.net/sparkliang/article/details/5279393