探索c#之一致性Hash詳解


 

閱讀目錄:

  1. 使用場景
  2. 算法原理
  3. 虛擬節點
  4. 代碼示例

使用場景

以Redis為例,當系統需要緩存的內容超過單機內存大小時,例如要緩存100G數據,單機內存僅有16G時。這時候就需要考慮進行緩存數據分片,也即是把100G的數據拆分成多塊小於單機內存的數據。例如以10G為單位,拆分10份,存儲到多台機器節點上。 但是數據怎么個分法更合理呢? 、

f(key)%n

這里配置n=10,不同的key根據數值余數映射到對應的機器。 很簡單的辦法就解決了多台節點key分法的問題。然而數據大小的增長和縮減是很難預知的, 如果需要增加一台緩存服務器。 配置n=11,會發現之前根據余數建立的映射關系發生混亂。映射錯亂后,就會發生大量key無法命中正確的節點,需要全部重新進行映射。  如果以后再添加節點,同樣會遇到這樣問題。 

    servers = ['redis:6379', 'redis:6380', 'redis:6381']
    server =  servers[f(key) % servers.length]

一致性hash(consistent hashing)

為了降低添加或刪除服務器節點,導致大量key無法命中的影響。就提出了一種更為合理的分法,也即是一致性hash算法。 下面看下為什么更合理些?

算法原理

空間歸屬

我們在腦中假想下:每台節點以CHash(ip)形式計算出一個數值,n台機器有n個數值。 把數值首尾相連,形成一個虛擬圓環的數值空間。
例如有3台機器:

servers =['redis:6379', 'redis:6380', 'redis:6381']。

CHash(server[0])==100  
Chash(server[1])==200  
CHash(server[2])==300  

把機器計算得到的數值,在虛擬圓環中按照順時針方向來確定空間歸屬,得到:

100~200空間屬於6379管。
200~300空間屬於6380管。
300+,100-空間屬於6381管。

key坐標

這時有3個key要存儲到redis,分別是key1—key3。 通過CHash函數計算出3個key的數值坐標:

CHash(key1)=102
CHash(key2)=240
CHash(key3)=350

空間映射

求出key的數值坐標后,就知道key與機器節點的映射關系。 即key1應存儲在6379,key3存儲到6381。

添加節點

由於緩存數據的增加,需要添加一台新節點6382。計算出空間數值:

CHash(6382)==250  

那么他在虛擬圓環中的位置如下:

從圖中得知,6379、6381的數值空間區域沒任何變化,它們存儲的key依舊可以正常命中的。
優點之一:對現有緩存的命中影響較小。
但原本6380的區域200~300被6382侵入了。 6382的空間數值250正好划分一半,即200~250的區域還歸6380管,但250~300的區域卻歸新來的管了。 (為示例而使用簡單數字區分,實際上沒這么精准)
優點之二:實現對數據的分片
同時也帶來了缺點就是: 原本存儲在6380(250~300這部分)的舊緩存數據就無法命中了,要去新的6382拿。 所以說一致性hash並不能完全解決這種影響,只能盡量降低。

移除節點

與添加節點同理。比如拿掉新加的6382,250~300區域還管原來的6380管,當然6382這部分緩存也就丟了。

虛擬節點

一致性Hash雖然實現了數據分片,但由於節點較少,key有可能會大量集中到某一台上面,導致緩存分布不均勻。 特別是在只有幾台或十幾台機器節點時。
為了降低這種影響,一致性hash算法提出虛擬節點的解決方案。 即一個物理機器節點對應着多個虛擬節點。 這里配置一個物理節點對應2個虛擬節點,此時應為:

6379={6379A,6379B}
6380={6380A,6380B}
6381={6381A,6381B}

這樣成了6個節點了(可以配置更多),它們同樣在虛擬圓環上按數值順時針排列。由於節點變多,對應的數值區域也變大。使key進行數值空間映射時變的更加離散性,從概率上來提高key的均勻分布。

原本需要計算真實節點數值,也變成計算虛擬節點數值, 然后由虛擬節點的數值構成虛擬圓環數值空間。其中每一組虛擬節點數值,對應單個物理節點。

servers= ['redis:6379', 'redis:6380', 'redis:6381'];

//下面f函數中先將servers與虛擬節點映射成 6379={6379A,6379B}, 6380={6380A,6380B},6381={6381A,6381B}
// 在對虛擬節點求各自的數值,而數值對應的還是物理節點。即:
vservers = f(servers) ={['redis:6379','100'],['redis:6379','300'] ....,['redis:6381',150]}; CHash(key1)==102 ∈ vservers[0]  ...... CHash(key3)==350 ∈ vservers[1]  

虛擬節點使key分布的更加均衡,但不能解決添加機、刪除節點帶來的影響。

代碼示例

1:使用字典模擬虛擬圓環,並添加節點。

2:計算key數值,應該歸屬到哪個節點數值空間區域。

3:計算分布頻率。復制的虛擬節點越多,分布越平均。

        private static readonly SortedDictionary<ulong, string> _circle = new SortedDictionary<ulong, string>();
        static void Main(string[] args)
        {
            int Replicas = 100;
            AddNode("127.0.0.1:6379", Replicas);
            AddNode("127.0.0.1:6380", Replicas);
            AddNode("127.0.0.1:6381", Replicas);
            List<string> nodes = new List<string>();
            for (int i = 0; i < 100; i++)
            {
                nodes.Add(GetTargetNode(i + "test" + (char)i));
            }
            var counts = nodes.GroupBy(n => n, n => n.Count()).ToList();
            counts.ForEach(index => Console.WriteLine(index.Key+"-"+index.Count()));
            Console.ReadLine();
        }

輸出:

127.0.0.1:6380-39
127.0.0.1:6381-29
127.0.0.1:6379-32

虛擬圓環的值:

其余代碼:

 public static void AddNode(string node, int repeat)
        {
            for (int i = 0; i < repeat; i++)
            {
                string identifier = node.GetHashCode().ToString() + "-" + i;
                ulong hashCode = Md5Hash(identifier);
                _circle.Add(hashCode, node);
            }
        }

        public static ulong Md5Hash(string key)
        {
            using (var hash = System.Security.Cryptography.MD5.Create())
            {
                byte[] data = hash.ComputeHash(Encoding.UTF8.GetBytes(key));
                var a = BitConverter.ToUInt64(data, 0);
                var b = BitConverter.ToUInt64(data, 8);
                ulong hashCode = a ^ b;
                return hashCode;
            }
        }
        public static string GetTargetNode(string key)
        {
            ulong hash = Md5Hash(key);
            ulong firstNode = ModifiedBinarySearch(_circle.Keys.ToArray(), hash);
            return _circle[firstNode];
        }

        /// <summary>
        /// 計算key的數值,得出空間歸屬。
        /// </summary>
        /// <param name="sortedArray"></param>
        /// <param name="val"></param>
        /// <returns></returns>
        public static ulong ModifiedBinarySearch(ulong[] sortedArray, ulong val)
        {
            int min = 0;
            int max = sortedArray.Length - 1;

            if (val < sortedArray[min] || val > sortedArray[max])
                return sortedArray[0];

            while (max - min > 1)
            {
                int mid = (max + min) / 2;
                if (sortedArray[mid] >= val)
                {
                    max = mid;
                }
                else
                {
                    min = mid;
                }
            }

            return sortedArray[max];
        }
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM