背景
隨着時代的發展,數據量與日俱增,相比縱向擴展單機的性能,人們更傾向於橫向擴展,將多臺一般的廉價機器組成集羣來充當超級計算機,節省了大量的成本,代價是極大地增加了系統的複雜性。爲了應對這些複雜性,一批又一批分佈式領域的技術相繼誕生,其中不乏一些看過之後令人拍案叫絕的精彩的想法。
從存儲來說,數據量大的時候,一臺機器不能勝任時,那麼通常的做法是將數據分片,存儲到多臺機器上,通過集羣的方式完成數據存儲的需求,舉個例子,你有大量的數據需要緩存,比如100G,一般的機器顯然沒有這麼大的內存,於是不得不把這100G分佈到比如10臺機器上,每臺存儲10G的數據。數據分配的算法有很多種,一種比較容易想到的就是hash,通過將數據對10取模,hash到各個機器上。看似很美好,但是有兩點因素是不得不考慮的:
組成集羣的機器都是廉價的小型計算機,機器故障是在正常不過的事情了。
隨着數據量的持續增加,你發現10臺機器不夠用了,想增加1臺上去,過一段時間,又需要加一臺。
以上兩種情況有一個共同點:機器數量的變動。而機器數量變動之後,對數據重新取模時,會造成大量的緩存失效。舉個例子:
本來10臺機器,有個key是100,通過key % 10將數據均勻分佈到各個機器上,這時100 % 10 = 0,這個key被分配到地0號機器上存儲了,然後一臺機器掛了,這時,去獲取剛纔存儲的key,100 % 9 = 1,重新hash後,變成了去第1號機器上獲取key,自然獲取不到,緩存未命中,於是不得不把key重新存儲到第1號機器上。。。
於是乎,人們感嘆,如果有什麼辦法,能在增減機器節點的時候,不需要或者儘可能少的需要爲各個key重新分配機器就好了。然後聰明絕頂的程序員們想出了一致性hash的辦法。
一致性Hash
什麼是一致性Hash呢?看看wiki百科上的定義:
Consistent hashing is a special kind of hashing such that when a hash table is resized, only K/Nkeys need to be remapped on average, where K is the number of keys, and N is the number of slots. In contrast, in most traditional hash tables, a change in the number of array slots causes nearly all keys to be remapped because the mapping between the keys and the slots is defined by a modular operation.
翻譯一下:
一致性hash是一種特殊的hash,當hash表的大小發生變化時,平均只有K/N個key需要重新計算映射關係(rehash),這裏K是hash表中key的數目,N是hash表中槽位的數目。相比之下,大多數傳統的hash表實現,當hash表的大小發生變化時,幾乎所有的key都需要重新映射,這是因爲key和hash表槽位之間的映射是通過取模預算實現的。
現在來看看傳說中的一致性hash是怎樣巧妙解決rehash的問題的,借用一張圖:
圖中所示就是一致性hash的全貌了。下面我們來詳細解釋圖中的細節:
1 和一般hash表使用數組表示不太一樣,一致性hash使用一個hash環來實現,因爲一般的hash函數都可以返回一個int型的整數,所以將hash環平均分成2的32次方份,然後key的hashcode對2的32次方取模,一定會落到環上的一點。
2 各個節點(比如機器名稱或者ip)的hashcode經過對2的32次方取模後,也一定會落到環上的一點。
3 如果key和機器落到同一個位置,那麼key存儲到這個節點上,如果key沒有落到某個機器節點上,那麼沿着環順時針尋找,將key存儲到遇到的第一個節點上。
4 當刪除一個節點(比如機器故障)時,獲取被刪除的節點上存儲的key時,因爲節點不存在了,所以沿着環繼續順時針走,會遇到下一個節點,這樣就將原屬於被刪除節點的key移動到了下一個節點上,而所有屬於其他節點的key並不受影響,無需重新分配。
5 增加一個節點時,也是同樣的道理,這裏不再詳細描述。
缺點
當hash環中的機器節點較少或者節點位置比較靠近時容易造成數據分佈不均勻的情況,就向下面這樣:
大多數key都被分配到節點A上,只有很少一部分key會被分配到節點B上,造成分佈的極度不均衡。
解決辦法
爲了解決數據分佈不均勻的缺點,我們可以在hash環中添加一些虛擬節點,使得key儘可能的均勻分佈到虛擬節點上,然後把虛擬節點存儲的數據映射到真實節點上即可。如下圖所示:
添加虛擬節點後,key1被分配到Node B存儲,key2被存儲到NodeB的虛擬節點Virtual B1上存儲,這樣最終key1、key2被分配到了Node B上,key3、key4、key5被分配到了Node A上,比之前的分佈均勻多了。
show me the code
可以認爲節點在環上是有序的,節點編號順時針遞增,java中可以用TreeMap來記錄機器節點在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 HashFunction hashFunction;
private final int numberOfReplicas;// 節點的複製因子,實際節點個數 * numberOfReplicas =
// 虛擬節點個數
private final SortedMap<Long, T> circle = new TreeMap<Long, T>();// 存儲虛擬節點的hash值到真實節點的映射
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++)
// 對於一個實際機器節點 node, 對應 numberOfReplicas 個虛擬節點
/*
* 不同的虛擬節點(i不同)有不同的hash值,但都對應同一個實際機器node
* 虛擬node一般是均衡分佈在環上的,數據存儲在順時針方向的虛擬node上
*/
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));
}
/*
* 獲得一個最近的順時針節點,根據給定的key 取Hash
* 然後再取得順時針方向上最近的一個虛擬節點對應的實際節點
*/
public T get(Object key) {
if (circle.isEmpty())
return null;
long hash = hashFunction.hash((String) key);// node 用String來表示,獲得node在哈希環中的hashCode
if (!circle.containsKey(hash)) {//數據映射在兩臺虛擬機器所在環之間,就需要按順時針方向尋找機器
//SortedMap的tailMap函數返回Map中所有大於該key的元素組成的子map
SortedMap<Long, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
//返回存儲key的真實的節點
return circle.get(hash);
}
public long getSize() {
return circle.size();
}
}
業界一些對一致性hash的應用
- OpenStack的swift對象存儲
- 亞馬遜的DynamoDB
- Apache的Cassandra