一致性哈希算法
摘自:http://blog.codinglabs.org/articles/consistent-hashing.html
算法簡述
一致性哈希算法(Consistent Hashing)最早在論文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出。簡單來說,一致性哈希將整個哈希值空間組織成一個虛擬的圓環,如假設某哈希函數H的值空間為0 - 232-1(即哈希值是一個32位無符號整形),整個哈希空間環如下:
整個空間按順時針方向組織。0和232-1在零點中方向重合。
下一步將各個服務器使用H進行一個哈希,具體可以選擇服務器的ip或主機名作為關鍵字進行哈希,這樣每台機器就能確定其在哈希環上的位置,這里假設將上文中三台服務器使用ip地址哈希后在環空間的位置如下:
接下來使用如下算法定位數據訪問到相應服務器:將數據key使用相同的函數H計算出哈希值h,通根據h確定此數據在環上的位置,從此位置沿環順時針“行走”,第一台遇到的服務器就是其應該定位到的服務器。
例如我們有A、B、C、D四個數據對象,經過哈希計算后,在環空間上的位置如下:
根據一致性哈希算法,數據A會被定為到Server 1上,D被定為到Server 3上,而B、C分別被定為到Server 2上。
容錯性與可擴展性分析
下面分析一致性哈希算法的容錯性和可擴展性。現假設Server 3宕機了:
可以看到此時A、C、B不會受到影響,只有D節點被重定位到Server 2。一般的,在一致性哈希算法中,如果一台服務器不可用,則受影響的數據僅僅是此服務器到其環空間中前一台服務器(即順着逆時針方向行走遇到的第一台服務器)之間數據,其它不會受到影響。
下面考慮另外一種情況,如果我們在系統中增加一台服務器Memcached Server 4:
此時A、D、C不受影響,只有B需要重定位到新的Server 4。一般的,在一致性哈希算法中,如果增加一台服務器,則受影響的數據僅僅是新服務器到其環空間中前一台服務器(即順着逆時針方向行走遇到的第一台服務器)之間數據,其它不會受到影響。
綜上所述,一致性哈希算法對於節點的增減都只需重定位環空間中的一小部分數據,具有較好的容錯性和可擴展性。
虛擬節點
一致性哈希算法在服務節點太少時,容易因為節點分部不均勻而造成數據傾斜問題。例如我們的系統中有兩台服務器,其環分布如下:
此時必然造成大量數據集中到Server 1上,而只有極少量會定位到Server 2上。為了解決這種數據傾斜問題,一致性哈希算法引入了虛擬節點機制,即對每一個服務節點計算多個哈希,每個計算結果位置都放置一個此服務節點,稱為虛擬節點。具體做法可以在服務器ip或主機名的后面增加編號來實現。例如上面的情況,我們決定為每台服務器計算三個虛擬節點,於是可以分別計算“Memcached Server 1#1”、“Memcached Server 1#2”、“Memcached Server 1#3”、“Memcached Server 2#1”、“Memcached Server 2#2”、“Memcached Server 2#3”的哈希值,於是形成六個虛擬節點:
同時數據定位算法不變,只是多了一步虛擬節點到實際節點的映射,例如定位到“Memcached Server 1#1”、“Memcached Server 1#2”、“Memcached Server 1#3”三個虛擬節點的數據均定位到Server 1上。這樣就解決了服務節點少時數據傾斜的問題。在實際應用中,通常將虛擬節點數設置為32甚至更大,因此即使很少的服務節點也能做到相對均勻的數據分布。
總結
目前一致性哈希基本成為了分布式系統組件的標准配置,例如Memcached的各種客戶端都提供內置的一致性哈希支持。本文只是簡要介紹了這個算法,更深入的內容可以參看論文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》,同時提供一個C語言版本的實現供參考。
實現:https://community.oracle.com/blogs/tomwhite/2007/11/27/consistent-hashing http://www.cnblogs.com/xrq730/p/5186728.html 查找的本質 利用查找樹sorted map實現。
import java.util.Collection; import java.util.SortedMap; import java.util.TreeMap; public class ConsistentHash<T> { private final HashFunction hashFunction; private final int numberOfReplicas; private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>(); public ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<T> nodes) { this.hashFunction = hashFunction; this.numberOfReplicas = numberOfReplicas; for (T node : nodes) { add(node); } } public void add(T node) { for (int i = 0; i < numberOfReplicas; i++) { circle.put(hashFunction.hash(node.toString() + i), node); } } public void remove(T node) { for (int i = 0; i < numberOfReplicas; i++) { circle.remove(hashFunction.hash(node.toString() + i)); } } public T get(Object key) { if (circle.isEmpty()) { return null; } int hash = hashFunction.hash(key); if (!circle.containsKey(hash)) { SortedMap<Integer, T> tailMap = circle.tailMap(hash); hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey(); } return circle.get(hash); } }