1.簡介
1.1 普通hash算法
普通hash算法是通過key將數據映射到具體節點上,如key%N,key爲數據的hash值,N爲節點數量,如果有機器加入或者退出集羣,則key映射失效了,導致數據丟失。
1.2 一致性hash算法
相比普通hash算法,一致性hash就可以解決這種問題。一致性hash是分佈式系統常用的一種算法,常用於負載均衡。
2.原理分析
2.1 環形Hash空間
一致性hash算法,將value映射爲一個32的key(常用MD5函數),即key分配到一個0~2^32次方的空間裏。我們可以想象這些數字頭尾相連,形成一個閉合的環形。如下圖:
2.2 將數據映射到環形空間上
現在我們假設有4個對象,object1,object2,object3,object4,我們按照一致性Hash算法,將它們的key分散到環形空間上。
Hash(Object1) = key1
Hash(Object2) = key2
Hash(Object3) = key3
Hash(Object4) = key4
如下圖
2.2 將機器映射到環形空間上
在採用一致性哈希算法的分佈式集羣中將新的機器加入,其原理是通過使用與對象存儲一樣的Hash算法將機器也映射到環中(一般情況下對機器的hash計算是採用機器的IP或者機器唯一的別名作爲輸入值),然後以順時針的方向計算,將所有對象存儲到離自己最近的機器中。
假設現在有node1,node2,node3三臺機器,通過Hash算法得到對應的key值,映射到環中,其示意圖如下:
Hash(node1) = key1
Hash(node2) = key2
Hash(node3) = key3
如下圖
通過上圖可以看出對象與機器處於同一哈希空間中,這樣按順時針轉動object1存儲到了NODE1中,object3存儲到了NODE2中,object2、object4存儲到了NODE3中。在這樣的部署環境中,hash環是不會變更的,因此,通過算出對象的hash值就能快速的定位到對應的機器中,這樣就能找到對象真正的存儲位置了。
2.3 節點的加入和刪除
2.3.1 節點的加入
如果往集羣中添加一個新的節點NODE4,通過對應的哈希算法得到KEY4,並映射到環中,如下圖:
通過按順時針遷移的規則,那麼object2被遷移到了NODE4中,其它對象還保持這原有的存儲位置。通過對節點的添加和刪除的分析,一致性哈希算法在保持了單調性的同時,還是數據的遷移達到了最小,這樣的算法對分佈式集羣來說是非常合適的,避免了大量數據遷移,減小了服務器的的壓力。
2.3.2 節點的刪除
以上面的分佈爲例,如果NODE2出現故障被刪除了,那麼按照順時針遷移的方法,object3將會被遷移到NODE3中,這樣僅僅是object3的映射位置發生了變化,其它的對象沒有任何的改動。如下圖:
2.3.3 平衡性
根據上面的圖解分析,一致性哈希算法滿足了單調性和負載均衡的特性以及一般hash算法的分散性,但這還並不能當做其被廣泛應用的原由,因爲還缺少了平衡性。下面將分析一致性哈希算法是如何滿足平衡性的。hash算法是不保證平衡的,如上面只部署了NODE1和NODE3的情況(NODE2被刪除的圖),object1存儲到了NODE1中,而object2、object3、object4都存儲到了NODE3中,這樣就照成了非常不平衡的狀態。在一致性哈希算法中,爲了儘可能的滿足平衡性,其引入了虛擬節點。
“虛擬節點”( virtual node )是實際節點(機器)在 hash 空間的複製品( replica ),一實際個節點(機器)對應了若干個“虛擬節點”,這個對應個數也成爲“複製個數”,“虛擬節點”在 hash 空間中以hash值排列。
以上面只部署了NODE1和NODE3的情況(NODE2被刪除的圖)爲例,之前的對象在機器上的分佈很不均衡,現在我們以2個副本(複製個數)爲例,這樣整個hash環中就存在了4個虛擬節點,最後對象映射的關係圖如下:
根據上圖可知對象的映射關係:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通過虛擬節點的引入,對象的分佈就比較均衡了。那麼在實際操作中,正真的對象查詢是如何工作的呢?對象從hash到虛擬節點到實際節點的轉換如下圖:
虛擬節點”的hash計算可以採用對應節點的IP地址加數字後綴的方式。例如假設NODE1的IP地址爲192.168.1.100。引入“虛擬節點”前,計算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虛擬節點”後,計算“虛擬節”點NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2
3.Java代碼實現
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;
/**
*
*/
/**
* 一致性Hash算法-Java實現
*
* @author tangthis
*
* <p>https://github.com/tangthis
*/
public class ConsistencyHash {
private TreeMap<Long,Object> nodes = null;
//真實服務器節點信息
private List<Object> shards = new ArrayList();
//設置虛擬節點數目
private int VIRTUAL_NUM = 4;
/**
* 初始化一致環
*/
public void init() {
shards.add("192.168.0.0-服務器0");
shards.add("192.168.0.1-服務器1");
shards.add("192.168.0.2-服務器2");
shards.add("192.168.0.3-服務器3");
shards.add("192.168.0.4-服務器4");
nodes = new TreeMap<Long,Object>();
for(int i=0; i<shards.size(); i++) {
Object shardInfo = shards.get(i);
for(int j=0; j<VIRTUAL_NUM; j++) {
nodes.put(hash(computeMd5("SHARD-" + i + "-NODE-" + j),j), shardInfo);
}
}
}
/**
* 根據key的hash值取得服務器節點信息
* @param hash
* @return
*/
public Object getShardInfo(long hash) {
Long key = hash;
SortedMap<Long, Object> tailMap=nodes.tailMap(key);
if(tailMap.isEmpty()) {
key = nodes.firstKey();
} else {
key = tailMap.firstKey();
}
return nodes.get(key);
}
/**
* 打印圓環節點數據
*/
public void printMap() {
System.out.println(nodes);
}
/**
* 根據2^32把節點分佈到圓環上面。
* @param digest
* @param nTime
* @return
*/
public long hash(byte[] digest, int nTime) {
long rv = ((long) (digest[3+nTime*4] & 0xFF) << 24)
| ((long) (digest[2+nTime*4] & 0xFF) << 16)
| ((long) (digest[1+nTime*4] & 0xFF) << 8)
| (digest[0+nTime*4] & 0xFF);
return rv & 0xffffffffL; /* Truncate to 32-bits */
}
/**
* Get the md5 of the given key.
* 計算MD5值
*/
public byte[] computeMd5(String k) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 not supported", e);
}
md5.reset();
byte[] keyBytes = null;
try {
keyBytes = k.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unknown string :" + k, e);
}
md5.update(keyBytes);
return md5.digest();
}
public static void main(String[] args) {
Random ran = new Random();
ConsistencyHash hash = new ConsistencyHash();
hash.init();
hash.printMap();
//循環50次,是爲了取50個數來測試效果,當然也可以用其他任何的數據來測試
for(int i=0; i<50; i++) {
System.out.println(hash.getShardInfo(hash.hash(hash.computeMd5(String.valueOf(i)),ran.nextInt(hash.VIRTUAL_NUM))));
}
}
}