目前我們很多時候都是在做分佈式系統,但是我們需把客戶端的請求均勻的分佈到N個服務器中,一般我們可以考慮通過Object的HashCodeHash%N,通過取餘,將客戶端的請求分佈到不同的的服務端。但是在分佈式集羣中我們通常需要添加或刪除服務器,所以通過取餘是不行的。一致性Hash就是爲了解決這個問題。
Consistent Hashing 一致性Hash的原理
1、環型Hash空間
根據常用的Hash,是將key哈希到一個長爲2^32的桶中,即0~2^32-1的數字空間,最後通過首尾相連,我們可以想象成一個閉合的圓。如圖:
2、把數據通過一定的Hash算法處理後,映射到環上
例如:我們有Object1、Object2、Object3、Object4,通過Hash算法求出值如下:
Hash(Object1) = key1;
Hash(Object2) = key2;
Hash(Object3) = key3;
Hash(Object4) = key4;
3、將機器信息通過hash算法映射到環上
一般情況下是對機器的信息通過計算hash,然後以順時針方向計算,將對象信息存儲在相應的位置。
4、虛擬節點
上面是Hash算法的特性,但是Hash算法缺少一個平衡性。
Hash算法的平衡行就是爲了儘可能使分配到每個數據桶裏面的節點是均衡的,一個簡單的例子:我們有3個分佈式服務器,在大量客戶端訪問時,通過Hash算法,使得他們能在每個服務器均勻的訪問。所以這裏引入了“虛擬節點”節點,從而保證數據節點均衡。
“虛擬節點”就是真實節點的複製品,一個真實的節點對應多個“虛擬節點”,這樣使得我們的節點能儘可能的在環形Hash空間均勻分佈,這樣我們再根據虛擬節點找到真實節點,從而保證每個真實節點上分配到的請求是均衡的。
具體的代碼實現如下:
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistencyHashing {
// 虛擬節點的個數
private static final int VIRTUAL_NUM = 5;
// 虛擬節點分配,key是hash值,value是虛擬節點服務器名稱
private static SortedMap<Integer, String> shards = new TreeMap<Integer, String>();
// 真實節點列表
private static List<String> realNodes = new LinkedList<String>();
//模擬初始服務器
private static String[] servers = { "192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.5", "192.168.1.6" };
static {
for (String server : servers) {
realNodes.add(server);
System.out.println("真實節點[" + server + "] 被添加");
for (int i = 0; i < VIRTUAL_NUM; i++) {
String virtualNode = server + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
System.out.println("虛擬節點[" + virtualNode + "] hash:" + hash + ",被添加");
}
}
}
/**
* 獲取被分配的節點名
*
* @param node
* @return
*/
public static String getServer(String node) {
int hash = getHash(node);
Integer key = null;
SortedMap<Integer, String> subMap = shards.tailMap(hash);
if (subMap.isEmpty()) {
key = shards.lastKey();
} else {
key = subMap.firstKey();
}
String virtualNode = shards.get(key);
return virtualNode.substring(0, virtualNode.indexOf("&&"));
}
/**
* 添加節點
*
* @param node
*/
public static void addNode(String node) {
if (!realNodes.contains(node)) {
realNodes.add(node);
System.out.println("真實節點[" + node + "] 上線添加");
for (int i = 0; i < VIRTUAL_NUM; i++) {
String virtualNode = node + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
System.out.println("虛擬節點[" + virtualNode + "] hash:" + hash + ",被添加");
}
}
}
/**
* 刪除節點
*
* @param node
*/
public static void delNode(String node) {
if (realNodes.contains(node)) {
realNodes.remove(node);
System.out.println("真實節點[" + node + "] 下線移除");
for (int i = 0; i < VIRTUAL_NUM; i++) {
String virtualNode = node + "&&VN" + i;
int hash = getHash(virtualNode);
shards.remove(hash);
System.out.println("虛擬節點[" + virtualNode + "] hash:" + hash + ",被移除");
}
}
}
/**
* FNV1_32_HASH算法
*/
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * 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 static void main(String[] args) {
//模擬客戶端的請求
String[] nodes = { "127.0.0.1", "10.9.3.253", "192.168.10.1" };
for (String node : nodes) {
System.out.println("[" + node + "]的hash值爲" + getHash(node) + ", 被路由到結點[" + getServer(node) + "]");
}
// 添加一個節點(模擬服務器上線)
addNode("192.168.1.7");
// 刪除一個節點(模擬服務器下線)
delNode("192.168.1.2");
for (String node : nodes) {
System.out.println("[" + node + "]的hash值爲" + getHash(node) + ", 被路由到結點[" + getServer(node) + "]");
}
}
}
測試結果:
從結果可以看出:服務器節點上線和下線並不會對我們服務有任何影響,除非所有的服務都下線。當之前映射的服務器下線,我們可以切換到和它Hash臨近的服務節點上,保證服務的負載均衡。
如果我們考慮每臺服務器性能不一致,比如服務器內存有32G、16G、8G的,我們可以根據不同的服務器性能,分配不同的負載因子(就是上面程序的VIRTUAL_NUM),這樣我們是不是可以想到和Dubbo裏面的負載因子是一致的。我們可以手動的調整每臺服務器的負載因子,從而根據每個服務器性能,分配不同權重的客戶端請求負載量 。
實現案例:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
public class ConsistencyHashingLoadFactor {
// 真實節點列表
private static List<Machine> realNodes = new ArrayList<Machine>();
// 虛擬節點,key是Hash值,value是虛擬節點信息
private static SortedMap<Integer, String> shards = new TreeMap<Integer, String>();
static {
realNodes.add(new Machine("192.168.1.1", LoadFactor.Memory8G));
realNodes.add(new Machine("192.168.1.2", LoadFactor.Memory16G));
realNodes.add(new Machine("192.168.1.3", LoadFactor.Memory32G));
realNodes.add(new Machine("192.168.1.4", LoadFactor.Memory16G));
for (Machine node : realNodes) {
for (int i = 0; i < node.getMemory().getVrNum(); i++) {
String server = node.getHost();
String virtualNode = server + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
}
}
}
/**
* 獲取被分配的節點名
*
* @param node
* @return
*/
public static Machine getServer(String node) {
int hash = getHash(node);
Integer key = null;
SortedMap<Integer, String> subMap = shards.tailMap(hash);
if (subMap.isEmpty()) {
key = shards.lastKey();
} else {
key = subMap.firstKey();
}
String virtualNode = shards.get(key);
String realNodeName = virtualNode.substring(0, virtualNode.indexOf("&&"));
for (Machine machine : realNodes) {
if (machine.getHost().equals(realNodeName)) {
return machine;
}
}
return null;
}
/**
* 添加節點
*
* @param node
*/
public static void addNode(Machine node) {
if (!realNodes.contains(node)) {
realNodes.add(node);
System.out.println("真實節點[" + node + "] 上線添加");
for (int i = 0; i < node.getMemory().getVrNum(); i++) {
String virtualNode = node.getHost() + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
System.out.println("虛擬節點[" + virtualNode + "] hash:" + hash + ",被添加");
}
}
}
/**
* 刪除節點
*
* @param node
*/
public static void delNode(Machine node) {
String host = node.getHost();
Iterator<Machine> it = realNodes.iterator();
while(it.hasNext()) {
Machine machine = it.next();
if(machine.getHost().equals(host)) {
it.remove();
System.out.println("真實節點[" + node + "] 下線移除");
for (int i = 0; i < node.getMemory().getVrNum(); i++) {
String virtualNode = node.getHost() + "&&VN" + i;
int hash = getHash(virtualNode);
shards.remove(hash);
System.out.println("虛擬節點[" + virtualNode + "] hash:" + hash + ",被移除");
}
}
}
}
/**
* FNV1_32_HASH算法
*/
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * 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 static void main(String[] args) {
// 模擬客戶端的請求
String[] nodes = { "127.0.0.1", "10.9.3.253", "192.168.10.1" };
for (String node : nodes) {
System.out.println("[" + node + "]的hash值爲" + getHash(node) + ", 被路由到結點[" + getServer(node) + "]");
}
// 添加一個節點(模擬服務器上線)
addNode(new Machine("192.168.1.7", LoadFactor.Memory16G));
// 刪除一個節點(模擬服務器下線)
delNode(new Machine("192.168.1.1", LoadFactor.Memory8G));
for (String node : nodes) {
System.out.println("[" + node + "]的hash值爲" + getHash(node) + ", 被路由到結點[" + getServer(node) + "]");
}
}
}
/**
* 機器類
*
* @author yangkuanjun
*
*/
class Machine {
private String host;
private LoadFactor memory;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public LoadFactor getMemory() {
return memory;
}
public void setMemory(LoadFactor memory) {
this.memory = memory;
}
public Machine(String host, LoadFactor memory) {
super();
this.host = host;
this.memory = memory;
}
@Override
public String toString() {
return "Machine [host=" + host + ", memory=" + memory + "]";
}
}
/**
* 負載因子
*
* @author yangkuanjun
*
*/
enum LoadFactor {
Memory8G(5), Memory16G(10), Memory32G(20);
private int vrNum;
private LoadFactor(int vrNum) {
this.vrNum = vrNum;
}
public int getVrNum() {
return vrNum;
}
}
測試結果:
從運行結果可以看出:負載因子較大的被分配的概率就越大。