一、算法背景
一致性哈希算法在1997年由麻省理工學(xué)院的Karger等人在解決分布式Cache中提出的,設(shè)計目標(biāo)是為了解決因特網(wǎng)中的熱點(Hot spot)問題,初衷和CARP十分類似。一致性哈希修正了CARP使用的簡單哈希算法帶來的問題,使得DHT可以在P2P環(huán)境中真正得到應(yīng)用。
二、應(yīng)用場景
現(xiàn)在一致性hash算法在分布式系統(tǒng)中也得到了廣泛應(yīng)用,分布式系統(tǒng)中涉及到集群部署,包括緩存Redis集群,數(shù)據(jù)庫集群,我們在使用Redis的時候,為了保證Redis的高可用,提高Redis的讀寫性能,最簡單的方式我們會做主從復(fù)制,組成Master-Master或者M(jìn)aster-Slave的形式,或者搭建Redis集群,進(jìn)行數(shù)據(jù)的讀寫分離,類似于數(shù)據(jù)庫的主從復(fù)制和讀寫分離。如下所示:

同樣數(shù)據(jù)庫中也是,當(dāng)單表數(shù)據(jù)大于500W的時候需要對其進(jìn)行分庫分表,當(dāng)數(shù)據(jù)量很大的時候(標(biāo)準(zhǔn)可能不一樣,要看Redis服務(wù)器容量)我們同樣可以對Redis進(jìn)行類似的操作,就是分庫分表。
假設(shè),我們有一個社交網(wǎng)站,需要使用Redis存儲圖片資源,存儲的格式為鍵值對,key值為圖片名稱,value為該圖片所在文件服務(wù)器的路徑,我們需要根據(jù)文件名查找該文件所在文件服務(wù)器上的路徑,數(shù)據(jù)量大概有2000W左右,按照我們約定的規(guī)則進(jìn)行分庫,規(guī)則就是隨機(jī)分配,我們可以部署8臺緩存服務(wù)器,每臺服務(wù)器大概含有500W條數(shù)據(jù),并且進(jìn)行主從復(fù)制,示意圖如下

由于規(guī)則是隨機(jī)的,所有我們的一條數(shù)據(jù)都有可能存儲在任何一組Redis中,例如上圖我們用戶查找一張名稱為”a.png”的圖片,由于規(guī)則是隨機(jī)的,我們不確定具體是在哪一個Redis服務(wù)器上的,因此我們需要進(jìn)行1、2、3、4,4次查詢才能夠查詢到(也就是遍歷了所有的Redis服務(wù)器),這顯然不是我們想要的結(jié)果,有了解過的小伙伴可能會想到,隨機(jī)的規(guī)則不行,可以使用類似于數(shù)據(jù)庫中的分庫分表規(guī)則:按照Hash值、取模、按照類別、按照某一個字段值等等常見的規(guī)則就可以出來了!好,按照我們的主題,我們就使用Hash的方式。
三、為Redis集群使用Hash
可想而知,如果我們使用Hash的方式hash(圖片名稱) % N,每一張圖片在進(jìn)行分庫的時候都可以定位到特定的服務(wù)器,示意圖如下:

因為圖片的名稱是不重復(fù)的,所以,當(dāng)我們對同一個圖片名稱做相同的哈希計算時,得出的結(jié)果應(yīng)該是不變的,如果我們有4臺服務(wù)器,使用哈希后的結(jié)果對4求余,那么余數(shù)一定是0、1、2或3,沒錯,正好與我們之前的服務(wù)器編號相同。
如果求余的結(jié)果為0, 我們就把當(dāng)前圖片名稱對應(yīng)的圖片緩存在0號服務(wù)器上;如果余數(shù)為1,就把當(dāng)前圖片名對應(yīng)的圖片緩存在1號服務(wù)器上;如果余數(shù)為2,同理。那么,當(dāng)我們訪問任意一個圖片的時候,只要再次對圖片名稱進(jìn)行上述運算,即可得出對應(yīng)的圖片應(yīng)該存放在哪一臺緩存服務(wù)器上,我們只要在這一臺服務(wù)器上查找圖片即可,如果圖片在對應(yīng)的服務(wù)器上不存在,則證明對應(yīng)的圖片沒有被緩存,也不用再去遍歷其他緩存服務(wù)器了,通過這樣的方法,即可將3萬張圖片隨機(jī)的分布到3臺緩存服務(wù)器上了,而且下次訪問某張圖片時,直接能夠判斷出該圖片應(yīng)該存在于哪臺緩存服務(wù)器上,這樣就能滿足我們的需求了,我們暫時稱上述算法為HASH算法或者取模算法。
上圖中,假設(shè)我們查找的是”a.png”,由于有4臺服務(wù)器(排除從庫),因此公式為
hash(a.png) % 4 = 2
,可知定位到了第2號服務(wù)器,這樣的話就不會遍歷所有的服務(wù)器,大大提升了性能!
四、使用Hash的問題
上述的方式雖然提升了性能,我們不再需要對整個Redis服務(wù)器進(jìn)行遍歷!但是,使用上述HASH算法進(jìn)行緩存時,會出現(xiàn)一些缺陷,主要體現(xiàn)在服務(wù)器數(shù)量變動的時候,所有緩存的位置都要發(fā)生改變!
試想一下,如果3臺緩存服務(wù)器已經(jīng)不能滿足我們的緩存需求,那么我們應(yīng)該怎么做呢?沒錯,很簡單,多增加兩臺緩存服務(wù)器不就行了,假設(shè),我們增加了一臺緩存服務(wù)器,那么緩存服務(wù)器的數(shù)量就由4臺變成了5臺,此時,如果仍然使用上述方法對同一張圖片進(jìn)行緩存,那么這張圖片所在的服務(wù)器編號必定與原來4臺服務(wù)器時所在的服務(wù)器編號不同,因為除數(shù)由4變?yōu)榱?,被除數(shù)不變的情況下,余數(shù)肯定不同,這種情況帶來的結(jié)果就是當(dāng)服務(wù)器數(shù)量變動時,所有緩存的位置都要發(fā)生改變,換句話說,當(dāng)服務(wù)器數(shù)量發(fā)生改變時,所有緩存在一定時間內(nèi)是失效的,當(dāng)應(yīng)用無法從緩存中獲取數(shù)據(jù)時,則會向后端服務(wù)器請求數(shù)據(jù),同理,假設(shè)4臺緩存中突然有一臺緩存服務(wù)器出現(xiàn)了故障,無法進(jìn)行緩存,那么我們則需要將故障機(jī)器移除,但是如果移除了一臺緩存服務(wù)器,那么緩存服務(wù)器數(shù)量從4臺變?yōu)?臺,如果想要訪問一張圖片,這張圖片的緩存位置必定會發(fā)生改變,以前緩存的圖片也會失去緩存的作用與意義,由于大量緩存在同一時間失效,造成了緩存的雪崩,此時前端緩存已經(jīng)無法起到承擔(dān)部分壓力的作用,后端服務(wù)器將會承受巨大的壓力,整個系統(tǒng)很有可能被壓垮,所以,我們應(yīng)該想辦法不讓這種情況發(fā)生,但是由于上述HASH算法本身的緣故,使用取模法進(jìn)行緩存時,這種情況是無法避免的。
我們來回顧一下使用上述算法會出現(xiàn)的問題。
問題1:當(dāng)緩存服務(wù)器數(shù)量發(fā)生變化時,會引起緩存的雪崩,可能會引起整體系統(tǒng)壓力過大而崩潰(大量緩存同一時間失效)。
問題2:當(dāng)緩存服務(wù)器數(shù)量發(fā)生變化時,幾乎所有緩存的位置都會發(fā)生改變,怎樣才能盡量減少受影響的緩存呢?
其實,上面兩個問題是一個問題,那么,一致性哈希算法能夠解決上述問題嗎?解決這些問題,一致性哈希算法誕生了。
五、一致性哈希的基本概念
一致性Hash算法也是使用取模的方法,只是,剛才描述的取模法是對服務(wù)器的數(shù)量進(jìn)行取模,而一致性Hash算法是對2^32取模,什么意思呢?簡單來說,一致性Hash算法將整個哈希值空間組織成一個虛擬的圓環(huán),如假設(shè)某哈希函數(shù)H的值空間為0-2^32-1(即哈希值是一個32位無符號整形),整個哈希環(huán)如下:

整個空間按順時針方向組織,圓環(huán)的正上方的點代表0,0點右側(cè)的第一個點代表1,以此類推,2、3、4、5、6……直到2^32-1,也就是說0點左側(cè)的第一個點代表2^32-1, 0和2^32-1在零點中方向重合,我們把這個由2^32個點組成的圓環(huán)稱為Hash環(huán)。
那么,一致性哈希算法與上圖中的圓環(huán)有什么關(guān)系呢?我們繼續(xù)聊,仍然以之前描述的場景為例,假設(shè)我們有4臺緩存服務(wù)器,服務(wù)器A、服務(wù)器B、服務(wù)器C,服務(wù)器D,那么,在生產(chǎn)環(huán)境中,這4臺服務(wù)器肯定有自己的IP地址或主機(jī)名,我們使用它們各自的IP地址或主機(jī)名作為關(guān)鍵字進(jìn)行哈希計算,使用哈希后的結(jié)果對2^32取模,可以使用如下公式示意:
hash(服務(wù)器A的IP地址) %? 2^32
通過上述公式算出的結(jié)果一定是一個0到2^32-1之間的一個整數(shù),我們就用算出的這個整數(shù),代表服務(wù)器A,既然這個整數(shù)肯定處于0到2^32-1之間,那么,上圖中的hash環(huán)上必定有一個點與這個整數(shù)對應(yīng),而我們剛才已經(jīng)說明,使用這個整數(shù)代表服務(wù)器A,那么,服務(wù)器A就可以映射到這個環(huán)上。
以此類推,下一步將各個服務(wù)器使用類似的Hash算式進(jìn)行一個哈希,這樣每臺機(jī)器就能確定其在哈希環(huán)上的位置,這里假設(shè)將上文中四臺服務(wù)器使用IP地址哈希后在環(huán)空間的位置如下:

接下來使用如下算法定位數(shù)據(jù)訪問到相應(yīng)服務(wù)器:將數(shù)據(jù)key使用相同的函數(shù)Hash計算出哈希值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時針“行走”,第一臺遇到的服務(wù)器就是其應(yīng)該定位到的服務(wù)器!
例如我們有Object A、Object B、Object C、Object D四個數(shù)據(jù)對象,經(jīng)過哈希計算后,在環(huán)空間上的位置如下:

根據(jù)一致性Hash算法,數(shù)據(jù)A會被定為到Node A上,B被定為到Node B上,C被定為到Node C上,D被定為到Node D上。
說到這里可能會有疑問,為什么hash一致性的數(shù)據(jù)空間范圍是2^32次方?
因為,java中int的最大值是2^31-1最小值是-2^31,2^32剛好是無符號整形的最大值;
進(jìn)一步追尾基礎(chǔ),為什么java中int的最大值是2^31-1最小值是-2^31?
因為,int的最大值最小值范圍設(shè)定是因為一個int占4個字節(jié),一個字節(jié)占8位,二進(jìn)制中剛好是32位。(基礎(chǔ)忘記的需要惡補(bǔ)一下了)
六、一致性Hash算法的容錯性和可擴(kuò)展性
現(xiàn)假設(shè)Node C不幸宕機(jī),可以看到此時對象A、B、D不會受到影響,只有C對象被重定位到Node D。一般的,在一致性Hash算法中,如果一臺服務(wù)器不可用,則受影響的數(shù)據(jù)僅僅是此服務(wù)器到其環(huán)空間中前一臺服務(wù)器(即沿著逆時針方向行走遇到的第一臺服務(wù)器)之間數(shù)據(jù),其它不會受到影響,如下所示:

下面考慮另外一種情況,如果在系統(tǒng)中增加一臺服務(wù)器Node X,如下圖所示:

此時對象Object A、B、D不受影響,只有對象C需要重定位到新的Node X !一般的,在一致性Hash算法中,如果增加一臺服務(wù)器,則受影響的數(shù)據(jù)僅僅是新服務(wù)器到其環(huán)空間中前一臺服務(wù)器(即沿著逆時針方向行走遇到的第一臺服務(wù)器)之間數(shù)據(jù),其它數(shù)據(jù)也不會受到影響。
綜上所述,一致性Hash算法對于節(jié)點的增減都只需重定位環(huán)空間中的一小部分?jǐn)?shù)據(jù),具有較好的容錯性和可擴(kuò)展性。
七、Hash環(huán)的數(shù)據(jù)傾斜問題
一致性Hash算法在服務(wù)節(jié)點太少時,容易因為節(jié)點分部不均勻而造成數(shù)據(jù)傾斜(被緩存的對象大部分集中緩存在某一臺服務(wù)器上)問題,例如系統(tǒng)中只有兩臺服務(wù)器,其環(huán)分布如下:

此時必然造成大量數(shù)據(jù)集中到Node A上,而只有極少量會定位到Node B上,從而出現(xiàn)hash環(huán)偏斜的情況,當(dāng)hash環(huán)偏斜以后,緩存往往會極度不均衡的分布在各服務(wù)器上,如果想要均衡的將緩存分布到2臺服務(wù)器上,最好能讓這2臺服務(wù)器盡量多的、均勻的出現(xiàn)在hash環(huán)上,但是,真實的服務(wù)器資源只有2臺,我們怎樣憑空的讓它們多起來呢,沒錯,就是憑空的讓服務(wù)器節(jié)點多起來,既然沒有多余的真正的物理服務(wù)器節(jié)點,我們就只能將現(xiàn)有的物理節(jié)點通過虛擬的方法復(fù)制出來。
這些由實際節(jié)點虛擬復(fù)制而來的節(jié)點被稱為"虛擬節(jié)點",即對每一個服務(wù)節(jié)點計算多個哈希,每個計算結(jié)果位置都放置一個此服務(wù)節(jié)點,稱為虛擬節(jié)點。具體做法可以在服務(wù)器IP或主機(jī)名的后面增加編號來實現(xiàn)。
例如上面的情況,可以為每臺服務(wù)器計算三個虛擬節(jié)點,于是可以分別計算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六個虛擬節(jié)點:

同時數(shù)據(jù)定位算法不變,只是多了一步虛擬節(jié)點到實際節(jié)點的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三個虛擬節(jié)點的數(shù)據(jù)均定位到Node A上。這樣就解決了服務(wù)節(jié)點少時數(shù)據(jù)傾斜的問題。在實際應(yīng)用中,通常將虛擬節(jié)點數(shù)設(shè)置為32甚至更大,因此即使很少的服務(wù)節(jié)點也能做到相對均勻的數(shù)據(jù)分布。
八、總結(jié)
上文中,我們一步步分析了什么是一致性Hash算法,主要是考慮到分布式系統(tǒng)每個節(jié)點都有可能失效,并且新的節(jié)點很可能動態(tài)的增加進(jìn)來的情況,如何保證當(dāng)系統(tǒng)的節(jié)點數(shù)目發(fā)生變化的時候,我們的系統(tǒng)仍然能夠?qū)ν馓峁┝己玫姆?wù),這是值得考慮的!