詳解Consistent Hashing演算法

chuanzhongdu1發表於2011-09-27

在做伺服器負載均衡時候可供選擇的負載均衡的演算法有很多,包括: 輪循演算法(Round Robin)、雜湊演算法(HASH)、最少連線演算法(Least Connection)、響應速度演算法(Response Time)、加權法(Weighted )等。其中雜湊演算法是最為常用的演算法.

典型的應用場景是: 有N臺伺服器提供快取服務,需要對伺服器進行負載均衡,將請求平均分發到每臺伺服器上,每臺機器負責1/N的服務。

常用的演算法是對hash結果取餘數 (hash() mod N ):對機器編號從0到N-1,按照自定義的 hash()演算法,對每個請求的hash()值按N取模,得到餘數i,然後將請求分發到編號為i的機器。但這樣的演算法方法存在致命問題,如果某一臺機器當機,那麼應該落在該機器的請求就無法得到正確的處理,這時需要將當掉的伺服器從演算法從去除,此時候會有(N-1)/N的伺服器的快取資料需要重新進行計算;如果新增一臺機器,會有N /(N+1)的伺服器的快取資料需要進行重新計算。對於系統而言,這通常是不可接受的顛簸(因為這意味著大量快取的失效或者資料需要轉移)。那麼,如何設計一個負載均衡策略,使得受到影響的請求儘可能的少呢?

在Memcached、Key-Value Store 、Bittorrent DHT、LVS中都採用了Consistent Hashing演算法,可以說Consistent Hashing 是分散式系統負載均衡的首選演算法。

1、Consistent Hashing演算法描述

下面以Memcached中的Consisten Hashing演算法為例說明(參考memcached的分散式演算法 )。

由於hash演算法結果一般為unsigned int型,因此對於hash函式的結果應該均勻分佈在[0,232 -1]間,如果我們把一個圓環用232 個點來進行均勻切割,首先按照hash(key)函式算出伺服器(節點)的雜湊值, 並將其分佈到0~232 的圓上。

用同樣的hash(key)函式求出需要儲存資料的鍵的雜湊值,並對映到圓上。然後從資料對映到的位置開始順時針查詢,將資料儲存到找到的第一個伺服器(節點)上。


Consistent Hashing原理示意圖

1. 新增一個節點:只有在圓環上新增節點到逆時針方向的第一個節點之間的資料會受到影響(增加節點順時針的第一個節點的資訊需要遷移到增加節點上)。

2. 刪除一個節點:只有在圓環上原來刪除節點到 逆時針 方向的第一個節點之間的資料會受到影響(刪除節點的資訊需要遷移到順時針的第一個節點上) ,因此通過Consistent Hashing很好地解決了負載均衡中由於新增節點、刪除節點引起的hash值顛簸問題。


Consistent Hashing新增伺服器示意圖

虛擬節點(virtual nodes): 之所以要引進虛擬節點是因為在伺服器(節點)數較少的情況下(例如只有3臺伺服器),通過hash(key)算出節點的雜湊值在圓環上並不是均勻分佈的(稀疏的),仍然會出現各節點負載不均衡的問題。虛擬節點可以認為是實際節點的複製品(replicas),本質上與實際節點實際上是一樣的(key並不相同)。引入虛擬節點後,通過將每個實際的伺服器(節點)數按照一定的比例(例如200倍)擴大後並計算其hash(key)值以均勻分佈到圓環上。在進行負載均衡時候,落到虛擬節點的雜湊值實際就落到了實際的節點上。由於所有的實際節點是按照相同的比例複製成虛擬節點的,因此解決了節點數較少的情況下雜湊值在圓環上均勻分佈的問題。


虛擬節點對Consistent Hashing結果的影響

從上圖可以看出,在節點數為10個的情況下,每個實際節點的虛擬節點數為實際節點的100-200倍的時候,結果還是很均衡的。

2、Consistent Hashing演算法實現:

文章Consistent Hashing 中描述了Consistent Hashing的Java實現,很簡潔。

  1. import java.util.Collection;  
  2. import java.util.SortedMap;  
  3. import java.util.TreeMap;  
  4.  
  5. public class ConsistentHash<T> {  
  6.  
  7.  private final HashFunction hashFunction;  
  8.  private final int numberOfReplicas;  
  9.  private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();  
  10.  
  11.  public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,  
  12.      Collection<T> nodes) {  
  13.    this.hashFunction = hashFunction;  
  14.    this.numberOfReplicas = numberOfReplicas;  
  15.  
  16.    for (T node : nodes) {  
  17.      add(node);  
  18.    }  
  19.  }  
  20.  
  21.  public void add(T node) {  
  22.    for (int i = 0; i < numberOfReplicas; i++) {  
  23.      circle.put(hashFunction.hash(node.toString() + i), node);  
  24.    }  
  25.  }  
  26.  
  27.  public void remove(T node) {  
  28.    for (int i = 0; i < numberOfReplicas; i++) {  
  29.      circle.remove(hashFunction.hash(node.toString() + i));  
  30.    }  
  31.  }  
  32.  
  33.  public T get(Object key) {  
  34.    if (circle.isEmpty()) {  
  35.      return null;  
  36.    }  
  37.    int hash = hashFunction.hash(key);  
  38.    if (!circle.containsKey(hash)) {  
  39.      SortedMap<Integer, T> tailMap = circle.tailMap(hash);  
  40.      hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();  
  41.    }  
  42.    return circle.get(hash);  
  43.  }  
  44.  
原文地址http://developer.51cto.com/art/201104/254419.htm

相關文章