五分鐘看懂一致性雜湊演算法
一致性雜湊演算法在1997年由麻省理工學院的Karger等人在解決分散式Cache中提出的,設計目標是為了解決因特網中的熱點(Hot spot)問題,初衷和CARP十分類似。一致性雜湊修正了CARP使用的簡單雜湊演算法帶來的問題,使得DHT可以在P2P環境中真正得到應用。
但現在一致性hash演算法在分散式系統中也得到了廣泛應用,研究過memcached快取資料庫的人都知道,memcached伺服器端本身不提供分散式cache的一致性,而是由客戶端來提供,具體在計算一致性hash時採用如下步驟:
- 首先求出memcached伺服器(節點)的雜湊值,並將其配置到0~232的圓(continuum)上。
- 然後採用同樣的方法求出儲存資料的鍵的雜湊值,並對映到相同的圓上。
- 然後從資料對映到的位置開始順時針查詢,將資料儲存到找到的第一個伺服器上。如果超過232
從上圖的狀態中新增一臺memcached伺服器。餘數分散式演算法由於儲存鍵的伺服器會發生巨大變化而影響快取的命中率,但Consistent Hashing中,只有在園(continuum)上增加伺服器的地點逆時針方向的第一臺伺服器上的鍵會受到影響,如下圖所示:
一致性Hash性質
考慮到分散式系統每個節點都有可能失效,並且新的節點很可能動態的增加進來,如何保證當系統的節點數目發生變化時仍然能夠對外提供良好的服務,這是值得考慮的,尤其實在設計分散式快取系統時,如果某臺伺服器失效,對於整個系統來說如果不採用合適的演算法來保證一致性,那麼緩存於系統中的所有資料都可能會失效(即由於系統節點數目變少,客戶端在請求某一物件時需要重新計算其hash值(通常與系統中的節點數目有關),由於hash值已經改變,所以很可能找不到儲存該物件的伺服器節點),因此一致性hash就顯得至關重要,良好的分散式cahce系統中的一致性hash演算法應該滿足以下幾個方面:
- 平衡性(Balance)
平衡性是指雜湊的結果能夠儘可能分佈到所有的緩衝中去,這樣可以使得所有的緩衝空間都得到利用。很多雜湊演算法都能夠滿足這一條件。
- 單調性(Monotonicity)
單調性是指如果已經有一些內容通過雜湊分派到了相應的緩衝中,又有新的緩衝區加入到系統中,那麼雜湊的結果應能夠保證原有已分配的內容可以被對映到新的緩衝區中去,而不會被對映到舊的緩衝集合中的其他緩衝區。簡單的雜湊演算法往往不能滿足單調性的要求,如最簡單的線性雜湊:x = (ax + b) mod (P),在上式中,P表示全部緩衝的大小。不難看出,當緩衝大小發生變化時(從P1到P2),原來所有的雜湊結果均會發生變化,從而不滿足單調性的要求。雜湊結果的變化意味著當緩衝空間發生變化時,所有的對映關係需要在系統內全部更新。而在P2P系統內,緩衝的變化等價於Peer加入或退出系統,這一情況在P2P系統中會頻繁發生,因此會帶來極大計算和傳輸負荷。單調性就是要求雜湊演算法能夠應對這種情況。
- 分散性(Spread)
在分散式環境中,終端有可能看不到所有的緩衝,而是隻能看到其中的一部分。當終端希望通過雜湊過程將內容對映到緩衝上時,由於不同終端所見的緩衝範圍有可能不同,從而導致雜湊的結果不一致,最終的結果是相同的內容被不同的終端對映到不同的緩衝區中。這種情況顯然是應該避免的,因為它導致相同內容被儲存到不同緩衝中去,降低了系統儲存的效率。分散性的定義就是上述情況發生的嚴重程度。好的雜湊演算法應能夠儘量避免不一致的情況發生,也就是儘量降低分散性。
- 負載(Load)
負載問題實際上是從另一個角度看待分散性問題。既然不同的終端可能將相同的內容對映到不同的緩衝區中,那麼對於一個特定的緩衝區而言,也可能被不同的使用者對映為不同的內容。與分散性一樣,這種情況也是應當避免的,因此好的雜湊演算法應能夠儘量降低緩衝的負荷。
- 平滑性(Smoothness)
平滑性是指快取伺服器的數目平滑改變和快取物件的平滑改變是一致的。
原理
基本概念
一致性雜湊演算法(Consistent Hashing)最早在論文《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》中被提出。簡單來說,一致性雜湊將整個雜湊值空間組織成一個虛擬的圓環,如假設某雜湊函式H的值空間為0-2^32-1(即雜湊值是一個32位無符號整形),整個雜湊空間環如下:
整個空間按順時針方向組織。0和232-1在零點中方向重合。
下一步將各個伺服器使用Hash進行一個雜湊,具體可以選擇伺服器的ip或主機名作為關鍵字進行雜湊,這樣每臺機器就能確定其在雜湊環上的位置,這裡假設將上文中四臺伺服器使用ip地址雜湊後在環空間的位置如下:
接下來使用如下演算法定位資料訪問到相應伺服器:將資料key使用相同的函式Hash計算出雜湊值,並確定此資料在環上的位置,從此位置沿環順時針“行走”,第一臺遇到的伺服器就是其應該定位到的伺服器。
例如我們有Object A、Object B、Object C、Object D四個資料物件,經過雜湊計算後,在環空間上的位置如下:
根據一致性雜湊演算法,資料A會被定為到Node A上,B被定為到Node B上,C被定為到Node C上,D被定為到Node D上。
下面分析一致性雜湊演算法的容錯性和可擴充套件性。現假設Node C不幸宕機,可以看到此時物件A、B、D不會受到影響,只有C物件被重定位到Node D。一般的,在一致性雜湊演算法中,如果一臺伺服器不可用,則受影響的資料僅僅是此伺服器到其環空間中前一臺伺服器(即沿著逆時針方向行走遇到的第一臺伺服器)之間資料,其它不會受到影響。
下面考慮另外一種情況,如果在系統中增加一臺伺服器Node X,如下圖所示:
此時物件Object A、B、D不受影響,只有物件C需要重定位到新的Node X 。一般的,在一致性雜湊演算法中,如果增加一臺伺服器,則受影響的資料僅僅是新伺服器到其環空間中前一臺伺服器(即沿著逆時針方向行走遇到的第一臺伺服器)之間資料,其它資料也不會受到影響。
綜上所述,一致性雜湊演算法對於節點的增減都只需重定位環空間中的一小部分資料,具有較好的容錯性和可擴充套件性。
另外,一致性雜湊演算法在服務節點太少時,容易因為節點分部不均勻而造成資料傾斜問題。例如系統中只有兩臺伺服器,其環分佈如下,
此時必然造成大量資料集中到Node A上,而只有極少量會定位到Node B上。為了解決這種資料傾斜問題,一致性雜湊演算法引入了虛擬節點機制,即對每一個服務節點計算多個雜湊,每個計算結果位置都放置一個此服務節點,稱為虛擬節點。具體做法可以在伺服器ip或主機名的後面增加編號來實現。例如上面的情況,可以為每臺伺服器計算三個虛擬節點,於是可以分別計算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的雜湊值,於是形成六個虛擬節點:
同時資料定位演算法不變,只是多了一步虛擬節點到實際節點的對映,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三個虛擬節點的資料均定位到Node A上。這樣就解決了服務節點少時資料傾斜的問題。在實際應用中,通常將虛擬節點數設定為32甚至更大,因此即使很少的服務節點也能做到相對均勻的資料分佈。
JAVA程式碼實現
package org.java.base.hash;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
public class ConsistentHash<T> {
private final int numberOfReplicas;// 節點的複製因子,實際節點個數 * numberOfReplicas =
// 虛擬節點個數
private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();// 儲存虛擬節點的hash值到真實節點的對映
public ConsistentHash( int numberOfReplicas,
Collection<T> nodes) {
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes){
add(node);
}
}
public void add(T node) {
for (int i = 0; i < numberOfReplicas; i++){
// 對於一個實際機器節點 node, 對應 numberOfReplicas 個虛擬節點
/*
* 不同的虛擬節點(i不同)有不同的hash值,但都對應同一個實際機器node
* 虛擬node一般是均衡分佈在環上的,資料儲存在順時針方向的虛擬node上
*/
String nodestr =node.toString() + i;
int hashcode =nodestr.hashCode();
System.out.println("hashcode:"+hashcode);
circle.put(hashcode, node);
}
}
public void remove(T node) {
for (int i = 0; i < numberOfReplicas; i++)
circle.remove((node.toString() + i).hashCode());
}
/*
* 獲得一個最近的順時針節點,根據給定的key 取Hash
* 然後再取得順時針方向上最近的一個虛擬節點對應的實際節點
* 再從實際節點中取得 資料
*/
public T get(Object key) {
if (circle.isEmpty())
return null;
int hash = key.hashCode();// node 用String來表示,獲得node在雜湊環中的hashCode
System.out.println("hashcode----->:"+hash);
if (!circle.containsKey(hash)) {//資料對映在兩臺虛擬機器器所在環之間,就需要按順時針方向尋找機器
SortedMap<Integer, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
return circle.get(hash);
}
public long getSize() {
return circle.size();
}
/*
* 查看錶示整個雜湊環中各個虛擬節點位置
*/
public void testBalance(){
Set<Integer> sets = circle.keySet();//獲得TreeMap中所有的Key
SortedSet<Integer> sortedSets= new TreeSet<Integer>(sets);//將獲得的Key集合排序
for(Integer hashCode : sortedSets){
System.out.println(hashCode);
}
System.out.println("----each location 's distance are follows: ----");
/*
* 檢視相鄰兩個hashCode的差值
*/
Iterator<Integer> it = sortedSets.iterator();
Iterator<Integer> it2 = sortedSets.iterator();
if(it2.hasNext())
it2.next();
long keyPre, keyAfter;
while(it.hasNext() && it2.hasNext()){
keyPre = it.next();
keyAfter = it2.next();
System.out.println(keyAfter - keyPre);
}
}
public static void main(String[] args) {
Set<String> nodes = new HashSet<String>();
nodes.add("A");
nodes.add("B");
nodes.add("C");
ConsistentHash<String> consistentHash = new ConsistentHash<String>(2, nodes);
consistentHash.add("D");
System.out.println("hash circle size: " + consistentHash.getSize());
System.out.println("location of each node are follows: ");
consistentHash.testBalance();
String node =consistentHash.get("apple");
System.out.println("node----------->:"+node);
}
}