分佈式系統中一致性哈希算法

分佈式系統中一致性哈希算法

業務場景

近年來,因爲互聯網的興起,B2C、O2O等商業概念的提出和移動端的發展,使得分佈式系統流行起來。分佈式系統相對於單一系統而言,帶來了流量大、系統高可用和高容錯的便利。功能強大的同時,也意味着實現起來須要更多的技術支持。例如系統訪問層的負債均衡、緩存層的多實例主從複製備份以及數據層的分庫分表等。java

咱們以負載均衡爲例,常見的負載均衡方法有不少,可是它們的優缺點都很明顯:算法

  • 隨機訪問策略:系統隨機訪問,缺點:可能形成服務器負載壓力不均衡,俗話講就是撐的撐死,餓的餓死。
  • 輪詢策略:請求均勻分配,若是服務器有性能差別,則沒法實現性能好的服務器可以多承擔一部分。
  • 權重輪詢策略:權值須要靜態配置,沒法自動調節,不適合對長鏈接和命中率有要求的場景。
  • Hash取模策略:不穩定,若是列表中某臺服務器宕機,則會致使路由算法產生變化,由此致使命中率的急劇降低。
  • 一致性哈希策略(本文重點分析)

以上幾個策略,排除本篇介紹的一致性哈希策略,可能使用最多的就是 Hash取模策略了。Hash取模策略的缺點也是很明顯的,這種缺點也許在負載均衡的時候不是很明顯,可是在涉及數據緩存的主從備份和數據庫分庫分表中就體現的較爲明顯了。sql

使用Hash取模的問題

負載均衡

負載均衡時,假設現有3臺服務器(編號分別爲0、一、2),使用哈希取模的計算方式則是:對訪問者的IP,經過固定算式hash(IP) % N(N爲服務器的個數),使得每一個IP均可以定位到特定的服務器。數據庫

例如現有IP地址 10.58.34.31,對IP哈希取模策時,計算結果爲2,即訪問編號爲2的服務器:緩存

String ip = "10.58.34.31";
int v1 = hash(ip) % 3;
System.out.println("訪問服務器:" + v1);// 訪問服務器:2

若是此時服務器2宕機了,則會致使全部計算結果爲2的 IP 對應的用戶都訪問異常(包括上例中的IP)。或者你新增了一臺服務器3,這時不修改N值的話那麼服務器3永遠不會被訪問到。服務器

![](E:Java NotePersonalNote分佈式架構理論image傳統分佈式算法(Hash取模策略).png)

固然若是你能動態獲取到當前可用服務器的個數,亦即N值是根據當前可用服務器個數動態來變化的,則可解決此問題。可是對於特定地區或特定IP訪問特定服務器類的需求會形成訪問誤差。架構

分庫分表(分佈式存儲)

負載均衡中有這種問題,那麼分庫分表中一樣也有這樣的問題。例如隨着業務的飛速增加,咱們的註冊用戶也愈來愈多,單個用戶表數量已經達到千萬級甚至更大。因爲Mysql的單表建議百萬級數據存儲,因此這時爲了保證系統查詢和運行效率,確定會考慮到分庫分表。負載均衡

對於分庫分表,數據的分配是個重要的問題,你須要保證數據分配在這個服務器,那麼在查詢時也須要到該服務器上來查詢,不然會形成數據查詢丟失的問題。運維

一般是根據用戶的 ID 哈希取模獲得的值而後路由到對應的存儲位置,計算公式爲:hash(userId) % N,其中N爲分庫或分表的個數。分佈式

例如分庫數爲2時,計算結果爲1,則ID爲1010的用戶存儲在編號爲1對應的庫中:

String userId = "1010";
int v1 = hash(userId) % 2;
System.out.println("存儲:" + v1);// 存儲:1

![](E:Java NotePersonalNote分佈式架構理論image數據庫分庫分表流程_1.PNG)

以後業務數量持續增加,又新增一臺用戶服務庫,當咱們根據ID=1010去查詢數據時,路由計算方式爲:

int v2 = hash(userId) % 3;
System.out.println("存儲:" + v2);// 存儲:0

咱們獲得的路由值是0,最後的結果就不用說了,存在編號1上的數據咱們去編號爲0的庫上去查詢確定是得不到查詢結果的。

![](E:Java NotePersonalNote分佈式架構理論image數據庫分庫分表流程_2.PNG)

爲了數據可用,你須要作數據遷移,按照新的路由規則對全部用戶從新分配存儲地址。每次的庫或表的數量改變你都須要作一次所有用戶信息數據的遷移。不用想這其中的工做量是有多費時費力了。

是否有某種方法,有效解決這種<u>分佈式存儲結構下動態增長或刪除節點</u>所帶來的問題,能保證這種不受實例數量變化影響而準確路由到正確的實例上的算法或實現機制呢?解決這些問題,一致性哈希算法誕生了。

一致性哈希算法

基本思想原理

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

上面說的哈希取模方法,它是針對一個點的,業務佈局嚴重依賴於這個計算的點值結果。你結算的結果是2,那麼就對應到編號爲2的服務器上。這樣的映射就形成了業務容錯性和可擴展性極低。

咱們思考下,是否能夠將這個計算結果的點值賦予範圍的意義?咱們知道Hash取模以後獲得的是一個 int 型的整值。

// Objects 類中默認的 hash 方法
public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

既然 hash的計算結果是 int 類型,而 java 中 int 的最小值是-2^31,最大值是2^31-1。意味着任何經過哈希取模以後的無符號值都會在 0 ~ 2^31-1範圍之間,共2^32個數。那咱們是否能夠不對服務器的數量進行取模而是直接對2^32取模。這就造成了一致性哈希的基本算法思想,什麼意思呢?

這裏須要注意一點:

默認的 hash 方法結果是有負值的狀況,所以須要咱們重寫hash方法,保證哈希值的非負性。

簡單來講,一致性Hash算法將整個哈希值空間組織成一個虛擬的圓環,如假設某哈希函數 H 的值空間爲 0 ~ 2^32-1(即哈希值是一個32位無符號整形),整個哈希環以下:

![](E:Java NotePersonalNote分佈式架構理論image201908252317.JPG)

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

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

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

最後會獲得一個 [0, 2^32-1]之間的一個無符號整形數,這個整數就表明服務器的編號。同時這個整數確定處於[0, 2^32-1]之間,那麼,上圖中的 hash 環上一定有一個點與這個整數對應。那麼這個服務器就能夠映射到這個環上。

多個服務器都經過這種方式進行計算,最後都會各自映射到圓環上的某個點,這樣每臺機器就能肯定其在哈希環上的位置,以下圖所示:

![](E:Java NotePersonalNote分佈式架構理論image201908252337.JPG)

如何提升容錯性和擴展性的

那麼用戶訪問,如何分配訪問的服務器呢?咱們根據用戶的 IP 使用上面相同的函數 Hash 計算出哈希值,並肯定此數據在環上的位置,今後位置沿環 順時針行走,遇到的第一臺服務器就是其應該定位到的服務器。

![](E:Java NotePersonalNote分佈式架構理論image201908252339.JPG)

從上圖能夠看出 用戶1 順時針遇到的第一臺服務器是 服務器3 ,因此該用戶被分配給服務器3來提供服務。同理能夠看出用戶2被分配給了服務器2。

新增服務器節點

若是這時須要新增一臺服務器節點,一致性哈希策略是如何應對的呢?以下圖所示,咱們新增了一臺服務器4,經過上述一致性哈希算法計算後得出它在哈希環的位置。

![](E:Java NotePersonalNote分佈式架構理論image201908252341.JPG)

能夠發現,原來訪問服務器3的用戶1如今訪問的對象是服務器4,用戶能正常訪問且服務不須要停機就能夠自動切換。

刪除服務器節點

若是這時某臺服務器異常宕機或者運維撤銷了一臺服務器,那麼這時會發生什麼狀況呢?以下圖所示,假設咱們撤銷了服務器2。

![](E:Java NotePersonalNote分佈式架構理論image201908252342.JPG)

能夠看出,咱們服務仍然能正常提供服務,只不過這時用戶2會被分配到服務1上了而已。

經過一致性哈希的方式,咱們提升了咱們系統的容錯性和可擴展性,分佈式節點的變更不會影響整個系統的運行且不須要咱們作一些人爲的調整策略。

Hash 環的數據傾斜性

一致性哈希雖然爲咱們提供了穩定的切換策略,可是它也有一些小缺陷。由於 hash取模算法獲得的結果是隨機的,咱們並不能保證各個服務節點能均勻的分配到哈希環上。

例如當有4個服務節點時,咱們把哈希環認爲是一個圓盤時鐘,咱們並不能保證4個服務節點恰好均勻的落在時鐘的 十二、三、六、9點上。

分佈不均勻就會產生一個問題,用戶的請求訪問就會不均勻,同時4個服務承受的壓力就會不均勻。這種問題現象咱們稱之爲,Hash環的數據傾斜問題

![](E:Java NotePersonalNote分佈式架構理論image201908252353.JPG)

如上圖所示,服務器0 到 服務器1 之間的哈希點值佔據比例最大,大量請求會集中到 服務器1 上,而只有極少許會定位到 服務器0 或其餘幾個節點上,從而出現 hash環偏斜的狀況。

若是想要均衡的將緩存分佈到每臺服務器上,最好能讓這每臺服務器儘可能多的、均勻的出如今hash環上,可是如上圖中所示,真實的服務器資源只有4臺,咱們怎樣憑空的讓它們多起來呢?

既然沒有多餘的真正的物理服務器節點,咱們就只能將現有的物理節點經過虛擬的方法複製出來。

這些<u>由實際節點虛擬複製而來的節點</u>被稱爲 "虛擬節點",即對每個服務節點計算多個哈希,每一個計算結果位置都放置一個此服務節點,稱爲虛擬節點。

![](E:Java NotePersonalNote分佈式架構理論image虛擬節點.JPG)

假如服務器1 的 IP 是 192.168.32.132,那麼原服務器1 節點在環形空間的位置就是hash("192.168.32.132") % 2^32

咱們基於 服務器1 構建兩個虛擬節點,Server1-A 和 Server1-B,虛擬節點在環形空間的位置能夠利用(IP+後綴)計算,例如:

hash("192.168.32.132#A") % 2^32
hash("192.168.32.132#B") % 2^32

此時,環形空間中再也不有物理節點 服務器1,服務器2,……,替代的是隻有虛擬節點 Server1-A,Server1-B,Server2-A,Server2-B,……。

解決Hash傾斜性

同時數據定位算法不變,只是多了一步虛擬節點到實際節點的映射,例如定位到 「Server1-A」、「Server1-B」 兩個虛擬節點的數據均定位到 服務器1上。這樣就解決了服務節點少時數據傾斜的問題。

在實際應用中,一般將虛擬節點數設置爲32甚至更大,所以即便不多的服務節點也能作到相對均勻的數據分佈。因爲虛擬節點數量較多,與虛擬節點的映射關係也變得相對均衡了。

咱們再來看下 一致性哈希算法的命中率計算公式:(1-n/(n+m))*100%,n 表明服務器臺數,m 表明變更服務器臺數,當服務器樣本臺數足夠大,則當變更服務器臺數越大時,命中率會越小,影響會愈來愈小。

總結

一致性哈希通常在分佈式緩存中使用的也比較多,本篇只介紹了服務的負載均衡和分佈式存儲,對於分佈式緩存其實原理是相似的,讀者能夠本身觸類旁通來思考下。

其實,在分佈式存儲和分佈式緩存中,當服務節點發生變化時(新增或減小),一致性哈希算法並不能杜絕數據遷移的問題,可是能夠有效避免數據的全量遷移,須要遷移的只是更改的節點和它的上游節點它們兩個節點之間的那部分數據。

另外,咱們都知道 hash算法 有一個避免不了的問題,就是哈希衝突。對於用戶請求 IP 的哈希衝突,其實只是不一樣用戶被分配到了同一臺服務器上,這個沒什麼影響。可是若是是服務節點有哈希衝突呢?這會致使兩個服務節點在哈希環上對應同一個點,其實我感受這個問題也不大,由於一方面哈希衝突的機率比較低,另外一方面咱們能夠經過虛擬節點也可減小這種狀況。

相關文章
相關標籤/搜索