一致性哈希(hash)算法

1、算法背景

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

 

2、應用場景

如今一致性hash算法在分佈式系統中也獲得了普遍應用,分佈式系統中涉及到集羣部署,包括緩存Redis集羣,數據庫集羣,咱們在使用Redis的時候,爲了保證Redis的高可用,提升Redis的讀寫性能,最簡單的方式咱們會作主從複製,組成Master-Master或者Master-Slave的形式,或者搭建Redis集羣,進行數據的讀寫分離,相似於數據庫的主從複製和讀寫分離。以下所示:java

 

一樣數據庫中也是,當單表數據大於500W的時候須要對其進行分庫分表,當數據量很大的時候(標準可能不同,要看Redis服務器容量)咱們一樣能夠對Redis進行相似的操做,就是分庫分表。算法

假設,咱們有一個社交網站,須要使用Redis存儲圖片資源,存儲的格式爲鍵值對,key值爲圖片名稱,value爲該圖片所在文件服務器的路徑,咱們須要根據文件名查找該文件所在文件服務器上的路徑,數據量大概有2000W左右,按照咱們約定的規則進行分庫,規則就是隨機分配,咱們能夠部署8臺緩存服務器,每臺服務器大概含有500W條數據,而且進行主從複製,示意圖以下數據庫

因爲規則是隨機的,全部咱們的一條數據都有可能存儲在任何一組Redis中,例如上圖咱們用戶查找一張名稱爲」a.png」的圖片,因爲規則是隨機的,咱們不肯定具體是在哪個Redis服務器上的,所以咱們須要進行一、二、三、4,4次查詢纔可以查詢到(也就是遍歷了全部的Redis服務器),這顯然不是咱們想要的結果,有了解過的小夥伴可能會想到,隨機的規則不行,可使用相似於數據庫中的分庫分表規則:按照Hash值、取模、按照類別、按照某一個字段值等等常見的規則就能夠出來了!好,按照咱們的主題,咱們就使用Hash的方式。後端

 

3、爲Redis集羣使用Hash

可想而知,若是咱們使用Hash的方式 hash(圖片名稱) % N ,每一張圖片在進行分庫的時候均可以定位到特定的服務器,示意圖以下:緩存

 

由於圖片的名稱是不重複的,因此,當咱們對同一個圖片名稱作相同的哈希計算時,得出的結果應該是不變的,若是咱們有4臺服務器,使用哈希後的結果對4求餘,那麼餘數必定是0、一、2或3,沒錯,正好與咱們以前的服務器編號相同。服務器

若是求餘的結果爲0, 咱們就把當前圖片名稱對應的圖片緩存在0號服務器上;若是餘數爲1,就把當前圖片名對應的圖片緩存在1號服務器上;若是餘數爲2,同理。那麼,當咱們訪問任意一個圖片的時候,只要再次對圖片名稱進行上述運算,便可得出對應的圖片應該存放在哪一臺緩存服務器上,咱們只要在這一臺服務器上查找圖片便可,若是圖片在對應的服務器上不存在,則證實對應的圖片沒有被緩存,也不用再去遍歷其餘緩存服務器了,經過這樣的方法,便可將3萬張圖片隨機的分佈到3臺緩存服務器上了,並且下次訪問某張圖片時,直接可以判斷出該圖片應該存在於哪臺緩存服務器上,這樣就能知足咱們的需求了,咱們暫時稱上述算法爲HASH算法或者取模算法。分佈式

上圖中,假設咱們查找的是」a.png」,因爲有4臺服務器(排除從庫),所以公式爲函數

hash(a.png) % 4 = 2

,可知定位到了第2號服務器,這樣的話就不會遍歷全部的服務器,大大提高了性能!性能

 

4、使用Hash的問題

上述的方式雖然提高了性能,咱們再也不須要對整個Redis服務器進行遍歷!可是,使用上述HASH算法進行緩存時,會出現一些缺陷,主要體如今服務器數量變更的時候,全部緩存的位置都要發生改變!

試想一下,若是3臺緩存服務器已經不能知足咱們的緩存需求,那麼咱們應該怎麼作呢?沒錯,很簡單,多增長兩臺緩存服務器不就好了,假設,咱們增長了一臺緩存服務器,那麼緩存服務器的數量就由4臺變成了5臺,此時,若是仍然使用上述方法對同一張圖片進行緩存,那麼這張圖片所在的服務器編號一定與原來4臺服務器時所在的服務器編號不一樣,由於除數由4變爲了5,被除數不變的狀況下,餘數確定不一樣,這種狀況帶來的結果就是當服務器數量變更時,全部緩存的位置都要發生改變,換句話說,當服務器數量發生改變時,全部緩存在必定時間內是失效的,當應用沒法從緩存中獲取數據時,則會向後端服務器請求數據,同理,假設4臺緩存中忽然有一臺緩存服務器出現了故障,沒法進行緩存,那麼咱們則須要將故障機器移除,可是若是移除了一臺緩存服務器,那麼緩存服務器數量從4臺變爲3臺,若是想要訪問一張圖片,這張圖片的緩存位置一定會發生改變,之前緩存的圖片也會失去緩存的做用與意義,因爲大量緩存在同一時間失效,形成了緩存的雪崩,此時前端緩存已經沒法起到承擔部分壓力的做用,後端服務器將會承受巨大的壓力,整個系統頗有可能被壓垮,因此,咱們應該想辦法不讓這種狀況發生,可是因爲上述HASH算法自己的緣故,使用取模法進行緩存時,這種狀況是沒法避免的。

咱們來回顧一下使用上述算法會出現的問題。

問題1:當緩存服務器數量發生變化時,會引發緩存的雪崩,可能會引發總體系統壓力過大而崩潰(大量緩存同一時間失效)。

問題2:當緩存服務器數量發生變化時,幾乎全部緩存的位置都會發生改變,怎樣才能儘可能減小受影響的緩存呢?

其實,上面兩個問題是一個問題,那麼,一致性哈希算法可以解決上述問題嗎?解決這些問題,一致性哈希算法誕生了。

 

5、一致性哈希的基本概念

一致性Hash算法也是使用取模的方法,只是,剛纔描述的取模法是對服務器的數量進行取模,而一致性Hash算法是對2^32取模,什麼意思呢?簡單來講,一致性Hash算法將整個哈希值空間組織成一個虛擬的圓環,如假設某哈希函數H的值空間爲0-2^32-1(即哈希值是一個32位無符號整形),整個哈希環以下:

整個空間按順時針方向組織,圓環的正上方的點表明0,0點右側的第一個點表明1,以此類推,二、三、四、五、6……直到2^32-1,也就是說0點左側的第一個點表明2^32-1, 0和2^32-1在零點中方向重合,咱們把這個由2^32個點組成的圓環稱爲Hash環

那麼,一致性哈希算法與上圖中的圓環有什麼關係呢?咱們繼續聊,仍然以以前描述的場景爲例,假設咱們有4臺緩存服務器,服務器A、服務器B、服務器C,服務器D,那麼,在生產環境中,這4臺服務器確定有本身的IP地址或主機名,咱們使用它們各自的IP地址或主機名做爲關鍵字進行哈希計算,使用哈希後的結果對2^32取模,可使用以下公式示意:

hash(服務器A的IP地址) %  2^32

經過上述公式算出的結果必定是一個0到2^32-1之間的一個整數,咱們就用算出的這個整數,表明服務器A,既然這個整數確定處於0到2^32-1之間,那麼,上圖中的hash環上一定有一個點與這個整數對應,而咱們剛纔已經說明,使用這個整數表明服務器A,那麼,服務器A就能夠映射到這個環上。

以此類推,下一步將各個服務器使用相似的Hash算式進行一個哈希,這樣每臺機器就能肯定其在哈希環上的位置,這裏假設將上文中四臺服務器使用IP地址哈希後在環空間的位置以下:

 

接下來使用以下算法定位數據訪問到相應服務器:  將數據key使用相同的函數Hash計算出哈希值,並肯定此數據在環上的位置,今後位置沿環順時針「行走」,第一臺遇到的服務器就是其應該定位到的服務器

例如咱們有Object A、Object B、Object C、Object D四個數據對象,通過哈希計算後,在環空間上的位置以下:

 

根據一致性Hash算法,數據A會被定爲到Node A上,B被定爲到Node B上,C被定爲到Node C上,D被定爲到Node D上。

說到這裏可能會有疑問,爲何hash一致性的數據空間範圍是2^32次方?

由於,java中int的最大值是2^31-1最小值是-2^31,2^32恰好是無符號整形的最大值;

進一步追尾基礎,爲何java中int的最大值是2^31-1最小值是-2^31?

由於,int的最大值最小值範圍設定是由於一個int佔4個字節,一個字節佔8位,二進制中恰好是32位。(基礎忘記的須要惡補一下了)

 

6、一致性Hash算法的容錯性和可擴展性

現假設Node C不幸宕機,能夠看到此時對象A、B、D不會受到影響,只有C對象被重定位到Node D。通常的,在一致性Hash算法中,若是一臺服務器不可用,則受影響的數據僅僅是此服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它不會受到影響,以下所示:

下面考慮另一種狀況,若是在系統中增長一臺服務器Node X,以下圖所示:

此時對象Object A、B、D不受影響,只有對象C須要重定位到新的Node X !通常的,在一致性Hash算法中,若是增長一臺服務器,則受影響的數據僅僅是新服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它數據也不會受到影響。

綜上所述,一致性Hash算法對於節點的增減都只需重定位環空間中的一小部分數據,具備較好的容錯性和可擴展性。

 

7、Hash環的數據傾斜問題

一致性Hash算法在服務節點太少時,容易由於節點分部不均勻而形成數據傾斜(被緩存的對象大部分集中緩存在某一臺服務器上)問題,例如系統中只有兩臺服務器,其環分佈以下:

此時必然形成大量數據集中到Node A上,而只有極少許會定位到Node B上,從而出現hash環偏斜的狀況,當hash環偏斜之後,緩存每每會極度不均衡的分佈在各服務器上,若是想要均衡的將緩存分佈到2臺服務器上,最好能讓這2臺服務器儘可能多的、均勻的出如今hash環上,可是,真實的服務器資源只有2臺,咱們怎樣憑空的讓它們多起來呢,沒錯,就是憑空的讓服務器節點多起來,既然沒有多餘的真正的物理服務器節點,咱們就只能將現有的物理節點經過虛擬的方法複製出來。

這些由實際節點虛擬複製而來的節點被稱爲"虛擬節點",即對每個服務節點計算多個哈希,每一個計算結果位置都放置一個此服務節點,稱爲虛擬節點。具體作法能夠在服務器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甚至更大,所以即便不多的服務節點也能作到相對均勻的數據分佈。

 

8、總結

上文中,咱們一步步分析了什麼是一致性Hash算法,主要是考慮到分佈式系統每一個節點都有可能失效,而且新的節點極可能動態的增長進來的狀況,如何保證當系統的節點數目發生變化的時候,咱們的系統仍然可以對外提供良好的服務,這是值得考慮的!

 

參考:白話解析:一致性哈希算法 consistent hashing

相關文章
相關標籤/搜索