主题:Ketama一致性Hash算法(含Java代码)
[下面以Memcached的分布式问题为讨论点,但将Memcachedserver抽象为节点(Node)]
引文中描述的一致性Hash算法有个潜在的问题是:
将节点hash后会不均匀地分布在环上,这样大量key在寻找节点时,会存在key命中各个节点的概率差别较大,无法实现有效的负载均衡。
如有三个节点Node1,Node2,Node3,分布在环上时三个节点挨的很近,落在环上的key寻找节点时,大量key顺时针总是分配给Node2,而其它两个节点被找到的概率都会很小。
这种问题的解决方案可以有:
改善Hash算法,均匀分配各节点到环上;[引文]使用虚拟节点的思想,为每个物理节点(服务器)在圆上分配100~200个点。这样就能抑制分布不均匀,最大限度地减小服务器增减时的缓存重新分布。用户数据映射在虚拟节点上,就表示用户数据真正存储位置是在该虚拟节点代表的实际物理服务器上。
在查看SpyMemcachedclient时,发现它采用一种称为Ketama的Hash算法,以虚拟节点的思想,解决Memcached的分布式问题。
对Ketama的介绍
引用
Ketamaisanimplementationofaconsistenthashingalgorithm,meaningyoucanaddorremoveserversfromthememcachedpoolwithoutcausingacompleteremapofallkeys.
Here’showitworks:
*Takeyourlistofservers(eg:1.2.3.4:11211,5.6.7.8:11211,9.8.7.6:11211)
*Hasheachserverstringtoseveral(100-200)unsignedints
*Conceptually,thesenumbersareplacedonacirclecalledthecontinuum.(imagineaclockfacethatgoesfrom0to2^32)
*Eachnumberlinkstotheserveritwashashedfrom,soserversappearatseveralpointsonthecontinuum,byeachofthenumberstheyhashedto.
*Tomapakey->server,hashyourkeytoasingleunsignedint,andfindthenextbiggestnumberonthecontinuum.Theserverlinkedtothatnumberisthecorrectserverforthatkey.
*Ifyouhashyourkeytoavaluenear2^32andtherearenopointsonthecontinuumgreaterthanyourhash,returnthefirstserverinthecontinuum.
Ifyouthenaddorremoveaserverfromthelist,onlyasmallproportionofkeysendupmappingtodifferentservers.
下面以SpyMemcached中的代码为例来说明这种算法的使用
该client采用TreeMap存储所有节点,模拟一个环形的逻辑关系。在这个环中,节点之前是存在顺序关系的,所以TreeMap的key必须实现Comparator接口。
那节点是怎样放入这个环中的呢?
Java代码收藏代码
//对所有节点,生成nCopies个虚拟结点 for(Node node : nodes) { //每四个虚拟结点为一组,为什么这样?下面会说到 for(int i=0; i<nCopies / 4; i++) { //getKeyForNode方法为这组虚拟结点得到惟一名称 byte[] digest=HashAlgorithm.computeMd5(getKeyForNode(node, i)); /** Md5是一个16字节长度的数组,将16字节的数组每四个字节一组, 分别对应一个虚拟结点,这就是为什么上面把虚拟结点四个划分一组的原因*/ for(int h=0;h<4;h++) { //对于每四个字节,组成一个long值数值,做为这个虚拟节点的在环中的惟一key Long k = ((long)(digest[3+h*4]&0xFF) << 24) | ((long)(digest[2+h*4]&0xFF) << 16) | ((long)(digest[1+h*4]&0xFF) << 8) | (digest[h*4]&0xFF); allNodes.put(k, node); } } }
上面的流程大概可以这样归纳:四个虚拟结点为一组,以getKeyForNode方法得到这组虚拟节点的name,Md5编码后,每个虚拟结点对应Md5码16个字节中的4个,组成一个long型数值,做为这个虚拟结点在环中的惟一key。第12行k为什么是Long型的呢?呵呵,就是因为Long型实现了Comparator接口。
处理完正式结点在环上的分布后,可以开始key在环上寻找节点的游戏了。
对于每个key还是得完成上面的步骤:计算出Md5,根据Md5的字节数组,通过KemataHash算法得到key在这个环中的位置。
Java代码收藏代码
final Node rv; byte[] digest = hashAlg.computeMd5(keyValue); Long key = hashAlg.hash(digest, 0); //如果找到这个节点,直接取节点,返回 if(!ketamaNodes.containsKey(key)) { //得到大于当前key的那个子Map,然后从中取出第一个key,就是大于且离它最近的那个key SortedMap<Long, Node> tailMap=ketamaNodes.tailMap(key); if(tailMap.isEmpty()) { key=ketamaNodes.firstKey(); } else { key=tailMap.firstKey(); } //在JDK1.6中,ceilingKey方法可以返回大于且离它最近的那个key //For JDK1.6 version // key = ketamaNodes.ceilingKey(key); // if (key == null) { // key = ketamaNodes.firstKey(); // } } rv=allNodes.get(key);
引文中已详细描述过这种取节点逻辑:在环上顺时针查找,如果找到某个节点,就返回那个节点;如果没有找到,则取整个环的第一个节点。
测试结果
测试代码是自己整理的,主体方法没有变
分布平均性测试:测试随机生成的众多key是否会平均分布到各个结点上
测试结果如下:
Java代码收藏代码
Nodescount:5,Keyscount:100000,Normalpercent:20.0%
--------------------boundary----------------------
Nodename:node1-Times:20821-Percent:20.821001%
Nodename:node3-Times:19018-Percent:19.018%
Nodename:node5-Times:19726-Percent:19.726%
Nodename:node2-Times:19919-Percent:19.919%
Nodename:node4-Times:20516-Percent:20.516%
最上面一行是参数说明,节点数目,总共有多少key,每个节点应该分配key的比例是多少。下面是每个结点分配到key的数目和比例。
多次测试后发现,这个Hash算法的节点分布还是不错的,都在标准比例左右徘徊,是个合适的负载均衡算法。
节点增删测试:在环上插入N个结点,每个节点nCopies个虚拟结点。随机生成众多key,在增删节点时,测试同一个key选择相同节点的概率
测试如果如下:
Java代码收藏代码
Normalcase:nodescount:50
Addedcase:nodescount:51
Reducedcase:nodescount:49
------------boundary-------------
Samepercentinaddedcase:93.765%
Samepercentinreducedcase:93.845%
上面三行分别是正常情况,节点增加,节点删除情况下的节点数目。下面两行表示在节点增加和删除情况下,同一个key分配在相同节点上的比例(命中率)。
多次测试后发现,命中率与结点数目和增减的节点数量有关。同样增删结点数目情况下,结点多时命中率高。同样节点数目,增删结点越少,命中率越高。这些都与实际情况相符。
附件为Ketama算法的Java代码及测试代码