五分鐘看懂一致性哈希算法

一致性哈希算法在1997年由麻省理工學院的Karger等人在解決分佈式Cache中提出的,設計目標是爲了解決因特網中的熱點(Hot spot)問題,初衷和CARP十分相似。一致性哈希修正了CARP使用的簡單哈希算法帶來的問題,使得DHT能夠在P2P環境中真正獲得應用。
java

但如今一致性hash算法在分佈式系統中也獲得了普遍應用,研究過memcached緩存數據庫的人都知道,memcached服務器端自己不提供分佈式cache的一致性,而是由客戶端來提供,具體在計算一致性hash時採用以下步驟:node

  1. 首先求出memcached服務器(節點)的哈希值,並將其配置到0~232的圓(continuum)上。算法

  2. 而後採用一樣的方法求出存儲數據的鍵的哈希值,並映射到相同的圓上。數據庫

  3. 而後從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器上。若是超過232仍然找不到服務器,就會保存到第一臺memcached服務器上。緩存

從上圖的狀態中添加一臺memcached服務器。餘數分佈式算法因爲保存鍵的服務器會發生巨大變化而影響緩存的命中率,但Consistent Hashing中,只有在圓(continuum)上增長服務器的地點逆時針方向的第一臺服務器上的鍵會受到影響,以下圖所示:服務器

一致性Hash性質

考慮到分佈式系統每一個節點都有可能失效,而且新的節點極可能動態的增長進來,如何保證當系統的節點數目發生變化時仍然可以對外提供良好的服務,這是值得考慮的,尤爲實在設計分佈式緩存系統時,若是某臺服務器失效,對於整個系統來講若是不採用合適的算法來保證一致性,那麼緩存於系統中的全部數據均可能會失效(即因爲系統節點數目變少,客戶端在請求某一對象時須要從新計算其hash值(一般與系統中的節點數目有關),因爲hash值已經改變,因此極可能找不到保存該對象的服務器節點),所以一致性hash就顯得相當重要,良好的分佈式cahce系統中的一致性hash算法應該知足如下幾個方面:app

  • 平衡性(Balance)dom

平衡性是指哈希的結果可以儘量分佈到全部的緩衝中去,這樣可使得全部的緩衝空間都獲得利用。不少哈希算法都可以知足這一條件。分佈式

  • 單調性(Monotonicity)ide

單調性是指若是已經有一些內容經過哈希分派到了相應的緩衝中,又有新的緩衝區加入到系統中,那麼哈希的結果應可以保證原有已分配的內容能夠被映射到新的緩衝區中去,而不會被映射到舊的緩衝集合中的其餘緩衝區。簡單的哈希算法每每不能知足單調性的要求,如最簡單的線性哈希: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);
 }
 
}
相關文章
相關標籤/搜索