一致性哈希 一致性哈希算法原理

一致性哈希

 

参考:

https://www.cnblogs.com/lpfuture/p/5796398.html

https://blog.csdn.net/u010558660/article/details/52767218

https://blog.csdn.net/lihao21/article/details/54193868

 

 

 

一致性哈希算法原理

 

一致性Hash算法背景

  一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得DHT可以在P2P环境中真正得到应用。

  但现在一致性hash算法在分布式系统中也得到了广泛应用,研究过memcached缓存数据库的人都知道,memcached服务器端本身不提供分布式cache的一致性,而是由客户端来提供,具体在计算一致性hash时采用如下步骤:

  1. 首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。
  2. 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
  3. 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。

  从上图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在园(continuum)上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响,如下图所示:

 

一致性Hash性质

  考虑到分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来,如何保证当系统的节点数目发生变化时仍然能够对外提供良好的服务,这是值得考虑的,尤其实在设计分布式缓存系统时,如果某台服务器失效,对于整个系统来说如果不采用合适的算法来保证一致性,那么缓存于系统中的所有数据都可能会失效(即由于系统节点数目变少,客户端在请求某一对象时需要重新计算其hash值(通常与系统中的节点数目有关),由于hash值已经改变,所以很可能找不到保存该对象的服务器节点),因此一致性hash就显得至关重要,良好的分布式cahce系统中的一致性hash算法应该满足以下几个方面:

  • 平衡性(Balance)

平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

  • 单调性(Monotonicity)

单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。简单的哈希算法往往不能满足单调性的要求,如最简单的线性哈希:x = (ax + b) mod (P),在上式中,P表示全部缓冲的大小。不难看出,当缓冲大小发生变化时(从P1到P2),原来所有的哈希结果均会发生变化,从而不满足单调性的要求。哈希结果的变化意味着当缓冲空间发生变化时,所有的映射关系需要在系统内全部更新。而在P2P系统内,缓冲的变化等价于Peer加入或退出系统,这一情况在P2P系统中会频繁发生,因此会带来极大计算和传输负荷。单调性就是要求哈希算法能够应对这种情况。

  • 分散性(Spread)

在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。

  • 负载(Load)

负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

  • 平滑性(Smoothness)

平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。

原理

基本概念

  一致性哈希算法(Consistent Hashing)最早在论文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出。简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希空间环如下:

  整个空间按顺时针方向组织。0和232-1在零点中方向重合。

  下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下:

  

接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。

  例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:

根据一致性哈希算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。

下面分析一致性哈希算法的容错性和可扩展性。现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。

下面考虑另外一种情况,如果在系统中增加一台服务器Node X,如下图所示:

此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X 。一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。

综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

另外,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,

此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

一致性哈希算法与Java实现

========================================================

一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将数据存储到具体的节点上,
如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,key是数据的key,N是机器节点数,
如果有一个机器加入或退出这个集群,则所有的数据映射都无效了。

一致性哈希算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。

一、一致性哈希算法

1、原理

(1)环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。

现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图

(2)把数据通过一定的hash算法处理后映射到环上

现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;


(3)将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中

(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。

假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;


通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。

在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

2、机器的删除与添加
普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会造成大量的对象存储位置失效。下面来分析一下一致性哈希算法是如何处理的。

(1)节点(机器)的删除
以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:


(2)节点(机器)的添加 
如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:


通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持着原有的存储位置。
通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。

3、平衡性--虚拟节点

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,

因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。

hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就造成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。

——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一个实际节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。

以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:


根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:


“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2

二、一致性hash算法的Java实现

1、不带虚拟节点的

[java]  view plain  copy
 
  1. package hash;  
  2.   
  3. import java.util.SortedMap;  
  4. import java.util.TreeMap;  
  5.   
  6. /** 
  7.  * 不带虚拟节点的一致性Hash算法 
  8.  */  
  9. public class ConsistentHashingWithoutVirtualNode {  
  10.   
  11.     //待添加入Hash环的服务器列表  
  12.     private static String[] servers = { "192.168.0.0:111", "192.168.0.1:111",  
  13.             "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111" };  
  14.   
  15.     //key表示服务器的hash值,value表示服务器  
  16.     private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();  
  17.   
  18.     //程序初始化,将所有的服务器放入sortedMap中  
  19.     static {  
  20.         for (int i=0; i<servers.length; i++) {  
  21.             int hash = getHash(servers[i]);  
  22.             System.out.println("[" + servers[i] + "]加入集合中, 其Hash值为" + hash);  
  23.             sortedMap.put(hash, servers[i]);  
  24.         }  
  25.         System.out.println();  
  26.     }  
  27.   
  28.     //得到应当路由到的结点  
  29.     private static String getServer(String key) {  
  30.         //得到该key的hash值  
  31.         int hash = getHash(key);  
  32.         //得到大于该Hash值的所有Map  
  33.         SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);  
  34.         if(subMap.isEmpty()){  
  35.             //如果没有比该key的hash值大的,则从第一个node开始  
  36.             Integer i = sortedMap.firstKey();  
  37.             //返回对应的服务器  
  38.             return sortedMap.get(i);  
  39.         }else{  
  40.             //第一个Key就是顺时针过去离node最近的那个结点  
  41.             Integer i = subMap.firstKey();  
  42.             //返回对应的服务器  
  43.             return subMap.get(i);  
  44.         }  
  45.     }  
  46.       
  47.     //使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别  
  48.     private static int getHash(String str) {  
  49.         final int p = 16777619;  
  50.         int hash = (int) 2166136261L;  
  51.         for (int i = 0; i < str.length(); i++)  
  52.             hash = (hash ^ str.charAt(i)) * p;  
  53.         hash += hash << 13;  
  54.         hash ^= hash >> 7;  
  55.         hash += hash << 3;  
  56.         hash ^= hash >> 17;  
  57.         hash += hash << 5;  
  58.   
  59.         // 如果算出来的值为负数则取其绝对值  
  60.         if (hash < 0)  
  61.             hash = Math.abs(hash);  
  62.         return hash;  
  63.         }  
  64.   
  65.     public static void main(String[] args) {  
  66.         String[] keys = {"太阳", "月亮", "星星"};  
  67.         for(int i=0; i<keys.length; i++)  
  68.             System.out.println("[" + keys[i] + "]的hash值为" + getHash(keys[i])  
  69.                     + ", 被路由到结点[" + getServer(keys[i]) + "]");  
  70.     }  
  71. }  

执行结果:
[192.168.0.0:111]加入集合中, 其Hash值为575774686
[192.168.0.1:111]加入集合中, 其Hash值为8518713
[192.168.0.2:111]加入集合中, 其Hash值为1361847097
[192.168.0.3:111]加入集合中, 其Hash值为1171828661
[192.168.0.4:111]加入集合中, 其Hash值为1764547046

[太阳]的hash值为1977106057, 被路由到结点[192.168.0.1:111]
[月亮]的hash值为1132637661, 被路由到结点[192.168.0.3:111]
[星星]的hash值为880019273, 被路由到结点[192.168.0.3:111]

2、带虚拟节点的

[java]  view plain  copy
 
  1. package hash;  
  2.   
  3. import java.util.LinkedList;  
  4. import java.util.List;  
  5. import java.util.SortedMap;  
  6. import java.util.TreeMap;  
  7.   
  8. import org.apache.commons.lang.StringUtils;  
  9.   
  10. /** 
  11.   * 带虚拟节点的一致性Hash算法 
  12.   */  
  13.  public class ConsistentHashingWithoutVirtualNode {  
  14.   
  15.      //待添加入Hash环的服务器列表  
  16.      private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111",  
  17.              "192.168.0.3:111", "192.168.0.4:111"};  
  18.        
  19.      //真实结点列表,考虑到服务器上线、下线的场景,即添加、删除的场景会比较频繁,这里使用LinkedList会更好  
  20.      private static List<String> realNodes = new LinkedList<String>();  
  21.        
  22.      //虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称  
  23.      private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>();  
  24.                
  25.      //虚拟节点的数目,这里写死,为了演示需要,一个真实结点对应5个虚拟节点  
  26.      private static final int VIRTUAL_NODES = 5;  
  27.        
  28.      static{  
  29.          //先把原始的服务器添加到真实结点列表中  
  30.          for(int i=0; i<servers.length; i++)  
  31.              realNodes.add(servers[i]);  
  32.            
  33.          //再添加虚拟节点,遍历LinkedList使用foreach循环效率会比较高  
  34.          for (String str : realNodes){  
  35.              for(int i=0; i<VIRTUAL_NODES; i++){  
  36.                  String virtualNodeName = str + "&&VN" + String.valueOf(i);  
  37.                  int hash = getHash(virtualNodeName);  
  38.                  System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash);  
  39.                  virtualNodes.put(hash, virtualNodeName);  
  40.              }  
  41.          }  
  42.          System.out.println();  
  43.      }  
  44.        
  45.      //使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别  
  46.      private static int getHash(String str){  
  47.          final int p = 16777619;  
  48.          int hash = (int)2166136261L;  
  49.          for (int i = 0; i < str.length(); i++)  
  50.              hash = (hash ^ str.charAt(i)) * p;  
  51.          hash += hash << 13;  
  52.          hash ^= hash >> 7;  
  53.          hash += hash << 3;  
  54.          hash ^= hash >> 17;  
  55.          hash += hash << 5;  
  56.            
  57.          // 如果算出来的值为负数则取其绝对值  
  58.          if (hash < 0)  
  59.              hash = Math.abs(hash);  
  60.          return hash;  
  61.      }  
  62.        
  63.      //得到应当路由到的结点  
  64.      private static String getServer(String key){  
  65.         //得到该key的hash值  
  66.          int hash = getHash(key);  
  67.          // 得到大于该Hash值的所有Map  
  68.          SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);  
  69.          String virtualNode;  
  70.          if(subMap.isEmpty()){  
  71.             //如果没有比该key的hash值大的,则从第一个node开始  
  72.             Integer i = virtualNodes.firstKey();  
  73.             //返回对应的服务器  
  74.             virtualNode = virtualNodes.get(i);  
  75.          }else{  
  76.             //第一个Key就是顺时针过去离node最近的那个结点  
  77.             Integer i = subMap.firstKey();  
  78.             //返回对应的服务器  
  79.             virtualNode = subMap.get(i);  
  80.          }  
  81.          //virtualNode虚拟节点名称要截取一下  
  82.          if(StringUtils.isNotBlank(virtualNode)){  
  83.              return virtualNode.substring(0, virtualNode.indexOf("&&"));  
  84.          }  
  85.          return null;  
  86.      }  
  87.        
  88.      public static void main(String[] args){  
  89.          String[] keys = {"太阳", "月亮", "星星"};  
  90.          for(int i=0; i<keys.length; i++)  
  91.              System.out.println("[" + keys[i] + "]的hash值为" +  
  92.                      getHash(keys[i]) + ", 被路由到结点[" + getServer(keys[i]) + "]");  
  93.      }  
  94.  }  

执行结果:
虚拟节点[192.168.0.0:111&&VN0]被添加, hash值为1686427075
虚拟节点[192.168.0.0:111&&VN1]被添加, hash值为354859081
虚拟节点[192.168.0.0:111&&VN2]被添加, hash值为1306497370
虚拟节点[192.168.0.0:111&&VN3]被添加, hash值为817889914
虚拟节点[192.168.0.0:111&&VN4]被添加, hash值为396663629
虚拟节点[192.168.0.1:111&&VN0]被添加, hash值为1032739288
虚拟节点[192.168.0.1:111&&VN1]被添加, hash值为707592309
虚拟节点[192.168.0.1:111&&VN2]被添加, hash值为302114528
虚拟节点[192.168.0.1:111&&VN3]被添加, hash值为36526861
虚拟节点[192.168.0.1:111&&VN4]被添加, hash值为848442551
虚拟节点[192.168.0.2:111&&VN0]被添加, hash值为1452694222
虚拟节点[192.168.0.2:111&&VN1]被添加, hash值为2023612840
虚拟节点[192.168.0.2:111&&VN2]被添加, hash值为697907480
虚拟节点[192.168.0.2:111&&VN3]被添加, hash值为790847074
虚拟节点[192.168.0.2:111&&VN4]被添加, hash值为2010506136
虚拟节点[192.168.0.3:111&&VN0]被添加, hash值为891084251
虚拟节点[192.168.0.3:111&&VN1]被添加, hash值为1725031739
虚拟节点[192.168.0.3:111&&VN2]被添加, hash值为1127720370
虚拟节点[192.168.0.3:111&&VN3]被添加, hash值为676720500
虚拟节点[192.168.0.3:111&&VN4]被添加, hash值为2050578780
虚拟节点[192.168.0.4:111&&VN0]被添加, hash值为586921010
虚拟节点[192.168.0.4:111&&VN1]被添加, hash值为184078390
虚拟节点[192.168.0.4:111&&VN2]被添加, hash值为1331645117
虚拟节点[192.168.0.4:111&&VN3]被添加, hash值为918790803
虚拟节点[192.168.0.4:111&&VN4]被添加, hash值为1232193678

[太阳]的hash值为1977106057, 被路由到结点[192.168.0.2:111]
[月亮]的hash值为1132637661, 被路由到结点[192.168.0.4:111]
[星星]的hash值为880019273, 被路由到结点[192.168.0.3:111]

原文:

http://blog.csdn.net/cywosp/article/details/23397179/    一致性哈希算法

http://www.open-open.com/lib/view/open1455374048042.html  一致性哈希算法的Java实现


一致性Hash(Consistent Hashing)原理剖析

引入

在业务开发中,我们常把数据持久化到数据库中。如果需要读取这些数据,除了直接从数据库中读取外,为了减轻数据库的访问压力以及提高访问速度,我们更多地引入缓存来对数据进行存取。读取数据的过程一般为:

这里写图片描述 
图1:加入缓存的数据读取过程

对于分布式缓存,不同机器上存储不同对象的数据。为了实现这些缓存机器的负载均衡,可以使用式子1来定位对象缓存的存储机器:

m = hash(o) mod n ——式子1

其中,o为对象的名称,n为机器的数量,m为机器的编号,hash为一hash函数。图2中的负载均衡器(load balancer)正是使用式子1来将客户端对不同对象的请求分派到不同的机器上执行,例如,对于对象o,经过式子1的计算,得到m的值为3,那么所有对对象o的读取和存储的请求都被发往机器3执行。

这里写图片描述 
图2:如何利用Hash取模实现负载均衡

式子1在大部分时候都可以工作得很好,然而,当机器需要扩容或者机器出现宕机的情况下,事情就比较棘手了。 
当机器扩容,需要增加一台缓存机器时,负载均衡器使用的式子变成:

m = hash(o) mod (n + 1) ——式子2

当机器宕机,机器数量减少一台时,负载均衡器使用的式子变成:

m = hash(o) mod (n - 1) ——式子3

我们以机器扩容的情况为例,说明简单的取模方法会导致什么问题。假设机器由3台变成4台,对象o1由式子1计算得到的m值为2,由式子2计算得到的m值却可能为0,1,2,3(一个 3t + 2的整数对4取模,其值可能为0,1,2,3,读者可以自行验证),大约有75%(3/4)的可能性出现缓存访问不命中的现象。随着机器集群规模的扩大,这个比例线性上升。当99台机器再加入1台机器时,不命中的概率是99%(99/100)。这样的结果显然是不能接受的,因为这会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。

一致性hash算法正是为了解决此类问题的方法,它可以保证当机器增加或者减少时,对缓存访问命中的概率影响减至很小。下面我们来详细说一下一致性hash算法的具体过程。

一致性Hash环

一致性hash算法通过一个叫作一致性hash环的数据结构实现。这个环的起点是0,终点是2^32 - 1,并且起点与终点连接,环的中间的整数按逆时针分布,故这个环的整数分布范围是[0, 2^32-1],如下图3所示:

这里写图片描述 
图3:一致性Hash环

将对象放置到Hash环

假设现在我们有4个对象,分别为o1,o2,o3,o4,使用hash函数计算这4个对象的hash值(范围为0 ~ 2^32-1):

hash(o1) = m1 
hash(o2) = m2 
hash(o3) = m3 
hash(o4) = m4

把m1,m2,m3,m4这4个值放置到hash环上,得到如下图4:

这里写图片描述 
图4:放置了对象的一致性Hash环

将机器放置到Hash环

使用同样的hash函数,我们将机器也放置到hash环上。假设我们有三台缓存机器,分别为 c1,c2,c3,使用hash函数计算这3台机器的hash值:

hash(c1) = t1 
hash(c2) = t2 
hash(c3) = t3

把t1,t2,t3 这3个值放置到hash环上,得到如下图5:

这里写图片描述 
图5:放置了机器的一致性Hash环

为对象选择机器

将对象和机器都放置到同一个hash环后,在hash环上顺时针查找距离这个对象的hash值最近的机器,即是这个对象所属的机器。 
例如,对于对象o2,顺序针找到最近的机器是c1,故机器c1会缓存对象o2。而机器c2则缓存o3,o4,机器c3则缓存对象o1。

这里写图片描述 
图6:在一致性Hash环上为对象选择机器

处理机器增减的情况

对于线上的业务,增加或者减少一台机器的部署是常有的事情。 
例如,增加机器c4的部署并将机器c4加入到hash环的机器c3与c2之间。这时,只有机器c3与c4之间的对象需要重新分配新的机器。对于我们的例子,只有对象o4被重新分配到了c4,其他对象仍在原有机器上。如图7所示:

这里写图片描述 
图7:增加机器后的一致性Hash环的结构

如上文前面所述,使用简单的求模方法,当新添加机器后会导致大部分缓存失效的情况,使用一致性hash算法后这种情况则会得到大大的改善。前面提到3台机器变成4台机器后,缓存命中率只有25%(不命中率75%)。而使用一致性hash算法,理想情况下缓存命中率则有75%,而且,随着机器规模的增加,命中率会进一步提高,99台机器增加一台后,命中率达到99%,这大大减轻了增加缓存机器带来的数据库访问的压力。

再例如,将机器c1下线(当然,也有可能是机器c1宕机),这时,只有原有被分配到机器c1对象需要被重新分配到新的机器。对于我们的例子,只有对象o2被重新分配到机器c3,其他对象仍在原有机器上。如图8所示:

这里写图片描述 
图8:减少机器后的一致性Hash环的结构

虚拟节点

上面提到的过程基本上就是一致性hash的基本原理了,不过还有一个小小的问题。新加入的机器c4只分担了机器c2的负载,机器c1与c3的负载并没有因为机器c4的加入而减少负载压力。如果4台机器的性能是一样的,那么这种结果并不是我们想要的。 
为此,我们引入虚拟节点来解决负载不均衡的问题。 
将每台物理机器虚拟为一组虚拟机器,将虚拟机器放置到hash环上,如果需要确定对象的机器,先确定对象的虚拟机器,再由虚拟机器确定物理机器。 
说得有点复杂,其实过程也很简单。

还是使用上面的例子,假如开始时存在缓存机器c1,c2,c3,对于每个缓存机器,都有3个虚拟节点对应,其一致性hash环结构如图9所示:

这里写图片描述 
图9:机器c1,c2,c3的一致性Hash环结构

假设对于对象o1,其对应的虚拟节点为c11,而虚拟节点c11对象缓存机器c1,故对象o1被分配到机器c1中。

新加入缓存机器c4,其对应的虚拟节点为c41,c42,c43,将这三个虚拟节点添加到hash环中,得到的hash环结构如图10所示:

这里写图片描述 
图10:机器c1,c2,c3,c4的一致性Hash环结构

新加入的缓存机器c4对应一组虚拟节点c41,c42,c43,加入到hash环后,影响的虚拟节点包括c31,c22,c11(顺时针查找到第一个节点),而这3个虚拟节点分别对应机器c3,c2,c1。即新加入的一台机器,同时影响到原有的3台机器。理想情况下,新加入的机器平等地分担了原有机器的负载,这正是虚拟节点带来的好处。而且新加入机器c4后,只影响25%(1/4)对象分配,也就是说,命中率仍然有75%,这跟没有使用虚拟节点的一致性hash算法得到的结果是相同的。

总结

一致性hash算法解决了分布式环境下机器增加或者减少时,简单的取模运算无法获取较高命中率的问题。通过虚拟节点的使用,一致性hash算法可以均匀分担机器的负载,使得这一算法更具现实的意义。正因如此,一致性hash算法被广泛应用于分布式系统中。

参考资料

    1. https://en.wikipedia.org/wiki/Consistent_hashing

    2. https://www.codeproject.com/articles/56138/consistent-hashing

    3. 《大型网站技术架构——核心原理与安全分析》,李智慧著,电子工业出版社

一致性Hash算法背景

  一致性哈希算法在1997年由麻省理工学院的Karger等人在解决分布式Cache中提出的,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得DHT可以在P2P环境中真正得到应用。

  但现在一致性hash算法在分布式系统中也得到了广泛应用,研究过memcached缓存数据库的人都知道,memcached服务器端本身不提供分布式cache的一致性,而是由客户端来提供,具体在计算一致性hash时采用如下步骤:

  1. 首先求出memcached服务器(节点)的哈希值,并将其配置到0~232的圆(continuum)上。
  2. 然后采用同样的方法求出存储数据的键的哈希值,并映射到相同的圆上。
  3. 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器上。如果超过232仍然找不到服务器,就会保存到第一台memcached服务器上。

  从上图的状态中添加一台memcached服务器。余数分布式算法由于保存键的服务器会发生巨大变化而影响缓存的命中率,但Consistent Hashing中,只有在园(continuum)上增加服务器的地点逆时针方向的第一台服务器上的键会受到影响,如下图所示:

 

一致性Hash性质

  考虑到分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来,如何保证当系统的节点数目发生变化时仍然能够对外提供良好的服务,这是值得考虑的,尤其实在设计分布式缓存系统时,如果某台服务器失效,对于整个系统来说如果不采用合适的算法来保证一致性,那么缓存于系统中的所有数据都可能会失效(即由于系统节点数目变少,客户端在请求某一对象时需要重新计算其hash值(通常与系统中的节点数目有关),由于hash值已经改变,所以很可能找不到保存该对象的服务器节点),因此一致性hash就显得至关重要,良好的分布式cahce系统中的一致性hash算法应该满足以下几个方面:

  • 平衡性(Balance)

平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。

  • 单调性(Monotonicity)

单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。简单的哈希算法往往不能满足单调性的要求,如最简单的线性哈希:x = (ax + b) mod (P),在上式中,P表示全部缓冲的大小。不难看出,当缓冲大小发生变化时(从P1到P2),原来所有的哈希结果均会发生变化,从而不满足单调性的要求。哈希结果的变化意味着当缓冲空间发生变化时,所有的映射关系需要在系统内全部更新。而在P2P系统内,缓冲的变化等价于Peer加入或退出系统,这一情况在P2P系统中会频繁发生,因此会带来极大计算和传输负荷。单调性就是要求哈希算法能够应对这种情况。

  • 分散性(Spread)

在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。

  • 负载(Load)

负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

  • 平滑性(Smoothness)

平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。

原理

基本概念

  一致性哈希算法(Consistent Hashing)最早在论文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出。简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希空间环如下:

  整个空间按顺时针方向组织。0和232-1在零点中方向重合。

  下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下:

  

接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。

  例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:

根据一致性哈希算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。

下面分析一致性哈希算法的容错性和可扩展性。现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。

下面考虑另外一种情况,如果在系统中增加一台服务器Node X,如下图所示:

此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X 。一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。

综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

另外,一致性哈希算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜问题。例如系统中只有两台服务器,其环分布如下,

此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:

同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三个虚拟节点的数据均定位到Node A上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

一致性哈希算法与Java实现

========================================================

一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将数据存储到具体的节点上,
如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,key是数据的key,N是机器节点数,
如果有一个机器加入或退出这个集群,则所有的数据映射都无效了。

一致性哈希算法解决了普通余数Hash算法伸缩性差的问题,可以保证在上线、下线服务器的情况下尽量有多的请求命中原来路由到的服务器。

一、一致性哈希算法

1、原理

(1)环形Hash空间

按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。

现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图

(2)把数据通过一定的hash算法处理后映射到环上

现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;


(3)将机器通过hash算法映射到环上

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中

(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。

假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;


通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。

在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

2、机器的删除与添加
普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会造成大量的对象存储位置失效。下面来分析一下一致性哈希算法是如何处理的。

(1)节点(机器)的删除
以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:


(2)节点(机器)的添加 
如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:


通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持着原有的存储位置。
通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。

3、平衡性--虚拟节点

根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,

因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。

hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就造成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。

——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一个实际节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。

以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:


根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:


“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2

二、一致性hash算法的Java实现

1、不带虚拟节点的

[java]  view plain  copy
 
  1. package hash;  
  2.   
  3. import java.util.SortedMap;  
  4. import java.util.TreeMap;  
  5.   
  6. /** 
  7.  * 不带虚拟节点的一致性Hash算法 
  8.  */  
  9. public class ConsistentHashingWithoutVirtualNode {  
  10.   
  11.     //待添加入Hash环的服务器列表  
  12.     private static String[] servers = { "192.168.0.0:111", "192.168.0.1:111",  
  13.             "192.168.0.2:111", "192.168.0.3:111", "192.168.0.4:111" };  
  14.   
  15.     //key表示服务器的hash值,value表示服务器  
  16.     private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();  
  17.   
  18.     //程序初始化,将所有的服务器放入sortedMap中  
  19.     static {  
  20.         for (int i=0; i<servers.length; i++) {  
  21.             int hash = getHash(servers[i]);  
  22.             System.out.println("[" + servers[i] + "]加入集合中, 其Hash值为" + hash);  
  23.             sortedMap.put(hash, servers[i]);  
  24.         }  
  25.         System.out.println();  
  26.     }  
  27.   
  28.     //得到应当路由到的结点  
  29.     private static String getServer(String key) {  
  30.         //得到该key的hash值  
  31.         int hash = getHash(key);  
  32.         //得到大于该Hash值的所有Map  
  33.         SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);  
  34.         if(subMap.isEmpty()){  
  35.             //如果没有比该key的hash值大的,则从第一个node开始  
  36.             Integer i = sortedMap.firstKey();  
  37.             //返回对应的服务器  
  38.             return sortedMap.get(i);  
  39.         }else{  
  40.             //第一个Key就是顺时针过去离node最近的那个结点  
  41.             Integer i = subMap.firstKey();  
  42.             //返回对应的服务器  
  43.             return subMap.get(i);  
  44.         }  
  45.     }  
  46.       
  47.     //使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别  
  48.     private static int getHash(String str) {  
  49.         final int p = 16777619;  
  50.         int hash = (int) 2166136261L;  
  51.         for (int i = 0; i < str.length(); i++)  
  52.             hash = (hash ^ str.charAt(i)) * p;  
  53.         hash += hash << 13;  
  54.         hash ^= hash >> 7;  
  55.         hash += hash << 3;  
  56.         hash ^= hash >> 17;  
  57.         hash += hash << 5;  
  58.   
  59.         // 如果算出来的值为负数则取其绝对值  
  60.         if (hash < 0)  
  61.             hash = Math.abs(hash);  
  62.         return hash;  
  63.         }  
  64.   
  65.     public static void main(String[] args) {  
  66.         String[] keys = {"太阳", "月亮", "星星"};  
  67.         for(int i=0; i<keys.length; i++)  
  68.             System.out.println("[" + keys[i] + "]的hash值为" + getHash(keys[i])  
  69.                     + ", 被路由到结点[" + getServer(keys[i]) + "]");  
  70.     }  
  71. }  

执行结果:
[192.168.0.0:111]加入集合中, 其Hash值为575774686
[192.168.0.1:111]加入集合中, 其Hash值为8518713
[192.168.0.2:111]加入集合中, 其Hash值为1361847097
[192.168.0.3:111]加入集合中, 其Hash值为1171828661
[192.168.0.4:111]加入集合中, 其Hash值为1764547046

[太阳]的hash值为1977106057, 被路由到结点[192.168.0.1:111]
[月亮]的hash值为1132637661, 被路由到结点[192.168.0.3:111]
[星星]的hash值为880019273, 被路由到结点[192.168.0.3:111]

2、带虚拟节点的

[java]  view plain  copy
 
  1. package hash;  
  2.   
  3. import java.util.LinkedList;  
  4. import java.util.List;  
  5. import java.util.SortedMap;  
  6. import java.util.TreeMap;  
  7.   
  8. import org.apache.commons.lang.StringUtils;  
  9.   
  10. /** 
  11.   * 带虚拟节点的一致性Hash算法 
  12.   */  
  13.  public class ConsistentHashingWithoutVirtualNode {  
  14.   
  15.      //待添加入Hash环的服务器列表  
  16.      private static String[] servers = {"192.168.0.0:111", "192.168.0.1:111", "192.168.0.2:111",  
  17.              "192.168.0.3:111", "192.168.0.4:111"};  
  18.        
  19.      //真实结点列表,考虑到服务器上线、下线的场景,即添加、删除的场景会比较频繁,这里使用LinkedList会更好  
  20.      private static List<String> realNodes = new LinkedList<String>();  
  21.        
  22.      //虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称  
  23.      private static SortedMap<Integer, String> virtualNodes = new TreeMap<Integer, String>();  
  24.                
  25.      //虚拟节点的数目,这里写死,为了演示需要,一个真实结点对应5个虚拟节点  
  26.      private static final int VIRTUAL_NODES = 5;  
  27.        
  28.      static{  
  29.          //先把原始的服务器添加到真实结点列表中  
  30.          for(int i=0; i<servers.length; i++)  
  31.              realNodes.add(servers[i]);  
  32.            
  33.          //再添加虚拟节点,遍历LinkedList使用foreach循环效率会比较高  
  34.          for (String str : realNodes){  
  35.              for(int i=0; i<VIRTUAL_NODES; i++){  
  36.                  String virtualNodeName = str + "&&VN" + String.valueOf(i);  
  37.                  int hash = getHash(virtualNodeName);  
  38.                  System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash);  
  39.                  virtualNodes.put(hash, virtualNodeName);  
  40.              }  
  41.          }  
  42.          System.out.println();  
  43.      }  
  44.        
  45.      //使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别  
  46.      private static int getHash(String str){  
  47.          final int p = 16777619;  
  48.          int hash = (int)2166136261L;  
  49.          for (int i = 0; i < str.length(); i++)  
  50.              hash = (hash ^ str.charAt(i)) * p;  
  51.          hash += hash << 13;  
  52.          hash ^= hash >> 7;  
  53.          hash += hash << 3;  
  54.          hash ^= hash >> 17;  
  55.          hash += hash << 5;  
  56.            
  57.          // 如果算出来的值为负数则取其绝对值  
  58.          if (hash < 0)  
  59.              hash = Math.abs(hash);  
  60.          return hash;  
  61.      }  
  62.        
  63.      //得到应当路由到的结点  
  64.      private static String getServer(String key){  
  65.         //得到该key的hash值  
  66.          int hash = getHash(key);  
  67.          // 得到大于该Hash值的所有Map  
  68.          SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);  
  69.          String virtualNode;  
  70.          if(subMap.isEmpty()){  
  71.             //如果没有比该key的hash值大的,则从第一个node开始  
  72.             Integer i = virtualNodes.firstKey();  
  73.             //返回对应的服务器  
  74.             virtualNode = virtualNodes.get(i);  
  75.          }else{  
  76.             //第一个Key就是顺时针过去离node最近的那个结点  
  77.             Integer i = subMap.firstKey();  
  78.             //返回对应的服务器  
  79.             virtualNode = subMap.get(i);  
  80.          }  
  81.          //virtualNode虚拟节点名称要截取一下  
  82.          if(StringUtils.isNotBlank(virtualNode)){  
  83.              return virtualNode.substring(0, virtualNode.indexOf("&&"));  
  84.          }  
  85.          return null;  
  86.      }  
  87.        
  88.      public static void main(String[] args){  
  89.          String[] keys = {"太阳", "月亮", "星星"};  
  90.          for(int i=0; i<keys.length; i++)  
  91.              System.out.println("[" + keys[i] + "]的hash值为" +  
  92.                      getHash(keys[i]) + ", 被路由到结点[" + getServer(keys[i]) + "]");  
  93.      }  
  94.  }  

执行结果:
虚拟节点[192.168.0.0:111&&VN0]被添加, hash值为1686427075
虚拟节点[192.168.0.0:111&&VN1]被添加, hash值为354859081
虚拟节点[192.168.0.0:111&&VN2]被添加, hash值为1306497370
虚拟节点[192.168.0.0:111&&VN3]被添加, hash值为817889914
虚拟节点[192.168.0.0:111&&VN4]被添加, hash值为396663629
虚拟节点[192.168.0.1:111&&VN0]被添加, hash值为1032739288
虚拟节点[192.168.0.1:111&&VN1]被添加, hash值为707592309
虚拟节点[192.168.0.1:111&&VN2]被添加, hash值为302114528
虚拟节点[192.168.0.1:111&&VN3]被添加, hash值为36526861
虚拟节点[192.168.0.1:111&&VN4]被添加, hash值为848442551
虚拟节点[192.168.0.2:111&&VN0]被添加, hash值为1452694222
虚拟节点[192.168.0.2:111&&VN1]被添加, hash值为2023612840
虚拟节点[192.168.0.2:111&&VN2]被添加, hash值为697907480
虚拟节点[192.168.0.2:111&&VN3]被添加, hash值为790847074
虚拟节点[192.168.0.2:111&&VN4]被添加, hash值为2010506136
虚拟节点[192.168.0.3:111&&VN0]被添加, hash值为891084251
虚拟节点[192.168.0.3:111&&VN1]被添加, hash值为1725031739
虚拟节点[192.168.0.3:111&&VN2]被添加, hash值为1127720370
虚拟节点[192.168.0.3:111&&VN3]被添加, hash值为676720500
虚拟节点[192.168.0.3:111&&VN4]被添加, hash值为2050578780
虚拟节点[192.168.0.4:111&&VN0]被添加, hash值为586921010
虚拟节点[192.168.0.4:111&&VN1]被添加, hash值为184078390
虚拟节点[192.168.0.4:111&&VN2]被添加, hash值为1331645117
虚拟节点[192.168.0.4:111&&VN3]被添加, hash值为918790803
虚拟节点[192.168.0.4:111&&VN4]被添加, hash值为1232193678

[太阳]的hash值为1977106057, 被路由到结点[192.168.0.2:111]
[月亮]的hash值为1132637661, 被路由到结点[192.168.0.4:111]
[星星]的hash值为880019273, 被路由到结点[192.168.0.3:111]

原文:

http://blog.csdn.net/cywosp/article/details/23397179/    一致性哈希算法

http://www.open-open.com/lib/view/open1455374048042.html  一致性哈希算法的Java实现


一致性Hash(Consistent Hashing)原理剖析

引入

在业务开发中,我们常把数据持久化到数据库中。如果需要读取这些数据,除了直接从数据库中读取外,为了减轻数据库的访问压力以及提高访问速度,我们更多地引入缓存来对数据进行存取。读取数据的过程一般为:

这里写图片描述 
图1:加入缓存的数据读取过程

对于分布式缓存,不同机器上存储不同对象的数据。为了实现这些缓存机器的负载均衡,可以使用式子1来定位对象缓存的存储机器:

m = hash(o) mod n ——式子1

其中,o为对象的名称,n为机器的数量,m为机器的编号,hash为一hash函数。图2中的负载均衡器(load balancer)正是使用式子1来将客户端对不同对象的请求分派到不同的机器上执行,例如,对于对象o,经过式子1的计算,得到m的值为3,那么所有对对象o的读取和存储的请求都被发往机器3执行。

这里写图片描述 
图2:如何利用Hash取模实现负载均衡

式子1在大部分时候都可以工作得很好,然而,当机器需要扩容或者机器出现宕机的情况下,事情就比较棘手了。 
当机器扩容,需要增加一台缓存机器时,负载均衡器使用的式子变成:

m = hash(o) mod (n + 1) ——式子2

当机器宕机,机器数量减少一台时,负载均衡器使用的式子变成:

m = hash(o) mod (n - 1) ——式子3

我们以机器扩容的情况为例,说明简单的取模方法会导致什么问题。假设机器由3台变成4台,对象o1由式子1计算得到的m值为2,由式子2计算得到的m值却可能为0,1,2,3(一个 3t + 2的整数对4取模,其值可能为0,1,2,3,读者可以自行验证),大约有75%(3/4)的可能性出现缓存访问不命中的现象。随着机器集群规模的扩大,这个比例线性上升。当99台机器再加入1台机器时,不命中的概率是99%(99/100)。这样的结果显然是不能接受的,因为这会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。

一致性hash算法正是为了解决此类问题的方法,它可以保证当机器增加或者减少时,对缓存访问命中的概率影响减至很小。下面我们来详细说一下一致性hash算法的具体过程。

一致性Hash环

一致性hash算法通过一个叫作一致性hash环的数据结构实现。这个环的起点是0,终点是2^32 - 1,并且起点与终点连接,环的中间的整数按逆时针分布,故这个环的整数分布范围是[0, 2^32-1],如下图3所示:

这里写图片描述 
图3:一致性Hash环

将对象放置到Hash环

假设现在我们有4个对象,分别为o1,o2,o3,o4,使用hash函数计算这4个对象的hash值(范围为0 ~ 2^32-1):

hash(o1) = m1 
hash(o2) = m2 
hash(o3) = m3 
hash(o4) = m4

把m1,m2,m3,m4这4个值放置到hash环上,得到如下图4:

这里写图片描述 
图4:放置了对象的一致性Hash环

将机器放置到Hash环

使用同样的hash函数,我们将机器也放置到hash环上。假设我们有三台缓存机器,分别为 c1,c2,c3,使用hash函数计算这3台机器的hash值:

hash(c1) = t1 
hash(c2) = t2 
hash(c3) = t3

把t1,t2,t3 这3个值放置到hash环上,得到如下图5:

这里写图片描述 
图5:放置了机器的一致性Hash环

为对象选择机器

将对象和机器都放置到同一个hash环后,在hash环上顺时针查找距离这个对象的hash值最近的机器,即是这个对象所属的机器。 
例如,对于对象o2,顺序针找到最近的机器是c1,故机器c1会缓存对象o2。而机器c2则缓存o3,o4,机器c3则缓存对象o1。

这里写图片描述 
图6:在一致性Hash环上为对象选择机器

处理机器增减的情况

对于线上的业务,增加或者减少一台机器的部署是常有的事情。 
例如,增加机器c4的部署并将机器c4加入到hash环的机器c3与c2之间。这时,只有机器c3与c4之间的对象需要重新分配新的机器。对于我们的例子,只有对象o4被重新分配到了c4,其他对象仍在原有机器上。如图7所示:

这里写图片描述 
图7:增加机器后的一致性Hash环的结构

如上文前面所述,使用简单的求模方法,当新添加机器后会导致大部分缓存失效的情况,使用一致性hash算法后这种情况则会得到大大的改善。前面提到3台机器变成4台机器后,缓存命中率只有25%(不命中率75%)。而使用一致性hash算法,理想情况下缓存命中率则有75%,而且,随着机器规模的增加,命中率会进一步提高,99台机器增加一台后,命中率达到99%,这大大减轻了增加缓存机器带来的数据库访问的压力。

再例如,将机器c1下线(当然,也有可能是机器c1宕机),这时,只有原有被分配到机器c1对象需要被重新分配到新的机器。对于我们的例子,只有对象o2被重新分配到机器c3,其他对象仍在原有机器上。如图8所示:

这里写图片描述 
图8:减少机器后的一致性Hash环的结构

虚拟节点

上面提到的过程基本上就是一致性hash的基本原理了,不过还有一个小小的问题。新加入的机器c4只分担了机器c2的负载,机器c1与c3的负载并没有因为机器c4的加入而减少负载压力。如果4台机器的性能是一样的,那么这种结果并不是我们想要的。 
为此,我们引入虚拟节点来解决负载不均衡的问题。 
将每台物理机器虚拟为一组虚拟机器,将虚拟机器放置到hash环上,如果需要确定对象的机器,先确定对象的虚拟机器,再由虚拟机器确定物理机器。 
说得有点复杂,其实过程也很简单。

还是使用上面的例子,假如开始时存在缓存机器c1,c2,c3,对于每个缓存机器,都有3个虚拟节点对应,其一致性hash环结构如图9所示:

这里写图片描述 
图9:机器c1,c2,c3的一致性Hash环结构

假设对于对象o1,其对应的虚拟节点为c11,而虚拟节点c11对象缓存机器c1,故对象o1被分配到机器c1中。

新加入缓存机器c4,其对应的虚拟节点为c41,c42,c43,将这三个虚拟节点添加到hash环中,得到的hash环结构如图10所示:

这里写图片描述 
图10:机器c1,c2,c3,c4的一致性Hash环结构

新加入的缓存机器c4对应一组虚拟节点c41,c42,c43,加入到hash环后,影响的虚拟节点包括c31,c22,c11(顺时针查找到第一个节点),而这3个虚拟节点分别对应机器c3,c2,c1。即新加入的一台机器,同时影响到原有的3台机器。理想情况下,新加入的机器平等地分担了原有机器的负载,这正是虚拟节点带来的好处。而且新加入机器c4后,只影响25%(1/4)对象分配,也就是说,命中率仍然有75%,这跟没有使用虚拟节点的一致性hash算法得到的结果是相同的。

总结

一致性hash算法解决了分布式环境下机器增加或者减少时,简单的取模运算无法获取较高命中率的问题。通过虚拟节点的使用,一致性hash算法可以均匀分担机器的负载,使得这一算法更具现实的意义。正因如此,一致性hash算法被广泛应用于分布式系统中。

参考资料

    1. https://en.wikipedia.org/wiki/Consistent_hashing

    2. https://www.codeproject.com/articles/56138/consistent-hashing

    3. 《大型网站技术架构——核心原理与安全分析》,李智慧著,电子工业出版社

猜你喜欢

转载自www.cnblogs.com/xuwc/p/9027070.html