外网上一篇关于一致性哈希讲得很清晰的文章,翻译记录一下。
原文地址:https://tom-e-white.com/2007/11/consistent-hashing.html
*根据博主习惯,下文中部分位置保留原英文词
原理
对于一致性哈希(Consistent Hashing)的需求随着运行缓存机群组时的限制而浮现。如果你有一组 n 台的缓存机,那么做负载均衡的一个普遍方法是将object o放在第 hash(o) mod n 台机器。然而当你增加或移除缓存机时,n 发生变化,导致每个对象都会被hash到新的位置。这可能造成灾难性的后果,存储源数据的机器会突然收到大量缓存机的请求,就好像所有缓存突然消失了一样。
我们希望的是,当一台缓存机加入时,它从其他机器承担一部分合理的负载。同样的,当一台缓存机被移除时,它承载的负载被分配到其它机器上。这就是一致性哈希所做的——尽可能 一致地 将同一对象映射到同一台缓存机。
一致性哈希背后的理念是使用hash函数同时对object和cache做hash计算。这样做的目的是将cache映射到一个区间(interval),该区间将容纳数个object的哈希。如果cache被移除,这个区间内的object将被相邻的cache区间承担。其他所有cache都不会改变。
具体来说,hash函数通常将object和cache映射到一个数字区间。所有Java程序员都应该对此感到熟悉——Object
类的hashCode
方法返回一个int值(-231 to 231-1)。想象将这个范围放到一个圈上,数字落在圈上的点。object(1,2,3,4)和cache(A,B,C)被标记在他们hash值的位置。
为了找到一个object所在的cache,我们从object位置出发,顺时针前进直到找到一个cache点。所以上图中,object 1和4落在cache A,object 2落在cache B,object 3落在cache C。考虑如果C被移除会发生什么:object 3现在属于cache A,其他object的映射都没有改变。接下来如果cache D被加到图中所示位置,它会接管object 3和4。
以上系统能良好运行有一个条件:所有cache应被均匀地击中。然而由于随机性(译注:并且物理cache节点相对object节点数量很少),有可能出现objects极不均匀分布于cache上的情况。解决方案是引入“虚拟节点”的概念,也就是圈上cache点的复制。每当加入物理节点时,我们创建数个虚拟节点并实际加入圆环。
下图可以看出这样做的显著效果。我在10个cache上模拟存储了10,000个objects。x轴代表cache的复制数(对数规模)。当该值较小时,可以看到objects在caches上的分布是不均衡的(y轴上显示的标准差很大)。当复制数增加时,objects的分布明显更均匀了。这个实验表明100至200的复制数能达到一个可接受的平衡度(标准差为5%-10%平均值)。
实现
下面是一份简单的Java实现。为了一致性哈希能有效实现,使用一个能充分打乱的hash函数非常重要。然而Object
的hashCode
方法并不能很好做到这一点——它们一般产生一组较小的integer——所以我们使用HashFunction
接口以允许使用自定义的哈希函数。这里推荐MD5哈希。
译注:
- MD5哈希得到128-bit串,一般以32位十六进制数表示,如果需要得到数字值(例如,Java中long, 64位),需要进行截断等处理。也可以考虑使用FNV,Murmur等算法直接实现字符串到N位数字的哈希。
- 作者此处未给MD5哈希的实现代码,可以参考github上这位作者的实现。
HashFunction接口:
public interface HashFunction {
Integer hash(Object key);
}
ConsistentHash.java:
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);
}
}