一、分布式方案介紹
比較流行的兩種方案:
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; }