一 致性哈希算法(Consistent Hashing Algorithm)是一種分布式算法,常用于負載均衡。Memcached client也選擇這種算法,解決將key-value均勻分配到眾多Memcached server上的問題。它可以取代傳統(tǒng)的取模操作,解決了取模操作無法應(yīng)對增刪Memcached Server的問題(增刪server會導致同一個key,在get操作時分配不到數(shù)據(jù)真正存儲的server,命中率會急劇下降),詳細的介紹在這篇帖 子中http://www.iteye.com/topic/611976(后文指代這篇文章的地方均稱為引文)。
[下面以Memcached的分布式問題為討論點,但將Memcached server抽象為節(jié)點(Node)]
引文中描述的一致性Hash算法有個潛在的問題是:
將節(jié)點hash后會不均勻地分布在環(huán)上,這樣大量key在尋找節(jié)點時,會存在key命中各個節(jié)點的概率差別較大,無法實現(xiàn)有效的負載均衡。
如有三個節(jié)點Node1,Node2,Node3,分布在環(huán)上時三個節(jié)點挨的很近,落在環(huán)上的key尋找節(jié)點時,大量key順時針總是分配給Node2,而其它兩個節(jié)點被找到的概率都會很小。
這種問題的解決方案可以有:
改善Hash算法,均勻分配各節(jié)點到環(huán)上;[引文]使用虛擬節(jié)點的思想,為每個物理節(jié)點(服務(wù)器)在圓上分配100~200個點。這樣就能抑制分布不均 勻,最大限度地減小服務(wù)器增減時的緩存重新分布。用戶數(shù)據(jù)映射在虛擬節(jié)點上,就表示用戶數(shù)據(jù)真正存儲位置是在該虛擬節(jié)點代表的實際物理服務(wù)器上。
在查看Spy Memcached client時,發(fā)現(xiàn)它采用一種稱為Ketama的Hash算法,以虛擬節(jié)點的思想,解決Memcached的分布式問題。
對Ketama的介紹
Ketama is an implementation of a consistent hashing algorithm, meaning you can add or remove servers from the memcached pool without causing a complete remap of all keys.
Here’s how it works:
* Take your list of servers (eg: 1.2.3.4:11211, 5.6.7.8:11211, 9.8.7.6:11211)
* Hash each server string to several (100-200) unsigned ints
* Conceptually, these numbers are placed on a circle called the continuum. (imagine a clock face that goes from 0 to 2^32)
* Each number links to the server it was hashed from, so servers appear at several points on the continuum, by each of the numbers they hashed to.
* To map a key->server, hash your key to a single unsigned int, and find the next biggest number on the continuum. The server linked to that number is the correct server for that key.
* If you hash your key to a value near 2^32 and there are no points on the continuum greater than your hash, return the first server in the continuum.
If you then add or remove a server from the list, only a small proportion of keys end up mapping to different servers.
下面以Spy Memcached中的代碼為例來說明這種算法的使用
該client采用TreeMap存儲所有節(jié)點,模擬一個環(huán)形的邏輯關(guān)系。在這個環(huán)中,節(jié)點之前是存在順序關(guān)系的,所以TreeMap的key必須實現(xiàn)Comparator接口。
那節(jié)點是怎樣放入這個環(huán)中的呢?
///////////////////////////////////////////////////////////////////////
//對所有節(jié)點,生成nCopies個虛擬結(jié)點
for(Node node : nodes) {
//每四個虛擬結(jié)點為一組,為什么這樣?下面會說到
for(int i=0; i<nCopies / 4; i++) {
//getKeyForNode方法為這組虛擬結(jié)點得到惟一名稱
byte[] digest=HashAlgorithm.computeMd5(getKeyForNode(node, i));
/** Md5是一個16字節(jié)長度的數(shù)組,將16字節(jié)的數(shù)組每四個字節(jié)一組,
分別對應(yīng)一個虛擬結(jié)點,這就是為什么上面把虛擬結(jié)點四個劃分一組的原因*/
for(int h=0;h<4;h++) {
//對于每四個字節(jié),組成一個long值數(shù)值,做為這個虛擬節(jié)點的在環(huán)中的惟一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);
}
}
}
///////////////////////////////////////////////////////////////////////
上 面的流程大概可以這樣歸納:四個虛擬結(jié)點為一組,以getKeyForNode方法得到這組虛擬節(jié)點的name,Md5編碼后,每個虛擬結(jié)點對應(yīng)Md5碼 16個字節(jié)中的4個,組成一個long型數(shù)值,做為這個虛擬結(jié)點在環(huán)中的惟一key。第12行k為什么是Long型的呢?呵呵,就是因為Long型實現(xiàn)了 Comparator接口。
處理完正式結(jié)點在環(huán)上的分布后,可以開始key在環(huán)上尋找節(jié)點的游戲了。
對于每個key還是得完成上面的步驟:計算出Md5,根據(jù)Md5的字節(jié)數(shù)組,通過Kemata Hash算法得到key在這個環(huán)中的位置。
///////////////////////////////////////////////////////////////////////
final Node rv;
byte[] digest = hashAlg.computeMd5(keyValue);
Long key = hashAlg.hash(digest, 0);
//如果找到這個節(jié)點,直接取節(jié)點,返回
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);
///////////////////////////////////////////////////////////////////////
引文中已詳細描述過這種取節(jié)點邏輯:在環(huán)上順時針查找,如果找到某個節(jié)點,就返回那個節(jié)點;如果沒有找到,則取整個環(huán)的第一個節(jié)點。
測試結(jié)果
測試代碼是自己整理的,主體方法沒有變
分布平均性測試:測試隨機生成的眾多key是否會平均分布到各個結(jié)點上
測試結(jié)果如下:
1.Nodes count : 5, Keys count : 100000, Normal percent : 20.0%
2.-------------------- boundary ----------------------
3.Node name :node1 - Times : 20821 - Percent : 20.821001%
4.Node name :node3 - Times : 19018 - Percent : 19.018%
5.Node name :node5 - Times : 19726 - Percent : 19.726%
6.Node name :node2 - Times : 19919 - Percent : 19.919%
7.Node name :node4 - Times : 20516 - Percent : 20.516%
最上面一行是參數(shù)說明,節(jié)點數(shù)目,總共有多少key,每個節(jié)點應(yīng)該分配key的比例是多少。下面是每個結(jié)點分配到key的數(shù)目和比例。
多次測試后發(fā)現(xiàn),這個Hash算法的節(jié)點分布還是不錯的,都在標準比例左右徘徊,是個合適的負載均衡算法。
節(jié)點增刪測試:在環(huán)上插入N個結(jié)點,每個節(jié)點nCopies個虛擬結(jié)點。隨機生成眾多key,在增刪節(jié)點時,測試同一個key選擇相同節(jié)點的概率
測試如果如下:
1.Normal case : nodes count : 50
2.Added case : nodes count : 51
3.Reduced case : nodes count : 49
4.------------ boundary -------------
5.Same percent in added case : 93.765%
6.Same percent in reduced case : 93.845%
上面三行分別是正常情況,節(jié)點增加,節(jié)點刪除情況下的節(jié)點數(shù)目。下面兩行表示在節(jié)點增加和刪除情況下,同一個key分配在相同節(jié)點上的比例(命中率)。
多次測試后發(fā)現(xiàn),命中率與結(jié)點數(shù)目和增減的節(jié)點數(shù)量有關(guān)。同樣增刪結(jié)點數(shù)目情況下,結(jié)點多時命中率高。同樣節(jié)點數(shù)目,增刪結(jié)點越少,命中率越高。這些都與實際情況相符。
附件為Ketama算法的Java代碼及測試代碼
"Ketama_Hashing_Algorithm.rar" http://vdisk.weibo.com/s/x4gf