1,對於待存儲的海量數據,如何將它們分配到各個機器中去?---數據分片與路由
當數據量很大時,通過改善單機硬件資源的縱向擴充方式來存儲數據變得越來越不適用,而通過增加機器數目來獲得水平橫向擴展的方式則越來越流行。因此,就有個問題,如何將這些海量的數據分配到各個機器中?數據分布到各個機器存儲之后,又如何進行查找?這里主要記錄一致性Hash算法如何將數據分配到各個機器中去。
2,衡量一致性哈希算法好處的四個標准:
①平衡性:平衡性是指哈希的結果能夠盡可能分布到所有的緩沖中去,這樣可以使得所有的緩沖空間都得到利用。②單調性:單調性是指如果已經有一些數據通過哈希分配到了相應的機器上,又有新的機器加入到系統中。哈希的結果應能夠保證原有已分配的內容可以被映射到原有的或者新的機器中去,而不會被映射到舊的機器集合中的其他機器上。
這里再解釋一下:就是原有的數據要么還是呆在它所在的機器上不動,要么被遷移到新的機器上,而不會遷移到舊的其他機器上。
③分散性:④負載:參考這里
3,一致性哈希的原理:
由於一般的哈希函數返回一個int(32bit)型的hashCode。因此,可以將該哈希函數能夠返回的hashCode表示成一個范圍為0---(2^32)-1 環。
將機器的標識(如:IP地址)作為哈希函數的Key映射到環上。如:
hash(Node1) =Key1 hash(Node2) = Key2,借用一張圖如下:
同樣,數據也通過相同的哈希函數映射到環上。這樣,按照順時針方向,數據存放在它所在的順時針方向上的那個機器上。這就是一致性哈希算法分配數據的方式!
4,JAVA實現一致性哈希算法的代碼分析:
❶設計哈希函數
這里采用了MD5算法,主要是用來保證平衡性,即能夠將機器均衡地映射到環上。貌似用Jdk中String類的hashCode並不能很好的保證平衡性。
1 import java.security.MessageDigest; 2 import java.security.NoSuchAlgorithmException; 3 4 /* 5 * 實現一致性哈希算法中使用的哈希函數,使用MD5算法來保證一致性哈希的平衡性 6 */ 7 public class HashFunction { 8 private MessageDigest md5 = null; 9 10 public long hash(String key) { 11 if (md5 == null) { 12 try { 13 md5 = MessageDigest.getInstance("MD5"); 14 } catch (NoSuchAlgorithmException e) { 15 throw new IllegalStateException("no md5 algrithm found"); 16 } 17 } 18 19 md5.reset(); 20 md5.update(key.getBytes()); 21 byte[] bKey = md5.digest(); 22 //具體的哈希函數實現細節--每個字節 & 0xFF 再移位 23 long result = ((long) (bKey[3] & 0xFF) << 24) 24 | ((long) (bKey[2] & 0xFF) << 16 25 | ((long) (bKey[1] & 0xFF) << 8) | (long) (bKey[0] & 0xFF)); 26 return result & 0xffffffffL; 27 } 28 }
❷實現一致性哈希算法:
為什么要引入虛擬機器節點?它的作用是什么?
引入虛擬機器節點,其目的就是為了解決數據分配不均衡的問題。因為,在將實際的物理機器映射到環上時,有可能大部分機器都映射到環上的某一個部分(比如左半圓上),而通過引入虛擬機器節點,在進行機器hash映射時,不是映射具體機器,而是映射虛擬機器,並保證虛擬機器對應的物理機器是均衡的---每台實際的機器對應着相等數目的Virtual nodes。如下圖:
如何解決集群中添加或者刪除機器上需要遷移大量數據的問題?
假設采用傳統的哈希取模法,設有K台物理機,H(key)=hash(key) mod K 即可實現數據分片。但當K變化時(如新增一台機器),所有已經映射好的數據都需要重新再映射。H(key)=hash(key) mod (K+1)。
一致性哈希采用的做法如下:引入一個環的概念,如上面的第一個圖。先將機器映射到這個環上,再將數據也通過相同的哈希函數映射到這個環上,數據存儲在它順時針走向的那台機器上。以環為中介,實現了數據與機器數目之間的解藕。這樣,當機器的數目變化時,只會影響到增加或刪除的那台機器所在的環的鄰接機器的數據存儲,而其他機器上的數據不受影響。(參考這篇文章:https://www.cnblogs.com/hapjin/p/5760463.html)
在具體JAVA實現代碼中,定義了一個TreeMap<k, V>用來保存虛擬機器節點到實際的物理機器的映射。機器以字符串形式來標識,故hash函數的參數為String
1 for (int i = 0; i < numberOfReplicas; i++) 2 // 對於一個實際機器節點 node, 對應 numberOfReplicas 個虛擬節點 3 /* 4 * 不同的虛擬節點(i不同)有不同的hash值,但都對應同一個實際機器node 5 * 虛擬node一般是均衡分布在環上的,數據存儲在順時針方向的虛擬node上 6 */ 7 circle.put(hashFunction.hash(node.toString() + i), node);
而對於 數據的存儲而言,邏輯上是按順時針方向存儲在虛擬機器節點中,虛擬機器節點通過TreeMap知道它實際需要將數據存儲在哪台物理機器上。此外,TreeMap中的Key是有序的,而環也是順時針有序的,這樣才能當數據被映射到兩台虛擬機器之間的弧上時,通過TreeMap的 tailMap()來尋找順時針方向上的下一台虛擬機。
1 if (!circle.containsKey(hash)) {//數據映射在兩台虛擬機器所在環之間,就需要按順時針方向尋找機器 2 SortedMap<Long, T> tailMap = circle.tailMap(hash); 3 hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); 4 }
完整代碼:
1 import java.util.Collection; 2 import java.util.HashSet; 3 import java.util.Iterator; 4 import java.util.Set; 5 import java.util.SortedMap; 6 import java.util.SortedSet; 7 import java.util.TreeMap; 8 import java.util.TreeSet; 9 10 public class ConsistentHash<T> { 11 private final HashFunction hashFunction; 12 private final int numberOfReplicas;// 節點的復制因子,實際節點個數 * numberOfReplicas = 13 // 虛擬節點個數 14 private final SortedMap<Long, T> circle = new TreeMap<Long, T>();// 存儲虛擬節點的hash值到真實節點的映射 15 16 public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, 17 Collection<T> nodes) { 18 this.hashFunction = hashFunction; 19 this.numberOfReplicas = numberOfReplicas; 20 for (T node : nodes) 21 add(node); 22 } 23 24 public void add(T node) { 25 for (int i = 0; i < numberOfReplicas; i++) 26 // 對於一個實際機器節點 node, 對應 numberOfReplicas 個虛擬節點 27 /* 28 * 不同的虛擬節點(i不同)有不同的hash值,但都對應同一個實際機器node 29 * 虛擬node一般是均衡分布在環上的,數據存儲在順時針方向的虛擬node上 30 */ 31 circle.put(hashFunction.hash(node.toString() + i), node); 32 } 33 34 public void remove(T node) { 35 for (int i = 0; i < numberOfReplicas; i++) 36 circle.remove(hashFunction.hash(node.toString() + i)); 37 } 38 39 /* 40 * 獲得一個最近的順時針節點,根據給定的key 取Hash 41 * 然后再取得順時針方向上最近的一個虛擬節點對應的實際節點 42 * 再從實際節點中取得 數據 43 */ 44 public T get(Object key) { 45 if (circle.isEmpty()) 46 return null; 47 long hash = hashFunction.hash((String) key);// node 用String來表示,獲得node在哈希環中的hashCode 48 if (!circle.containsKey(hash)) {//數據映射在兩台虛擬機器所在環之間,就需要按順時針方向尋找機器 49 SortedMap<Long, T> tailMap = circle.tailMap(hash); 50 hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); 51 } 52 return circle.get(hash); 53 } 54 55 public long getSize() { 56 return circle.size(); 57 } 58 59 /* 60 * 查看MD5算法生成的hashCode值---表示整個哈希環中各個虛擬節點位置 61 */ 62 public void testBalance(){ 63 Set<Long> sets = circle.keySet();//獲得TreeMap中所有的Key 64 SortedSet<Long> sortedSets= new TreeSet<Long>(sets);//將獲得的Key集合排序 65 for(Long hashCode : sortedSets){ 66 System.out.println(hashCode); 67 } 68 69 System.out.println("----each location 's distance are follows: ----"); 70 /* 71 * 查看用MD5算法生成的long hashCode 相鄰兩個hashCode的差值 72 */ 73 Iterator<Long> it = sortedSets.iterator(); 74 Iterator<Long> it2 = sortedSets.iterator(); 75 if(it2.hasNext()) 76 it2.next(); 77 long keyPre, keyAfter; 78 while(it.hasNext() && it2.hasNext()){ 79 keyPre = it.next(); 80 keyAfter = it2.next(); 81 System.out.println(keyAfter - keyPre); 82 } 83 } 84 85 public static void main(String[] args) { 86 Set<String> nodes = new HashSet<String>(); 87 nodes.add("A"); 88 nodes.add("B"); 89 nodes.add("C"); 90 91 ConsistentHash<String> consistentHash = new ConsistentHash<String>(new HashFunction(), 2, nodes); 92 consistentHash.add("D"); 93 94 System.out.println("hash circle size: " + consistentHash.getSize()); 95 System.out.println("location of each node are follows: "); 96 consistentHash.testBalance(); 97 } 98 99 }
參考資料:
五分鍾理解一致性哈希算法(consistent hashing)
一致性hash算法 - consistent hashing