日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Hash一致性算法(分片机制)

發布時間:2024/1/8 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Hash一致性算法(分片机制) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一 哈希簡介
1.1 簡介
我們首先來簡單介紹一下什么是哈希(以下簡稱hash),hash本質來說就是映射,或者說是鍵值對key-value,不同的hash之間不過就是實現key-value映射的算法不同,例如java中計算對象的hashcode值會有不同的算法,常用于各種分布式存儲分片的id取模算法等,都屬于hash算法。
分布式系統中,假設有 n 個節點,傳統方案使用 mod(key, n) 映射數據和節點。
當擴容或縮容時(哪怕只是增減1個節點),映射關系變為 mod(key, n+1) / mod(key, n-1),絕大多數數據的映射關系都會失效。

1.2算法原理:
映射方案

1.2.1公用哈希函數和哈希環
設計哈希函數 Hash(key),要求取值范圍為 [0, 2^32)
各哈希值在上圖 Hash 環上的分布:時鐘12點位置為0,按順時針方向遞增,臨近12點的左側位置為2^32-1。

1.2.2 節點(Node)映射至哈希環
如圖哈希環上的綠球所示,四個節點 Node A/B/C/D,
其 IP 地址或機器名,經過同一個 Hash() 計算的結果,映射到哈希環上。

1.2.3 對象(Object)映射于哈希環
如圖哈希環上的黃球所示,四個對象 Object A/B/C/D,
其鍵值,經過同一個 Hash() 計算的結果,映射到哈希環上。

1.2.4 對象(Object)映射至節點(Node)
在對象和節點都映射至同一個哈希環之后,要確定某個對象映射至哪個節點,
只需從該對象開始,沿著哈希環順時針方向查找,找到的第一個節點,即是。
可見,Object A/B/C/D 分別映射至 Node A/B/C/D。
刪除節點
現實場景:服務器縮容時刪除節點,或者有節點宕機。如下圖,要刪除節點 Node C:
只會影響欲刪除節點(Node C)與上一個(順時針為前進方向)節點(Node B)與之間的對象,也就是 Object C,
這些對象的映射關系,按照 2.1.4 的規則,調整映射至欲刪除節點的下一個節點 Node D。
其他對象的映射關系,都無需調整。


增加節點
現實場景:服務器擴容時增加節點。比如要在 Node B/C 之間增加節點 Node X:
只會影響欲新增節點(Node X)與上一個(順時針為前進方向)節點(Node B)與之間的對象,也就是 Object C,
這些對象的映射關系,按照 2.1.4 的規則,調整映射至新增的節點 Node X。
其他對象的映射關系,都無需調整。

虛擬節點
對于前面的方案,節點數越少,越容易出現節點在哈希環上的分布不均勻,導致各節點映射的對象數量嚴重不均衡(數據傾斜);相反,節點數越多越密集,數據在哈希環上的分布就越均勻。
但實際部署的物理節點有限,我們可以用有限的物理節點,虛擬出足夠多的虛擬節點(Virtual Node),最終達到數據在哈希環上均勻分布的效果:
如下圖,實際只部署了2個節點 Node A/B,
每個節點都復制成3倍,結果看上去是部署了6個節點。
可以想象,當復制倍數為 2^32 時,就達到絕對的均勻,通常可取復制倍數為32或更高。
虛擬節點哈希值的計算方法調整為:對“節點的IP(或機器名)+虛擬節點的序號(1~N)”作哈希。
算法實現
一致性哈希算法有多種具體的實現,包括 Chord 算法,KAD 算法等,都比較復雜。
這里給出一個簡易實現及其演示,可以看到一致性哈希的均衡性和單調性的優勢。
單調性在本例中沒有統計數據,但根據前面原理可知,增刪節點后只有很少量的數據需要調整映射關系。

3.1 源碼
public class ConsistentHashing {
// 物理節點
private Set physicalNodes = new TreeSet() {
{
add(“192.168.1.101”);
add(“192.168.1.102”);
add(“192.168.1.103”);
add(“192.168.1.104”);
}
};

//虛擬節點 private final int VIRTUAL_COPIES = 1048576; // 物理節點至虛擬節點的復制倍數 private TreeMap<Long, String> virtualNodes = new TreeMap<>(); // 哈希值 => 物理節點// 32位的 Fowler-Noll-Vo 哈希算法 // https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function private static Long FNVHash(String key) {final int p = 16777619;Long hash = 2166136261L;for (int idx = 0, num = key.length(); idx < num; ++idx) {hash = (hash ^ key.charAt(idx)) * p;}hash += hash << 13;hash ^= hash >> 7;hash += hash << 3;hash ^= hash >> 17;hash += hash << 5;if (hash < 0) {hash = Math.abs(hash);}return hash; }// 根據物理節點,構建虛擬節點映射表 public ConsistentHashing() {for (String nodeIp : physicalNodes) {addPhysicalNode(nodeIp);} }// 添加物理節點 public void addPhysicalNode(String nodeIp) {for (int idx = 0; idx < VIRTUAL_COPIES; ++idx) {long hash = FNVHash(nodeIp + "#" + idx);virtualNodes.put(hash, nodeIp);} }// 刪除物理節點 public void removePhysicalNode(String nodeIp) {for (int idx = 0; idx < VIRTUAL_COPIES; ++idx) {long hash = FNVHash(nodeIp + "#" + idx);virtualNodes.remove(hash);} }// 查找對象映射的節點 public String getObjectNode(String object) {long hash = FNVHash(object);SortedMap<Long, String> tailMap = virtualNodes.tailMap(hash); // 所有大于 hash 的節點Long key = tailMap.isEmpty() ? virtualNodes.firstKey() : tailMap.firstKey();return virtualNodes.get(key); }// 統計對象與節點的映射關系 public void dumpObjectNodeMap(String label, int objectMin, int objectMax) {// 統計Map<String, Integer> objectNodeMap = new TreeMap<>(); // IP => COUNTfor (int object = objectMin; object <= objectMax; ++object) {String nodeIp = getObjectNode(Integer.toString(object));Integer count = objectNodeMap.get(nodeIp);objectNodeMap.put(nodeIp, (count == null ? 0 : count + 1));}// 打印double totalCount = objectMax - objectMin + 1;System.out.println("======== " + label + " ========");for (Map.Entry<String, Integer> entry : objectNodeMap.entrySet()) {long percent = (int) (100 * entry.getValue() / totalCount);System.out.println("IP=" + entry.getKey() + ": RATE=" + percent + "%");} }public static void main(String[] args) {ConsistentHashing ch = new ConsistentHashing();// 初始情況ch.dumpObjectNodeMap("初始情況", 0, 65536);// 刪除物理節點ch.removePhysicalNode("192.168.1.103");ch.dumpObjectNodeMap("刪除物理節點", 0, 65536);// 添加物理節點ch.addPhysicalNode("192.168.1.108");ch.dumpObjectNodeMap("添加物理節點", 0, 65536); }

}

3.2 復制倍數為 1 時的均衡性
修改代碼中 VIRTUAL_COPIES = 1(相當于沒有虛擬節點),運行結果如下(可見各節點負荷很不均衡):

======== 初始情況 ========
IP=192.168.1.101: RATE=45%
IP=192.168.1.102: RATE=3%
IP=192.168.1.103: RATE=28%
IP=192.168.1.104: RATE=22%
======== 刪除物理節點 ========
IP=192.168.1.101: RATE=45%
IP=192.168.1.102: RATE=3%
IP=192.168.1.104: RATE=51%
======== 添加物理節點 ========
IP=192.168.1.101: RATE=45%
IP=192.168.1.102: RATE=3%
IP=192.168.1.104: RATE=32%
IP=192.168.1.108: RATE=18%
3.2 復制倍數為 32 時的均衡性
修改代碼中 VIRTUAL_COPIES = 32,運行結果如下(可見各節點負荷比較均衡):

======== 初始情況 ========
IP=192.168.1.101: RATE=29%
IP=192.168.1.102: RATE=21%
IP=192.168.1.103: RATE=25%
IP=192.168.1.104: RATE=23%
======== 刪除物理節點 ========
IP=192.168.1.101: RATE=39%
IP=192.168.1.102: RATE=37%
IP=192.168.1.104: RATE=23%
======== 添加物理節點 ========
IP=192.168.1.101: RATE=35%
IP=192.168.1.102: RATE=20%
IP=192.168.1.104: RATE=23%
IP=192.168.1.108: RATE=20%

3.2 復制倍數為 1M 時的均衡性
修改代碼中 VIRTUAL_COPIES = 1048576,運行結果如下(可見各節點負荷非常均衡):

======== 初始情況 ========
IP=192.168.1.101: RATE=24%
IP=192.168.1.102: RATE=24%
IP=192.168.1.103: RATE=25%
IP=192.168.1.104: RATE=25%
======== 刪除物理節點 ========
IP=192.168.1.101: RATE=33%
IP=192.168.1.102: RATE=33%
IP=192.168.1.104: RATE=33%
======== 添加物理節點 ========
IP=192.168.1.101: RATE=25%
IP=192.168.1.102: RATE=24%
IP=192.168.1.104: RATE=24%
IP=192.168.1.108: RATE=24%

二 面臨的問題
一個算法的出現一定是為了解決某個問題或者是某類問題,理解算法解決了什么樣的問題非常有助于我們理解算法本身,那么一致性哈希是為了解決什么樣的問題呢?我們首先來看一下普通的hash算法會遇到什么樣的問題,我們以id取模算法為例,這種算法經常被用到分布式存儲的分片算法中:

如圖所示,假如我們以id % 3作為分片條件,有1-20這些元素,這樣這20個元素會按照與3取模的結果分布在0、1、2這三個片中,一切看起來都簡單又和諧,但隨著業務的發展,我們可能需要擴容,需要再加一個片,我們需要把算法換成id%4,這個時候會發生什么樣的變化呢?

對比兩個圖,我們發現,擴容了一個分片之后,百分之七八十的的數據都發生了遷移,大規模的數據遷移就是這個算法的缺點所在。如果是我們示例中的這種小規模數據,可能影響還不是很大,但是在企業級應用中,可能需要操作的是十億百億規模的數據,這時候要遷移它們當中百分之七八十的數據,復雜度和危險性都是非常高的。

有一種方法能夠減小數據遷移的規模,就是成倍擴容,例如示例中的3個片我們直接擴容成6個片,這樣可以將數據遷移的規模減小到50%,如果讀者閱讀過HashMap的源碼,會發現,HashMap在擴容時調用的resize方法就是將容量擴容為原來的2倍,筆者當時在閱讀HashMap源碼時就沒搞懂為什么一定要擴容兩倍,原因就是在這了,就是為了減少數據遷移的規模。

但是這種方式又會引入另外兩個問題,一個是資源浪費,可能我們的業務發展和體量暫時不需要擴容一倍,所以直接擴容一倍之后會造成一定的資源浪費。另一個是成本問題,擴容意味著增加服務器,成倍擴容無疑意味著需要更多的服務器,成本還是很高的。這兩個問題在大規模集群中尤為明顯。
一致性哈希
下面我們來看一致性哈希是如何解決這些問題的,首先我們來看網上經常能看到的有關一致性哈希的一張圖:

idmod = id % 100;
if (idmod >= 0 && idmod < 25) {
return db1;
} else if (idmod >= 25 && idmod < 50) {
return db2;
} else if (idmod >= 50 && idmod < 75) {
return db3;
} else {
return db4;
}
我們用id % 100的結果作為分片的依據,并將集群分為四個片,每個片對應一段區間,這時,假如我們發現db3對應的區間也就是idmod在50-75之間的數據發生了熱點情況,我們需要對這個片進行擴容,那我們可以將算法改造成這樣:

idmod = id % 100;
if (idmod >= 0 && idmod < 25) {
return db1;
} else if (idmod >= 25 && idmod < 50) {
return db2;
} else if (idmod >= 50 && idmod < 65) {
return db3;
} else if (idmod >= 65 && idmod < 75) {
return db5;
} else {
return db4;
}
我們擴容了一個分片db5,并將原來的熱點分片db3中的一部分區間中的數據遷移到db5中,這樣就可以在不影響其他分片的情況下完成數據遷移,擴容的節點數量也可以進行控制,這就是一致性哈希。

我們再回頭看一下上面這樣圖,大概的意思就是這樣,原本有四個節點node1-node4,圖中粉色的點就是我們id取模之后的值落到這個環上的位置,這就是所謂的哈希環,然后擴容了一個藍色的節點node5,擴容只會影響原來node2-node4之間的數據。

一致性哈希也同樣有它的問題所在,我們上面提到,一致性哈希可以解決熱點問題,那如果我們的數據分布的很均勻,沒有熱點問題,還是需要擴容,怎么辦,按照上文的理解就需要為每個節點都擴容一個節點,這不又是成倍的擴容了么,又遇到了這個n -> 2n的問題,該怎么解決呢?

虛擬節點
我們來做這樣一個映射,首先將id % 65536,這樣可以得到0-65535這樣一個區間,然后做一個這樣的映射:

hash id node id
0 0
1 1
2 2
3 3
4 0
5 1
… …
65535 3
這個時候如果我們需要擴容節點,增加一個節點node id為4,我們只需要調整這張虛擬節點的映射表,隨意的按照我們的需求來調整,比如我們可以將hash id為5、6、7的數據映射到node id為4的節點上,所以虛擬節點的關鍵就是我們要維護好這張映射表。這里id與多少取模選擇了65536,實際應用中取多少合適呢?很顯然,這個值越大,分布就會越均勻,我們可以調整的空間也越大,但是實現和維護的難度也會上升,所以實際應用中到底應該取什么值還是需要結合實際業務來做出權衡。

總結
無論是哪種算法,它們要解決的問題都是盡量的減少數據遷移的規模,還有就是減少擴容的成本,那是不是說我們就一定要選擇虛擬節點的這種算法呢?恰恰相反,我們推薦盡量使用的簡單的方法來解決問題,不要一開始就使用復雜的方式,這樣很容易產生過度設計,虛擬節點的算法雖然可以解決n -> 2n和數據遷移規模的問題,但它的缺點就是比較復雜,實現復雜,維護也復雜,所以我們推薦應用一開始盡量優先選用id取模的算法也就是n -> 2n的方式進行擴容,當集群到達一定規模之后,我們可以做一張如上的虛擬節點映射表,將原來的取模算法平滑的切換為虛擬節點算法,對應用沒有任何影響,然后再按照虛擬節點的方式進行擴容,這是我們最推薦的方式。

總結

以上是生活随笔為你收集整理的Hash一致性算法(分片机制)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。