一致性hash算法在memcached中的使用

MiraBergstr 8年前發布 | 35K 次閱讀 緩存服務器 memcached

來自: http://blog.csdn.net//chenleixing/article/details/47604593


一、概述

  1、我們的memcache客戶端(這里我看的spymemcache的源碼),使用了一致性hash算法ketama進行數據存儲節點的選擇。與常規的hash算法思路不同,只是對我們要存儲數據的key進行hash計算,分配到不同節點存儲。一致性hash算法是對我們要存儲數據的服務器進行hash計算,進而確認每個key的存儲位置。

 2、常規hash算法的應用以及其弊端

    最常規的方式莫過于hash取模的方式。比如集群中可用機器適量為N,那么key值為K的的數據請求很簡單的應該路由到hash(K) mod N對應的機器。的確,這種結構是簡單的,也是實用的。但是在一些高速發展的web系統中,這樣的解決方案仍有些缺陷。隨著系統訪問壓力的增長,緩存系統不得不通過增加機器節點的方式提高集群的相應速度和數據承載量。增加機器意味著按照hash取模的方式,在增加機器節點的這一時刻,大量的緩存命不中,緩存數據需要重新建立,甚至是進行整體的緩存數據遷移,瞬間會給DB帶來極高的系統負載,設置導致DB服務器宕機。

  3、設計分布式cache系統時,一致性hash算法可以幫我們解決哪些問題?

   分布式緩存設計核心點:在設計分布式cache系統的時候,我們需要讓key的分布均衡,并且在增加cache server后,cache的遷移做到最少。

   這里提到的一致性hash算法ketama的做法是:選擇具體的機器節點不在只依賴需要緩存數據的key的hash本身了,而是機器節點本身也進行了hash運算。


 

二、一致性哈希算法情景描述(轉載)

1、 hash機器節點


首先求出機器節點的hash值(怎么算機器節點的hash?ip可以作為hash的參數吧。。當然還有其他的方法了),然后將其分布到0~2^32的一個圓環上(順時針分布)。如下圖所示:

圖一

集群中有機器:A , B, C, D, E五臺機器,通過一定的hash算法我們將其分布到如上圖所示的環上。


2、訪問方式

如果有一個寫入緩存的請求,其中Key值為K,計算器hash值Hash(K), Hash(K) 對應于圖 – 1環中的某一個點,如果該點對應沒有映射到具體的某一個機器節點,那么順時針查找,直到第一次找到有映射機器的節點,該節點就是確定的目標節點,如果超過了2^32仍然找不到節點,則命中第一個機器節點。比如 Hash(K) 的值介于A~B之間,那么命中的機器節點應該是B節點(如上圖 )。


3、增加節點的處理

如上圖 – 1,在原有集群的基礎上欲增加一臺機器F,增加過程如下:

計算機器節點的Hash值,將機器映射到環中的一個節點,如下圖:

圖二 

增加機器節點F之后,訪問策略不改變,依然按照(2)中的方式訪問,此時緩存命不中的情況依然不可避免,不能命中的數據是hash(K)在增加節點以前落在C~F之間的數據。盡管依然存在節點增加帶來的命中問題,但是比較傳統的 hash取模的方式,一致性hash已經將不命中的數據降到了最低。

 

Consistent Hashing最大限度地抑制了hash鍵的重新分布。另外要取得比較好的負載均衡的效果,往往在服務器數量比較少的時候需要增加虛擬節點來保證服務器能均勻的分布在圓環上。因為使用一般的hash方法,服務器的映射地點的分布非常不均勻。使用虛擬節點的思想,為每個物理節點(服務器)在圓上分配100~200個點。這樣就能抑制分布不均勻,最大限度地減小服務器增減時的緩存重新分布。用戶數據映射在虛擬節點上,就表示用戶數據真正存儲位置是在該虛擬節點代表的實際物理服務器上。
下面有一個圖描述了需要為每臺物理服務器增加的虛擬節點。


圖三

x軸表示的是需要為每臺物理服務器擴展的虛擬節點倍數(scale),y軸是實際物理服務器數,可以看出,當物理服務器的數量很小時,需要更大的虛擬節點,反之則需要更少的節點,從圖上可以看出,在物理服務器有10臺時,差不多需要為每臺服務器增加100~200個虛擬節點才能達到真正的負載均衡。

三、以spymemcache源碼來演示虛擬節點應用

1、上邊描述的一致性Hash算法有個潛在的問題是:
     (1)、將節點hash后會不均勻地分布在環上,這樣大量key在尋找節點時,會存在key命中各個節點的概率差別較大,無法實現有效的負載均衡。
     (2)、如有三個節點Node1,Node2,Node3,分布在環上時三個節點挨的很近,落在環上的key尋找節點時,大量key順時針總是分配給Node2,而其它兩個節點被找到的概率都會很小。

2、這種問題的解決方案可以有:
     改善Hash算法,均勻分配各節點到環上;[引文]使用虛擬節點的思想,為每個物理節點(服務器)在圓上分配100~200個點。這樣就能抑制分布不均勻,最大限度地減小服務器增減時的緩存重新分布。用戶數據映射在虛擬節點上,就表示用戶數據真正存儲位置是在該虛擬節點代表的實際物理服務器上。

在查看Spy Memcached client時,發現它采用一種稱為Ketama的Hash算法,以虛擬節點的思想,解決Memcached的分布式問題。

3、源碼說明

該client采用TreeMap存儲所有節點,模擬一個環形的邏輯關系。在這個環中,節點之前是存在順序關系的,所以TreeMap的key必須實現Comparator接口。
那節點是怎樣放入這個環中的呢?

  

  1.    protected void setKetamaNodes(List<MemcachedNode> nodes) {  
  2. TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>();  
  3. int numRepsconfig.getNodeRepetitions();  
  4. for(MemcachedNode node : nodes) {  
  5.     // Ketama does some special work with md5 where it reuses chunks.  
  6.     if(hashAlg == HashAlgorithm.KETAMA_HASH) {  
  7.         for(int i=0; i<numReps / 4; i++) {  
  8.             byte[] digest=HashAlgorithm.computeMd5(config.getKeyForNode(node, i));  
  9.             for(int h=0;h<4;h++) {  
  10.                 Long k = ((long)(digest[3+h*4]&0xFF) << 24)  
  11.                     | ((long)(digest[2+h*4]&0xFF) << 16)  
  12.                     | ((long)(digest[1+h*4]&0xFF) << 8)  
  13.                     | (digest[h*4]&0xFF);  
  14.                 newNodeMap.put(k, node);  
  15.                 getLogger().debug("Adding node %s in position %d", node, k);  
  16.             }  
  17.   
  18.         }  
  19.     } else {  
  20.         for(int i=0; i<numReps; i++) {  
  21.             newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node);  
  22.         }  
  23.     }  
  24. }  
  25. assert newNodeMap.size() == numReps * nodes.size();  
  26. ketamaNodes = newNodeMap;  
protected void setKetamaNodes(List<MemcachedNode> nodes) {
    TreeMap<Long, MemcachedNode> newNodeMap = new TreeMap<Long, MemcachedNode>();
    int numReps= config.getNodeRepetitions();
    for(MemcachedNode node : nodes) {
        // Ketama does some special work with md5 where it reuses chunks.
        if(hashAlg == HashAlgorithm.KETAMA_HASH) {
            for(int i=0; i<numReps / 4; i++) {
                byte[] digest=HashAlgorithm.computeMd5(config.getKeyForNode(node, i));
                for(int h=0;h<4;h++) {
                    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);
                    newNodeMap.put(k, node);
                    getLogger().debug("Adding node %s in position %d", node, k);
                }

            }
        } else {
            for(int i=0; i<numReps; i++) {
                newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node);
            }
        }
    }
    assert newNodeMap.size() == numReps * nodes.size();
    ketamaNodes = newNodeMap;



上面的流程大概可以這樣歸納:四個虛擬結點為一組,以getKeyForNode方法得到這組虛擬節點的name,Md5編碼后,每個虛擬結點對應Md5碼16個字節中的4個,組成一個long型數值,做為這個虛擬結點在環中的惟一key。第10行k為什么是Long型的呢?就是因為Long型實現了Comparator接口。

處理完正式結點在環上的分布后,可以開始key在環上尋找節點的游戲了。
對于每個key還是得完成上面的步驟:計算出Md5,根據Md5的字節數組,通過Kemata Hash算法得到key在這個環中的位置。

 

  1. MemcachedNode getNodeForKey(long hash) {  
  2.     final MemcachedNode rv;  
  3.     if(!ketamaNodes.containsKey(hash)) {  
  4.         // Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5  
  5.         // in a lot of places, so I'm doing this myself.  
  6.         SortedMap<Long, MemcachedNode> tailMap=getKetamaNodes().tailMap(hash);  
  7.         if(tailMap.isEmpty()) {  
  8.             hash=getKetamaNodes().firstKey();  
  9.         } else {  
  10.             hash=tailMap.firstKey();  
  11.         }  
  12.     }  
  13.     rv=getKetamaNodes().get(hash);  
  14.     return rv;  
  15. }  
MemcachedNode getNodeForKey(long hash) {
        final MemcachedNode rv;
        if(!ketamaNodes.containsKey(hash)) {
            // Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5
            // in a lot of places, so I'm doing this myself.
            SortedMap<Long, MemcachedNode> tailMap=getKetamaNodes().tailMap(hash);
            if(tailMap.isEmpty()) {
                hash=getKetamaNodes().firstKey();
            } else {
                hash=tailMap.firstKey();
            }
        }
        rv=getKetamaNodes().get(hash);
        return rv;
    }


 上邊代碼的實現就是在環上順時針查找,沒找到就去的第一個,然后就知道對應的物理節點了。

四、應用場景分析

1、memcache的add方法:通過一致性hash算法確認當前客戶端對應的cacheserver的hash值以及要存儲數據key的hash進行對應,確認cacheserver,獲取connection進行數據存儲

2、memcache的get方法:通過一致性hash算法確認當前客戶端對應的cacheserver的hash值以及要提取數據的hash值,進而確認存儲的cacheserver,獲取connection進行數據提取

五、總結

1、一致性hash算法只是幫我們減少cache集群中的機器數量增減的時候,cache的數據能進行最少重建。只要cache集群的server數量有變化,必然產生數據命中的問題

2、對于數據的分布均衡問題,通過虛擬節點的思想來達到均衡分配。當然,我們cache server節點越少就越需要虛擬節點這個方式來均衡負載。

3、我們的cache客戶端根本不會維護一個map來記錄每個key存儲在哪里,都是通過key的hash和cacheserver(也許ip可以作為參數)的hash計算當前的key應該存儲在哪個節點上。

4、當我們的cache節點崩潰了。我們必定丟失部分cache數據,并且要根據活著的cache server和key進行新的一致性匹配計算。有可能對部分沒有丟失的數據也要做重建...

5、至于正常到達數據存儲節點,如何找到key對應的數據,那就是cache server本身的內部算法實現了,此處不做描述。


這里只是針對數據的存儲方式以及提取方式進行了流程展示。

 

轉載:http://blog.csdn.net/kongqz/article/details/6695417

 

 

 本文由用戶 MiraBergstr 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!