Memcache Hash算法揭祕



    在大型互聯網應用架構中,通常由多臺Memcache緩存服務器來構建Memcache集羣,也叫做分佈式Memcache。數據在寫入緩存和從緩存中讀取時,都會採用某中Hash算法,將數據Hash到某臺具體的Memcache上,爲了使應用在線的動態增加和移除Memcache服務器而不影響或很少影響其他已部署的Memcache服務器(也就是其他Memcache服務器中緩存的數據還能正常使用),這種Hash算法被稱作爲一致性Hash

    由一篇文章---Memcache分佈式實現原理我們知道,Java_Memcahe支持一般Hash算法和實現應用中使用最廣泛的一致性Hash算法,這兩種算法都支持Server配置權重的虛擬節點Hash算法。下面我們由淺入深一一來探討學習一下Java_Memcache的各種Hash 算法。

根據Memcache操作的步驟


SockIOPool sockpool= SockIOPool.getInstance();
//設置緩存服務器地址,可以設置多個實現分佈式緩存
sockpool.setServers(new String[]{"127.0.0.1:11211","127.0.0.1:11212"});
//設置初始連接5
sockpool.setInitConn(5);
......
sockpool.initialize();//根據server以及一些配置信息初始化
memCache = new MemCachedClient();
//使用memcache操作數據memCache.get()  & memCache.set()

一般Hash算法

初始化

SockIOPool類初始化時會調用SchoonerSocketIOPoolPopulateBuckets方法,保存好Server信息,以便後續操作數據時使用。

private void populateBuckets()
{
    buckets = new ArrayList();
    for(int i = 0; i < servers.length; i++)
    {
        if(weights != null && weights.length > i)
        {
            for(int j = 0; j < weights[i].intValue(); j++)
                buckets.add(servers[i]);
        } else
        {
            buckets.add(servers[i]);//按Server的個數保存好Server地址及端口
        }
        Object obj;
        if(authInfo != null)
            obj = new AuthSchoonerSockIOFactory(servers[i], isTcp, bufferSize, socketTO, socketConnectTO, nagle, authInfo);
        else
            obj = new SchoonerSockIOFactory(servers[i], isTcp, bufferSize, socketTO, socketConnectTO, nagle);
        GenericObjectPool genericobjectpool = new GenericObjectPool(((org.apache.commons.pool.PoolableObjectFactory) (obj)), maxConn, (byte)1, maxIdle, maxConn);
        ((SchoonerSockIOFactory) (obj)).setSockets(genericobjectpool);
        socketPool.put(servers[i], genericobjectpool);//保存好Server與對應的對象工廠,該工廠管理與Server建立Socket的連接
    }
}

在不考慮權重時,按Server的個數保存好Server信息,並保存與該server對應的對象工廠,用於管理與server建立Socket連接。


操作數據

在使用MemcacheClient進行get/set 時實際操作的是AscIIClientget/set操作,由Memcache分佈式原理我們知道,get方法最後執行的是get(String s, String s1, Integer integer, boolean flag)方法。

public class AscIIClient extends MemCachedClient
{
    private Object get(String s, String s1, Integer integer, boolean flag)
    {
        SchoonerSockIO schoonersockio;
        String s2;
        ......  
        s1 = sanitizeKey(s1);//UTF-8編碼
         
        schoonersockio = pool.getSock(s1, integer);//獲取對應的memcache  server socket,後續直接從該server上讀取數據
        ......
    }
}

然後我們再看看pool.getSocket

public class SchoonerSockIOPool
{
    public final SchoonerSockIO getSock(String s, Integer integer)
    {
        ......
        if(i == 1){
        //這裏作了一點優化,如果只有一臺Memcache服務器,就直接拿List中的第一個Server,沒必要後面去Hash
            SchoonerSockIO schoonersockio = hashingAlg != 3 ? getConnection((String)buckets.get(0)) : getConnection((String)consistentBuckets.get(consistentBuckets.firstKey()));
            return schoonersockio;
        }
        HashSet hashset = new HashSet(Arrays.asList(servers));
        long l = getBucket(s, integer);//利用key--s的hashcode%Server個數,找到某個server在list中的位置
        String s1 = hashingAlg != 3 ? (String)buckets.get((int)l) : (String)consistentBuckets.get(Long.valueOf(l));
        do
        {
            if(hashset.isEmpty())
                break;
            SchoonerSockIO schoonersockio1 = getConnection(s1);//根據Server IP和商品,建立Socket
            if(schoonersockio1 != null)
                return schoonersockio1; //返回Socket
            ......
        } while(true);
        return null;
    }

    private final long getBucket(String s, Integer integer)
    {
        long l = getHash(s, integer);
        ......
        long l1 = l % (long)buckets.size(); //利用HashCode % server個數
        if(l1 < 0L)
            l1 *= -1L;
        return l1;
    }

    private final long getHash(String s, Integer integer)
    {
        if(integer != null)
            if(hashingAlg == 3)
                return integer.longValue() & 4294967295L;
            else
                return integer.longValue();
        switch(hashingAlg)
        {
        case 0: // '\0'
            return (long)s.hashCode();//直接返回key的hashCode

        case 1: // '\001'
            return origCompatHashingAlg(s);

        case 2: // '\002'
            return newCompatHashingAlg(s);

        case 3: // '\003'
            return md5HashingAlg(s);
        }
        hashingAlg = 0;
        return (long)s.hashCode();
    }

小結: 在初始化時將所有的Memcache Server存放在List中,後面操作數據時,根據數據的HashCode  % server個數,計算出數據真正操作在哪臺Server上,然後再建立與該Serversocket連接,操作數據。

問題:在site上,如果後期動態增加或移除Memcache server,數據的hashcode沒有發生變化,但是由於server數據發生變化,取模運算的模變化了,計算的結果也會發生變化,所有Memcache Server上的數據都會失效,這對後臺數據庫來說簡單就是災難。


基於權重的Hash算法

在上述算法中,利用取模運算,每臺Memcache Server具體等同的概率被命中,但是在實際應用中,各個server的硬件,軟件配置不同,都使用等同的命中率是不太合適的。例如,server AC的可供Memcach使用內存都是2G,另外一臺server B8G,使用相同的命中率顯然會浪費寶貴的內存資源。

爲了解決這一問題,Memcache Client引入了權重的概念,算法會根據每臺Server的權重來分配命中到該機器上的概率。例如:server AC的權重都設置爲2server B的權重設置爲8,那麼AC的命中率爲2/12B的命中率爲8/12。話不多話,直接上代碼。

private void populateBuckets()
{
    buckets = new ArrayList();
    for(int i = 0; i < servers.length; i++)
    {
        if(weights != null && weights.length > i)
        {
            //如果配置了權重,根據權重在list中保存server信息時增加虛擬server
            for(int j = 0; j < weights[i].intValue(); j++)
                buckets.add(servers[i]);
        } 
    ......

結合前面的例子會更容易讓人理解,在list中保存2server A的信息,8server B的信息,2server C的信息,總共12server。後面使用相同的Hash算法進行%12運算,根據概率的平均分佈,list中每個server的概率都是1/12,但是其中2server對應的是server A8server對應的是server B2server對應的是server C

這樣,根據各個server的權重就能決定server的數據命中率,以達到合理使用資源的要求了。


一致性Hash算法

初始化

SockIOPool初始化時根據配置的hash算法完成初始化, SocketIOPoolhashingAlg ==3表示一致性Hash.




public class SchoonerSockIOPool
{
    private TreeMap consistentBuckets;
    ......
    private void populateConsistentBuckets()
    {
        consistentBuckets = new TreeMap();
        MessageDigest messagedigest = (MessageDigest)MD5.get();
        if(totalWeight.intValue() <= 0 && weights != null)
        {
            for(int i = 0; i < weights.length; i++)
            {
                SchoonerSockIOPool schoonersockiopool = this;
                schoonersockiopool.totalWeight = Integer.valueOf(schoonersockiopool.totalWeight.intValue() + (weights[i] != null ? weights[i].intValue() : 1));
            }
        }
        if(weights == null)
            totalWeight = Integer.valueOf(servers.length);
        for(int j = 0; j < servers.length; j++)
        {
            int k = 1;
            if(weights != null && weights[j] != null)
                k = weights[j].intValue();
            double d = Math.floor((double)(40 * servers.length * k) / (double)totalWeight.intValue());
            for(long l = 0L; (double)l < d; l++)
            {
                byte abyte0[] = messagedigest.digest((new StringBuilder()).append(servers[j]).append("-").append(l).toString().getBytes());
                for(int i1 = 0; i1 < 4; i1++)
                {
                    Long long1 = Long.valueOf((long)(abyte0[3 + i1 * 4] & 255) << 24 | (long)(abyte0[2 + i1 * 4] & 255) << 16 | (long)(abyte0[1 + i1 * 4] & 255) << 8 | (long)(abyte0[0 + i1 * 4] & 255));
                    consistentBuckets.put(long1, servers[j]);
                }

            }
            Object obj;
            if(authInfo != null)
                obj = new AuthSchoonerSockIOFactory(servers[j], isTcp, bufferSize, socketTO, socketConnectTO, nagle, authInfo);
            else
                obj = new SchoonerSockIOFactory(servers[j], isTcp, bufferSize, socketTO, socketConnectTO, nagle);
            GenericObjectPool genericobjectpool = new GenericObjectPool(((org.apache.commons.pool.PoolableObjectFactory) (obj)), maxConn, (byte)1, maxIdle, maxConn);
            ((SchoonerSockIOFactory) (obj)).setSockets(genericobjectpool);
            socketPool.put(servers[j], genericobjectpool);
        }
    }

上面的算法理解起來比較費力,先考慮只有沒有配置權重的情況,算法簡化爲

for(int j = 0; j < servers.length; j++)
{
	double d = 40;
	for(long l = 0L; (double)l < 40 ; l++)
	{
	    byte abyte0[] = messagedigest.digest((new StringBuilder()).append(servers[j]).append("-").append(l).toString().getBytes());
	    for(int i1 = 0; i1 < 4; i1++)
	    {
	        Long long1 = Long.valueOf((long)(abyte0[3 + i1 * 4] & 255) << 24 | (long)(abyte0[2 + i1 * 4] & 255) << 16 | (long)(abyte0[1 + i1 * 4] & 255) << 8 | (long)(abyte0[0 + i1 * 4] & 255));
	        consistentBuckets.put(long1, servers[j]);
	    }
    }
}

首先利用server信息進行MD5加密產生字符數組,然後最內層的for循環將字符數據分割成4片,每一片轉換爲32位的long

a[3] a[2] a[1] a[0]

a[7] a[6] a[5] a[4]

a[11] a[10] a[9] a[8]5

a[15] a[14] a[13] a[12]

歸根到底是爲了保證每個long值的唯一性。

每臺server都會保存160份信息在Map中。


操作數據

在使用MemcacheClient進行get/set 時實際操作的是AscIIClientget/set操作,get方法最後執行的是get(String s, String s1, Integer integer, boolean flag)方法,先調用 pool.getSocket獲取對應memache serversocket,然後再操作數據。

public class SchoonerSockIOPool
{
    public final SchoonerSockIO getSock(String s, Integer integer)
    {
        ......
        HashSet hashset = new HashSet(Arrays.asList(servers));
        long l = getBucket(s, integer);
        String s1 = hashingAlg != 3 ? (String)buckets.get((int)l) : (String)consistentBuckets.get(Long.valueOf(l));
        do
        {
            if(hashset.isEmpty())
                break;
            SchoonerSockIO schoonersockio1 = getConnection(s1);//根據Server IP和商品,建立Socket
            if(schoonersockio1 != null)
                return schoonersockio1; //返回Socket
            ......
        } while(true);
        return null;
    }

    private final long getBucket(String s, Integer integer)
    {
        long l = getHash(s, integer);
        if(hashingAlg == 3)
            return findPointFor(Long.valueOf(l)).longValue();
        ......
    }

    private final long getHash(String s, Integer integer)
    {
        ......
        switch(hashingAlg)
        {
        ......

        case 3: // '\003'
            return md5HashingAlg(s);//返回key的MD5 hashCode
        }
        hashingAlg = 0;
        return (long)s.hashCode();
    }

    private static long md5HashingAlg(String s)
    {
        //根據key值進行MD5加密,取前32位作爲hashCode
        MessageDigest messagedigest = (MessageDigest)MD5.get();
        messagedigest.reset();
        messagedigest.update(s.getBytes());
        byte abyte0[] = messagedigest.digest();
        long l = (long)(abyte0[3] & 255) << 24 | (long)(abyte0[2] & 255) << 16 | (long)(abyte0[1] & 255) << 8 | (long)(abyte0[0] & 255);
//在初始化時也用到了該值,只不過初始化時計算MD5是用的server信息
        return l;
    }

    private final Long findPointFor(Long long1)
    {
        SortedMap sortedmap = consistentBuckets.tailMap(long1);
        return sortedmap.isEmpty() ? (Long)consistentBuckets.firstKey() : (Long)sortedmap.firstKey();
    }

這裏需要了解一下tailMap API



翻譯過來就是該方法返回Mapkey值大於或等於參數的所有元素的視圖,對該視圖的修改都會反映到Map中,反之對Map的修改也會反映到視圖中。

在找到Hash值大於key Hash值的所有server後,使用第一個server。整個處理流程可以表示爲下圖。


小結:一致性Hash算法首先利用server信息進行MD5 Hash,將server映射到32位的圓環上(只取32),在操作數據時,用MD5data進行Hash,將data映射到32位的圓環上(也取32),順時針找到第一個Hash值比data Hash值大的server

需要注意的是,該算法也加入了虛擬節點的思想,每節server進行MD5 Hash映射到圓環上時都虛擬到了很多個節點,在本例中每臺server虛擬出了160個節點,這樣做是爲了保證數據能較均勻的分佈到各個server上。

採用MD5 serverdata進行Hash時不依賴於其他server,因此Hash值的計算是一致性的,而僅僅在將data映射到某臺server上時取決於第一個Hash值大於或等於data Hash值的server,在動態增加和移除server時僅會影響該server右邊直到逆時針下一個server之間的data,而其他區間的data不受影響,該算法被廣泛應用中Memcache集羣中。

 

基於權重的一致性Hash算法

對於一般Hash算法,一致性Hash算法還有一個很重要的區別:在沒有配置權重時,一致性Hash算法也會增加虛擬節點。基於權重的一致性Hash算法也是爲了保證數據的命中率。

結合一般Hash算法中的例子,Server A的權重是2Server B的權重是8Server C的權重是2,在初始化populateConsistentBuckets 中,Server A會分配80個虛擬節點,Server B會分配320個虛擬節點,Server C會分配80個虛擬節點。


總結

通過對源碼的分析,我們理清了Memcache 的一般Hash算法和一致性Hash算法,揭開了Memcache分佈式算法的真實面目,在理解完算法後,我們是不是應該更加透澈地理解Memcache了呢?(寫於2015-05-17)





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章