一致性Hash算法在Memcached中的應用

前言

  你們應該都知道Memcached要想實現分佈式只能在客戶端來完成,目前比較流行的是經過一致性hash算法來實現.常規的方法是將server的hash值與server的總檯數進行求餘,即hash%N,這種方法的弊端是當增減服務器時,將會有較多的緩存須要被從新分配且會形成緩存分配不均勻的狀況(有可能某一臺服務器分配的不少,其它的卻不多).html

   今天分享一種叫作」ketama」的一致性hash算法,它經過虛擬節點的概念和不一樣的緩存分配規則有效的抑制了緩存分佈不均勻,並最大限度地減小服務器增減時緩存的從新分佈。 java

實現思路

  假設咱們如今有N臺Memcached的Server,若是咱們用統一的規則對memcached進行Set,Get操做. 使具備不一樣key的object很均衡的分散存儲在這些Server上,Get操做時也是按一樣規則去對應的Server上取出object,這樣各個Server之間不就是一個總體了嗎?node

那究竟是一個什麼樣的規則?算法

  以下圖所示,咱們如今有5臺(A,B,C,D,E)Memcached的Server,咱們將其串聯起來造成一個環形,每一臺Server都表明圓環上的一個點,每個點都具備惟一的Hash值,這個圓環上一共有2^32個點.c#

       那麼該如何肯定每臺Server具體分佈在哪一個點上? 這裏咱們經過」Ketama」的Hash算法來計算出每臺Server的Hash值,拿到Hash值後就能夠對應到圓環上點了.(能夠用Server的IP地址做爲Hash算法的Key.)數組

  這樣作的好處是,以下圖當我新增Server  F時,那麼我只須要將hash值落在C和F之間的object從本來的D上從新分配到F上就能夠了,其它的server上的緩存不須要從新分配,而且新增的Server也能及時幫忙緩衝其它Server的壓力.緩存

  到此咱們已經解決了增減服務器時大量緩存須要被從新分配的弊端.那該如何解決緩存分配不均勻的問題呢?由於如今咱們的server只佔據圓環上的6個點,而圓環上總共有2^32個點,這極其容易致使某一臺server上熱點很是多,某一臺上熱點不多的狀況.服務器

  」虛擬節點」的概念很好的解決了這種負載不均衡的問題.經過將每臺物理存在的Server分割成N個虛擬的Server節點(N一般根據物理Server個數來定,這裏有個比較好的閾值爲250).這樣每一個物理Server實際上對應了N個虛擬的節點. 存儲點多了,各個Server的負載天然要均衡一些.就像地鐵站出口同樣,出口越多,每一個出口出現擁擠的狀況就會越少.負載均衡

   代碼實現:分佈式

//保存全部虛擬節點信息, key : 虛擬節點的hash key, value: 虛擬節點對應的真實server
        private Dictionary<uint, string> hostDictionary = new Dictionary<uint, string>();
        //保存全部虛擬節點的hash key, 已按升序排序
        private uint[] ketamaHashKeys = new uint[] { };
        //保存真實server主機地址
        private string[] realHostArr = new string[] { };
        //每臺真實server對應虛擬節點個數
        private int VirtualNodeNum = 250;

        public KetamaVirtualNodeInit(string[] hostArr)
        {
            this.realHostArr = hostArr;
            this.InitVirtualNodes();
        }

        /// <summary>
        /// 初始化虛擬節點
        /// </summary>
        private void InitVirtualNodes()
        {
            hostDictionary = new Dictionary<uint, string>();
            List<uint> hostKeys = new List<uint>();
            if (realHostArr == null || realHostArr.Length == 0)
            {
                throw new Exception("不能傳入空的Server集合");
            }

            for (int i = 0; i < realHostArr.Length; i++)
            {
                for (int j = 0; j < VirtualNodeNum; j++)
                {
                    byte[] nameBytes = Encoding.UTF8.GetBytes(string.Format("{0}-node{1}", realHostArr[i], j));
                    //調用Ketama hash算法獲取hash key
                    uint hashKey = BitConverter.ToUInt32(new KetamaHash().ComputeHash(nameBytes), 0);
                    hostKeys.Add(hashKey);
                    if (hostDictionary.ContainsKey(hashKey))
                    {
                        throw new Exception("建立虛擬節點時發現相同hash key,請檢查是否傳入了相同Server");
                    }
                    hostDictionary.Add(hashKey, realHostArr[i]);
                }
            }

            hostKeys.Sort();
            ketamaHashKeys = hostKeys.ToArray();
        }

 

一致性hash算法的分配規則

  到此咱們已經知道了全部虛擬節點的Hash值, 如今讓咱們來看下當咱們拿到一個對象時如何存入Server, 或是拿到一個對象的Key時該如何取出對象. 

       Set一個對象時,先將對象的Key做爲」Ketama」算法的Key,計算出Hash值後咱們須要作下面幾個步驟.

       1:首先檢查虛擬節點當中是否有與當前對象Hash值相等的,若有則直接將對象存入那個Hash值相等的節點,後面的步驟就不繼續了.

       2:如沒有,則找出第一個比當前對象Hash值要大的節點,(節點的Hash值按升序進行排序,圓環上對應按照順時針來排列),即離對象最近的節點,而後將對象存入該節點.

       3:若是沒有找到Hash值比對象要大的Server,證實對象的Hash值是介於最後一個節點和第一個節點之間的,也就是圓環上的E和A之間.這種狀況就直接將對象存入第一個節點,即A.

  代碼實現:  

     /// <summary>
        /// 根據hash key 獲取對應的真實Server
        /// </summary>
        /// <param name="hash"></param>
        /// <returns></returns>
        public string GetHostByHashKey(string key)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(key);
            uint hash = BitConverter.ToUInt32(new KetamaHash().ComputeHash(bytes), 0);

            //尋找與當前hash值相等的Server. 
            int i = Array.BinarySearch(ketamaHashKeys, hash);

            //若是i小於零則表示沒有hash值相等的虛擬節點
            if (i < 0)
            {
                //將i繼續按位求補,獲得數組中第一個大於當前hash值的虛擬節點
                i = ~i;

                //若是按位求補後的i大於等於數組的大小,則表示數組中沒有大於當前hash值的虛擬節點
                //此時直接取第一個server
                if (i >= ketamaHashKeys.Length)
                {
                    i = 0;
                }
            }

            //根據虛擬節點的hash key 返回對應的真實server host地址
            return hostDictionary[ketamaHashKeys[i]];
        }

 

Get一個對象,一樣也是經過」Ketama」算法計算出Hash值,而後與Set過程同樣尋找節點,找到以後直接取出對象便可.

那麼這個」Ketama」到底長什麼樣呢,讓咱們來看看代碼實現.

    /// <summary>
    ///  Ketama hash加密算法
    ///  關於HashAlgorithm參見MSDN連接
    ///  http://msdn.microsoft.com/zh-cn/library/system.security.cryptography.hashalgorithm%28v=vs.110%29.aspx
    /// </summary>
    public class KetamaHash : HashAlgorithm
    {

        private static readonly uint FNV_prime = 16777619;
        private static readonly uint offset_basis = 2166136261;

        protected uint hash;

        public KetamaHash()
        {
            HashSizeValue = 32;
        }

        public override void Initialize()
        {
            hash = offset_basis;
        }

        protected override void HashCore(byte[] array, int ibStart, int cbSize)
        {
            int length = ibStart + cbSize;
            for (int i = ibStart; i < length; i++)
            {
                hash = (hash * FNV_prime) ^ array[i];
            }
        }

        protected override byte[] HashFinal()
        {
            hash += hash << 13;
            hash ^= hash >> 7;
            hash += hash << 3;
            hash ^= hash >> 17;
            hash += hash << 5;
            return BitConverter.GetBytes(hash);
        }
    }

測試性能

最後我把本身參考BeitMemcached寫的算法與老代(Discuz!代震軍)參考SPYMemcached寫的作了一下對比.

源碼在後面有下載.

結果:查找5W個key的時間比老代的版本快了100多倍,但在負載均衡方面差了一些. 

測試數據:

   1:真實Server都是5臺

       2:隨機生成5W個字符串key(生成方法直接拿老代的)

       3:虛擬節點都是250個 

       個人版本:

老代的版本:

 

參考資料

BeitMemcached源碼

老代: 一致性Hash算法(KetamaHash)的c#實現

總結一致性哈希(Consistent Hashing) 

 

測試代碼下載:Memcached-ketama

相關文章
相關標籤/搜索