转载自:http://blog.csdn.net/lovingprince/article/details/4645448
网站为了支撑更大的用户访问量,往往需要对用户访问的数据做cache,对于访 问量特别大的门户网站,一般都提供专门的cache服务机群和负载均衡来专门处理缓存,负载均衡的算法很多,轮循算法、哈希算法、最少连接算法、响应速度 算法等,hash算法是比较常用的一种,它的常用思想是先计算出一个hash值,然后使用 CRC余数算法将hash值和机器数mod后取余数,机器的编号可以是0到N-1(N是机器数),计算出的结果一一对应即可。
我们知道缓存最关键的就是命中率这个因素,如果命中率非常低,那么缓存也就失去了它的意义,因此实际生产环境中我们的一个重要目标就是提高缓存命中率。如 上所述,采用一般的CRC取余的hash算法虽然能达到负载均衡的目的,但是它存在一个严重的问题,那就是如果我们其中一台服务器down掉,那么我们就 需要在计算缓存过程中将这台服务器去掉,即N台服务器,目前就只有N-1台提供缓存服务,此时需要一个rehash过程,而reash得到的结果将导致正 常的用户请求不能找到原来缓存数据的正确机器,其他N-1台服务器上的缓存数据将大量失效,此时所有的用户请求全部会集中到数据库上,严重可能导致整个生 产环境挂掉.
举个例子,有5台服务器,他们编号分别是0(A),1(B),2(C),3(D),4(E) ,正常情况下,假设用户数据hash值为12,那么对应的数据应该缓存在12%5=2号服务器上,假设编号为3的服务器此时挂掉,那么将其移除后就得到一 个新的0(A),1(B),2(C),3(E)(注:这里的编号3其实就是原来的4号服务器)服务器列表,此时用户来取数据,同样hash值为 12,rehash后的得到的机器编号12%4=0号服务器,可见,此时用户到0号服务器去找数据明显就找不到,出现了cache不命中现象,如果不命中 此时应用会从后台数据库重新读取数据再cache到0号服务器上,如果大量用户出现这种情况,那么后果不堪设想。同样,增加一台缓存服务器,也会导致同样 的后果,感兴趣的读者可以自行推敲。
可以有一种设想,要提高命中率就得减少增加或者移除服务器rehash带来的影响,那么有这样一种算法么?Consistent hashing算法就是这样一种hash算法,它的算法思想是:首先求出服务器(节点)的哈希值,并将其配置到0~2^32的圆上。然后用同样的方法求出 存储数据的键的哈希值,并映射到圆上。然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过2^32仍然找不到服务器,就 会保存到第一台服务器上。下面有一张比较经典的图,直接用过来,不修改了。
图一 Consistent Hashing原理示意图
这里有四台服务器,我们假设增加一台服务器Node5,可以看到,它影响的数据只是在增加Node5逆时针方向的数据会受到影响。同样,删除其中一台服务器,例如删除服务器node4,那么影响的数据也只是node4上缓存的数据。
图二 Consistent Hashing添加服务器
Consistent Hashing最大限度地抑制了hash键的重新分布。另外要取得比较好的负载均衡的效果,往往在服务器数量比较少的时候需要增加虚拟节点来保证服务器能 均匀的分布在圆环上。因为使用一般的hash方法,服务器的映射地点的分布非常不均匀。使用虚拟节点的思想,为每个物理节点(服务器)在圆上分配 100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。用户数据映射在虚拟节点上,就表示用户数据真正存储位置是在该 虚拟节点代表的实际物理服务器上。
下面有一个图描述了需要为每台物理服务器增加的虚拟节点。
图三 虚拟节点倍数- 物理节点数关系图
x轴表示的是需要为每台物理服务器扩展的虚拟节点倍数(scale),y轴是实际物理服务器数,可以看出,当物理服务器的数量很小时,需要更大的虚拟节 点,反之则需要更少的节点,从图上可以看出,在物理服务器有10台时,差不多需要为每台服务器增加100~200个虚拟节点才能达到真正的负载均衡。
下面是一个简单的java参考实现:
- 1. import java.util.Collection;
- 2. import java.util.SortedMap;
- 3. import java.util.TreeMap;
- 4.
- 5. public class ConsistentHash<T> {
- 6.
- 7. private final HashFunction hashFunction;
- 8. private final int numberOfReplicas;
- 9. private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
- 10.
- 11. public ConsistentHash(
- 12. HashFunction hashFunction, //hash算法
- 13. int numberOfReplicas,//虚拟节点数
- 14. Collection<T> nodes//物理节点
- 15. ) {
- 16. this.hashFunction = hashFunction;
- 17. this.numberOfReplicas = numberOfReplicas;
- 18.
- 19. for (T node : nodes) {
- 20. add(node);
- 21. }
- 22. }
- 23.
- 24. public void add(T node) {
- 25. for (int i = 0; i < numberOfReplicas; i++) {
- 26. circle.put(hashFunction.hash(node.toString() + i), node);
- 27. }
- 28. }
- 29.
- 30. public void remove(T node) {
- 31. for (int i = 0; i < numberOfReplicas; i++) {
- 32. circle.remove(hashFunction.hash(node.toString() + i));
- 33. }
- 34. }
- 35.
- 36. //关键算法
- 37. public T get(Object key) {
- 38. if (circle.isEmpty()) {
- 39. return null;
- 40. }
- 41. int hash = hashFunction.hash(key);
- 42. if (!circle.containsKey(hash)) {
- 43. SortedMap<Integer, T> tailMap = circle.tailMap(hash);
- 44. hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
- 45. }
- 46. return circle.get(hash);
- 47. }
- 48.
- 49. }
附:
public interface SortedMap<K,V>
保证按照键的升序排列的映射,可以按照键的自然顺序(参见 Comparable 接口)进行排序,或者通过创建有序映射时提供的比较器进行排序。对有序映射的集合视图(由 entrySet、keySet 和 values 方法返回)进行迭代时,此顺序就会反映出来。要采用此排序,还需要提供一些其他操作(此接口是相似于 SortedSet 接口的映射)。
插入有序映射的所有键都必须实现 Comparable 接口(或者被指定的比较器所接受)。另外,所有这些键都必须是可互相比较的:k1.compareTo(k2)(或 comparator.compare(k1, k2))对有序映射中的任意两个元素 k1 和 k2 都不得抛出 ClassCastException。试图违反这些限制将导致违反方法或者构造方法的调用,从而抛出 ClassCastException。
注意,如果有序映射正确实现了 Map 接口,则有序映射所保持的顺序(无论是否明确提供了比较器)都必须保持相等一致性。(相等一致性 的精确定义请参阅 Comparable 接口或 Comparator 接口)。这也是因为 Map 接口是按照 equals 操作定义的,但有序映射使用它的 compareTo(或 compare)方法对所有键进行比较,因此从有序映射的观点来看,此方法认为相等的两个键就是相等的。即使顺序没有保持相等一致性,树映射的行为仍然是 定义良好的,只不过没有遵守 Map 接口的常规协定。
所有通用有序映射实现类都应该提供 4 个“标准”构造方法:1) void(不带参数)构造方法,创建空的有序映射,按照键的自然顺序 排序。2) 带有一个 Comparator 类型参数的构造方法,创建一个空的有序映射,根据指定的比较器排序。3) 带有一个 Map 类型参数的构造方法,创建一个键-值映射关系与参数相同的有序映射,按照键的自然顺序排序。4) 带有一个有序映射类型参数的构造方法,创建一个新的有序映射,键-值映射关系及排序方法与输入的有序映射相同。除了 JDK 实现(TreeMap 类)遵循此建议外,无法保证强制实施此建议(因为接口不能包含构造方法)。
此接口是 Java Collections Framework 的成员。
SortedMap<K,V> tailMap(K fromKey)
如果用户试图插入超出指定范围的键,则此方法返回的映射将抛出 IllegalArgumentException。
注:此方法总是返回包括(低)终结点的视图。如果需要不含此终结点的视图,而且元素类型允许计算给定值的后继值,则只需要请求以 successor(lowEndpoint) 为界的 tailMap。例如,假设 m 是一个用字符串作为键的映射。下面的语句将得到一个包含 m 中键严格大于 low 的所有键-值映射关系的视图:
Map tail = m.tailMap(low+"\0");
fromKey
- tailMap 的低终结点(包括)。
ClassCastException
- 如果 fromKey 与此映射的比较器不兼容(如果此映射没有比较器,并且 fromKey 没有实现 Comparable)。如果 fromKey 不能和映射中的当前键比较,则实现可能抛出此异常,但不要求。
IllegalArgumentException
- 如果此映射本身是一个 subMap、headMap 或 tailMap,并且 fromKey 不在 subMap、headMap 或 tailMap 指定的范围之内。
NullPointerException
- 如果 fromKey 为 null,并且此有序映射不能接受 null 键。
K firstKey()
NoSuchElementException
- 如果是空映射。