memcached緩存分布式部署方案


一、分布式方案介紹

比較流行的兩種方案:

1.取余分布:

計算key的哈希值,與服務器數量取余,得到目標服務器。優點:實現簡單,當某台服務器不可用時,故障轉移方便;缺點:當增減服務器時, Key與服務器取余變動量較大,緩存重組代價極大。

代碼實現可參考開源組件Memcached.ClientLibrary下的SockIOPool,源碼地址:

https://sourceforge.net/p/memcacheddotnet/code/HEAD/tree/trunk/clientlib/src/clientlib/SockIOPool.cs

2.一致性哈希環分布:

其原理參考

https://www.cnblogs.com/lpfuture/p/5796398.html

http://www.zsythink.net/archives/1182

這兩位老哥寫的很清楚和直白,很容易理解。

一致性哈希環分布需要物理節點和虛擬節點,且虛擬節點對應到物理節點的服務器上。

二、代碼實現

由於Memcached.ClientLibrary的作者已出取余分布的實現,這里不再敘述,以下代碼和測試均是一致性哈希分布的。

1.數據結構:

服務器列表:List<string> servers;//IP:PORT

服務器虛擬節點數:List<int> weights;//與servers一一對應,靈活設置每server的不同虛擬節點數

節點存儲結構:SortedDictionary<long, String> buckets;

Key:long類型,存儲節點的hash%2^32;

Value:String類型,存儲節點,即IP:PORT;

2.代碼

計算哈希值算法,參考Memcached.ClientLibrary下的SockIOPool:

https://sourceforge.net/p/memcacheddotnet/code/HEAD/tree/trunk/clientlib/src/clientlib/SockIOPool.cs

        private int CalculateHashValue(String key)

        {

            int hv;

            switch (_hashingAlgorithm)

            {

                case EnumHashingAlgorithm.Native:

                    hv = key.GetHashCode();

                    break;

 

                case EnumHashingAlgorithm.OldCompatibleHash:

                    hv = HashingAlgorithmTool.OriginalHashingAlgorithm(key);

                    break;

 

                case EnumHashingAlgorithm.NewCompatibleHash:

                    hv = HashingAlgorithmTool.NewHashingAlgorithm(key);

                    break;

 

                default:

                    // use the native hash as a default

                    hv = key.GetHashCode();

                    _hashingAlgorithm = EnumHashingAlgorithm.Native;

                    break;

            }

            return hv;

        }

 

經過測試,OldCompatibleHash方式計算的哈希值比較散列。

//哈希取余值,為什么是2的32次方:IPV4的總量是2的32次方個,可以保證環上的IP不重復
long HashValue = (long)Math.Pow(2, 32);
將Key生成一致性哈希環中的哈希值
        private long GenerateConsistentHashValue(String key)
        {
            long serverHV = CalculateHashValue(key);
            long mod = serverHV % HashValue;
            if (mod < 0)
            {
                mod = mod + HashValue;
            }
            return mod;
        }

將Servers生成節點(物理+虛擬):

        private void GenerateServersToBuckets()
        {
            for (int i = 0; i < _servers.Count; i++)
            {
                // 創建物理節點
                String server = _servers[i];
                long mod = GenerateConsistentHashValue(server);
                buckets.Add(mod, server);
                //創建虛擬節點
                List<String> virtualHostServers = GenerateVirtualServer(server, this.Weights[i]);
                foreach (String v in virtualHostServers)
                {
                    mod = GenerateConsistentHashValue(v);
                    buckets.Add(mod, server);
                }
            }
        }

根據物理節點生成虛擬節點

private static List<String> GenerateVirtualServer(String server, int count)
        {
            if (count > 255)
            {
                throw new ArgumentException("每服務器虛擬節點數不能超過254");
            }
            List<String> virtualServers = new List<string>();

            #region 1.按修改IP值+隨機GUID生成虛擬節點
            String[] ipaddr = server.Split(':');
            String ip = ipaddr[0];
            string port = ipaddr[1];
            int header = Convert.ToInt32(ip.Split('.')[0]);
            String invariantIPPart = ip.Substring(ip.IndexOf('.'));
            int succ = 0;
            for (int i = 1; i <= 255; i++)
            {
                if (i != header)
                {
                    String virtualServer = i.ToString() + invariantIPPart + ":" + port + i;// Guid.NewGuid().ToString("N").ToUpper();
                    virtualServers.Add(virtualServer);
                    succ++;
                }
                if (succ == count)
                {
                    break;
                }
            } 
            #endregion

            #region 2.物理節點自增序號||隨機GUID
            //for (int i = 0; i < count; i++)
            //{
            //    //virtualServers.Add(server + i.ToString());
            //    virtualServers.Add(server + i.ToString()+Guid.NewGuid().ToString());
            //} 
            #endregion

            #region 32.其它生成算法
            //TODO
            #endregion

            return virtualServers;
        }

 

三、節點分布測試

四台服務器:{ "192.168.1.100:11211", "192.168.1.101:11211", "192.168.1.102:11211", "192.168.1.103:11211" }

哈希算法不同,則節點分布規則不同

1.物理節點分布

2.每物理節點10虛擬節點

節點分布測試結果:

節點數共有(物理4+虛擬4*10):44

在第一個節點和第二個節點間:

服務器A的虛擬節點數:1    占比:10%

服務器B的虛擬節點數:1    占比:10%

服務器C的虛擬節點數:0    占比:0%

服務器D的虛擬節點數:2    占比:20%

在第二個節點和第三個節點間:

服務器A的虛擬節點數:0    占比:0%

服務器B的虛擬節點數:0    占比:0%

服務器C的虛擬節點數:2    占比:20%

服務器D的虛擬節點數:2    占比:20%

在第三個節點和第四個節點間:

服務器A的虛擬節點數:4    占比:40%

服務器B的虛擬節點數:5    占比:50%

服務器C的虛擬節點數:4    占比:40%

服務器D的虛擬節點數:4    占比:40%

在第四個節點和第一個節點間:

服務器A的虛擬節點數:5    占比:50%

服務器B的虛擬節點數:4    占比:40%

服務器C的虛擬節點數:4    占比:40%

服務器D的虛擬節點數:2    占比:20%

 3.每物理節點30虛擬節點

節點分布測試結果:

節點數共有(物理4+虛擬4*30):124

在第一個節點和第二個節點間:

服務器A的虛擬節點數:7    占比:23%

服務器B的虛擬節點數:7    占比:23%

服務器C的虛擬節點數:6    占比:20%

服務器D的虛擬節點數:7    占比:23%

在第二個節點和第三個節點間:

服務器A的虛擬節點數:3    占比:10%

服務器B的虛擬節點數:1    占比:3%

服務器C的虛擬節點數:4    占比:13%

服務器D的虛擬節點數:4    占比:13%

在第三個節點和第四個節點間:

服務器A的虛擬節點數:11    占比:36%

服務器B的虛擬節點數:11    占比:36%

服務器C的虛擬節點數:10    占比:33%

服務器D的虛擬節點數:10    占比:33%

在第四個節點和第一個節點間:

服務器A的虛擬節點數:9    占比:30%

服務器B的虛擬節點數:11    占比:36%

服務器C的虛擬節點數:10    占比:33%

服務器D的虛擬節點數:9    占比:30%

4. 每物理節點50虛擬節點:.

 

 

節點分布測試結果:

節點數共有(物理4+虛擬4*50):204

在第一個節點和第二個節點間:

服務器A的虛擬節點數:14    占比:28%

服務器B的虛擬節點數:13    占比:26%

服務器C的虛擬節點數:12    占比:24%

服務器D的虛擬節點數:13    占比:26%

在第二個節點和第三個節點間:

服務器A的虛擬節點數:4    占比:8%

服務器B的虛擬節點數:3    占比:6%

服務器C的虛擬節點數:5    占比:10%

服務器D的虛擬節點數:7    占比:14%

在第三個節點和第四個節點間:

服務器A的虛擬節點數:17    占比:34%

服務器B的虛擬節點數:18    占比:36%

服務器C的虛擬節點數:16    占比:32%

服務器D的虛擬節點數:16    占比:32%

在第四個節點和第一個節點間:

服務器A的虛擬節點數:15    占比:30%

服務器B的虛擬節點數:16    占比:32%

服務器C的虛擬節點數:17    占比:34%

服務器D的虛擬節點數:14    占比:28%

5. 每物理節點80虛擬節點

 

 

節點分布測試結果:

節點數共有(物理4+虛擬4*80):324

在第一個節點和第二個節點間:

服務器A的虛擬節點數:22    占比:27%

服務器B的虛擬節點數:23    占比:28%

服務器C的虛擬節點數:21    占比:26%

服務器D的虛擬節點數:22    占比:27%

在第二個節點和第三個節點間:

服務器A的虛擬節點數:7    占比:8%

服務器B的虛擬節點數:5    占比:6%

服務器C的虛擬節點數:9    占比:11%

服務器D的虛擬節點數:10    占比:12%

在第三個節點和第四個節點間:

服務器A的虛擬節點數:27    占比:33%

服務器B的虛擬節點數:27    占比:33%

服務器C的虛擬節點數:25    占比:31%

服務器D的虛擬節點數:24    占比:30%

在第四個節點和第一個節點間:

服務器A的虛擬節點數:24    占比:30%

服務器B的虛擬節點數:25    占比:31%

服務器C的虛擬節點數:25    占比:31%

服務器D的虛擬節點數:24    占比:30%

6. 每物理節點100虛擬節點

 

 

節點分布測試結果:

節點數共有(物理4+虛擬4*100):404

在第一個節點和第二個節點間:

服務器A的虛擬節點數:29    占比:29%

服務器B的虛擬節點數:30    占比:30%

服務器C的虛擬節點數:28    占比:28%

服務器D的虛擬節點數:28    占比:28%

在第二個節點和第三個節點間:

服務器A的虛擬節點數:8    占比:8%

服務器B的虛擬節點數:8    占比:8%

服務器C的虛擬節點數:11    占比:11%

服務器D的虛擬節點數:12    占比:12%

在第三個節點和第四個節點間:

服務器A的虛擬節點數:33    占比:33%

服務器B的虛擬節點數:32    占比:32%

服務器C的虛擬節點數:30    占比:30%

服務器D的虛擬節點數:30    占比:30%

在第四個節點和第一個節點間:

服務器A的虛擬節點數:30    占比:30%

服務器B的虛擬節點數:30    占比:30%

服務器C的虛擬節點數:31    占比:31%

服務器D的虛擬節點數:30    占比:30%

說明:由於統計計算時按int取值,服務器虛擬節點比率總和可能有1的誤差。

總結:以上可以看出當總節點在300以上時,各物理節點之間的虛擬節點所占比率變化較小,說明分布比較均勻。

四、存取數據查找服務器

原理:根據數據的Key與HashValue取余值hv,查找buckets中Key>=hv的第一個服務器,即是Key的目標服務器,當返回的服務器不可用時,還可以進行故障轉移

1.從節點環中查找服務器

private String FindServer(String key, ref long startIndex)
        {
            long mod = startIndex;
            if (mod < 0)
            {
                mod = GenerateConsistentHashValue(key);
            }
            foreach (KeyValuePair<long, String> kvp in buckets)
            {
                startIndex = kvp.Key;
                //找到第一個大於或等於key的服務器
                if (kvp.Key >= mod)
                {
                    //若找到的服務器不可用,則繼續查找下一服務器
                    if (_hostDead.ContainsKey(kvp.Value))
                    {
                        continue;
                    }
                    return kvp.Value;
                }
            }
            //如果大於mod的服務器均不可用或沒有找到,則從頭開始找可用服務器
            foreach (KeyValuePair<long, String> kvp in buckets)
            {
                startIndex = kvp.Key;
                if (kvp.Key >= mod)
                {
                    break;
                }
                if (_hostDead.ContainsKey(kvp.Value))
                {
                    continue;
                }
                return kvp.Value;
            }
            //不存在可用服務器
            return string.Empty;
        }

2.獲取服務器及連接

 public ISockIO GetSock(string key)
        {
            if (buckets.Count == 0)
            {
                return null;
            }

            if (buckets.Count == 1)
            {
                return GetConnection(buckets[0]);
            }

            long startIndex = -1;//開始查找位置,-1表示從hash(key)% HashValue位置開始查找
            int tries = 0;//重試次數,不超過總服務器數
            while (tries++ <= this.servers.Count)
            {
                String server = FindServer(key, ref startIndex);
                //找不到可用的服務器
                if (String.IsNullOrEmpty(server))
                {
                    return null;
                }
                ISockIO sock = GetConnection(server);
                if (sock != null)
                {
                    WriteLog.Write("key:" + key + ",server:" + server);
                    return sock;
                }
                //是否需要故障轉移,若需要,則會繼續查找可用的服務器
                if (!_failover)
                {
                    return null;
                }
            }
            return null;
        }

 


免責聲明!

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



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