目前咱們不少時候都是在作分佈式系統,可是咱們需把客戶端的請求均勻的分佈到N個服務器中,通常咱們能夠考慮經過Object的HashCodeHash%N,經過取餘,將客戶端的請求分佈到不一樣的的服務端。可是在分佈式集羣中咱們一般須要添加或刪除服務器,因此經過取餘是不行的。一致性Hash就是爲了解決這個問題。java
Consistent Hashing 一致性Hash的原理node
一、環型Hash空間算法
根據經常使用的Hash,是將key哈希到一個長爲2^32的桶中,即0~2^32-1的數字空間,最後經過首尾相連,咱們能夠想象成一個閉合的圓。如圖:服務器
二、把數據經過必定的Hash算法處理後,映射到環上負載均衡
例如:咱們有Object一、Object二、Object三、Object4,經過Hash算法求出值以下:分佈式
Hash(Object1) = key1;ide
Hash(Object2) = key2;性能
Hash(Object3) = key3;測試
Hash(Object4) = key4;this
三、將機器信息經過hash算法映射到環上
通常狀況下是對機器的信息經過計算hash,而後以順時針方向計算,將對象信息存儲在相應的位置。
四、虛擬節點
上面是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; } }
測試結果:
從運行結果能夠看出:負載因子較大的被分配的機率就越大。