1. 程式人生 > >分散式系統中一致性雜湊演算法

分散式系統中一致性雜湊演算法

問題場景

近年來B2C、O2O等商業概念的提出和移動端的發展,使得分散式系統流行了起來。分散式系統相對於單系統,解決了流量大、系統高可用和高容錯等問題。功能強大也意味著實現起來需要更多技術的支援。例如系統訪問層的負載均衡,快取層的多例項主從複製備份,資料層的分庫分表等。

我們以負載均衡為例,常見的負載均衡方法有很多,但是它們的優缺點也都很明顯:

  • 隨機訪問策略。系統隨機訪問,缺點:可能造成伺服器負載壓力不均衡,俗話講就是撐的撐死,餓的餓死。
  • 輪詢策略。請求均勻分配,如果伺服器有效能差異,則無法實現效能好的伺服器能夠多承擔一部分。
  • 權重輪詢策略。權值需要靜態配置,無法自動調節,不適合對長連線和命中率有要求的場景。
  • Hash取模策略。不穩定,如果列表中某臺伺服器宕機,則會導致路由演算法產生變化,由此導致命中率的急劇下降。
  • 一致性雜湊策略。

以上幾個策略,排除本篇介紹的一致性雜湊,可能使用最多的就是 Hash取模策略了。Hash取模策略的缺點也是很明顯的,這種缺點也許在負載均衡的時候不是很明顯,但是在涉及資料訪問的主從備份和分庫分表中就體現明顯了。

使用Hash取模的問題

負載均衡

負載均衡時,假設現有3臺伺服器(編號分別為0、1、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永遠不會被訪問到。

當然如果你能動態獲取到當前可用伺服器的個數,亦即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

之後業務數量持續增長,又新增一臺使用者服務庫,當我們根據ID=1010去查詢資料時,路由計算方式為:

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

我們得到的路由值是0,最後的結果就不用說了,存在編號1上的資料我們去編號為0的庫上去查詢肯定是得不到查詢結果的。

為了資料可用,你需要做資料遷移,按照新的路由規則對所有使用者重新分配儲存地址。每次的庫或表的數量改變你都需要做一次全部使用者資訊資料的遷移。不用想這其中的工作量是有多費時費力了。

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

基本原理

一致性雜湊演算法在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位無符號整形),整個雜湊環如下:

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

那麼,一致性雜湊演算法與上圖中的圓環有什麼關係呢?仍然以之前描述的場景為例,假設我們有4臺伺服器,伺服器0、伺服器1、伺服器2,伺服器3,那麼,在生產環境中,這4臺伺服器肯定有自己的 IP 地址或主機名,我們使用它們各自的 IP 地址或主機名作為關鍵字進行雜湊計算,使用雜湊後的結果對2^32取模,可以使用如下公式示意:

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

最後會得到一個 [0, 2^32-1]之間的一個無符號整形數,這個整數就代表伺服器的編號。同時這個整數肯定處於[0, 2^32-1]之間,那麼,上圖中的 hash 環上必定有一個點與這個整數對應。那麼這個伺服器就可以對映到這個環上。

多個伺服器都通過這種方式進行計算,最後都會各自對映到圓環上的某個點,這樣每臺機器就能確定其在雜湊環上的位置,如下圖所示。

容錯性和可擴充套件性

那麼使用者訪問,如何分配訪問的伺服器呢?我們根據使用者的 IP 使用上面相同的函式 Hash 計算出雜湊值,並確定此資料在環上的位置,從此位置沿環 順時針行走,遇到的第一臺伺服器就是其應該定位到的伺服器。

從上圖可以看出 使用者1 順時針遇到的第一臺伺服器是 伺服器3 ,所以該使用者被分配給伺服器3來提供服務。同理可以看出使用者2被分配給了伺服器2。

1. 新增伺服器節點

如果這時需要新增一臺伺服器節點,一致性雜湊策略是如何應對的呢?如下圖所示,我們新增了一臺伺服器4,通過上述一致性雜湊演算法計算後得出它在雜湊環的位置。

可以發現,原來訪問伺服器3的使用者1現在訪問的物件是伺服器4,使用者能正常訪問且服務不需要停機就可以自動切換。

2. 刪除伺服器節點

如果這時某臺伺服器異常宕機或者運維撤銷了一臺伺服器,那麼這時會發生什麼情況呢?如下圖所示,假設我們撤銷了伺服器2。

可以看出,我們服務仍然能正常提供服務,只不過這時使用者2會被分配到服務1上了而已。

通過一致性雜湊的方式,我們提高了我們系統的容錯性和可擴充套件性,分散式節點的變動不會影響整個系統的執行且不需要我們做一些人為的調整策略。

Hash環的資料傾斜問題

一致性雜湊雖然為我們提供了穩定的切換策略,但是它也有一些小缺陷。因為 hash取模演算法得到的結果是隨機的,我們並不能保證各個服務節點能均勻的分配到雜湊環上。

例如當有4個服務節點時,我們把雜湊環認為是一個圓盤時鐘,我們並不能保證4個服務節點剛好均勻的落在時鐘的 12、3、6、9點上。

分佈不均勻就會產生一個問題,使用者的請求訪問就會不均勻,同時4個服務承受的壓力就會不均勻。這種問題現象我們稱之為,Hash環的資料傾斜問題。

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

如果想要均衡的將快取分佈到每臺伺服器上,最好能讓這每臺伺服器儘量多的、均勻的出現在hash環上,但是如上圖中所示,真實的伺服器資源只有4臺,我們怎樣憑空的讓它們多起來呢?

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

這些由實際節點虛擬複製而來的節點被稱為 "虛擬節點",即對每一個服務節點計算多個雜湊,每個計算結果位置都放置一個此服務節點,稱為虛擬節點。具體做法可以在伺服器IP或主機名的後面增加編號來實現。

如上圖所示,假如 伺服器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,……。

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

在實際應用中,通常將虛擬節點數設定為32甚至更大,因此即使很少的服務節點也能做到相對均勻的資料分佈。由於虛擬節點數量較多,與虛擬節點的對映關係也變得相對均衡了。

總結

一致性雜湊一般在分散式快取中使用的也比較多,本篇只介紹了服務的負載均衡和分散式儲存,對於分散式快取其實原理是類似的,讀者可以自己舉一反三來思考下。

其實,在分散式儲存和分散式快取中,當服務節點發生變化時(新增或減少),一致性雜湊演算法並不能杜絕資料遷移的問題,但是可以有效避免資料的全量遷移,需要遷移的只是更改的節點和它的上游節點它們兩個節點之間的那部分資料。

另外,我們都知道 hash演算法 有一個避免不了的問題,就是雜湊衝突。對於使用者請求IP的雜湊衝突,其實只是不同使用者被分配到了同一臺伺服器上,這個沒什麼影響。但是如果是服務節點有雜湊衝突呢?這會導致兩個服務節點在雜湊環上對應同一個點,其實我感覺這個問題也不大,因為一方面雜湊衝突的概率比較低,另一方面我們可以通過虛擬節點也可減少這種情況