【分佈式系統】深入理解一致性 Hash 算法 【分佈式系統】深入理解一致性 Hash 算法 Hash算法原理 一致性hash算法的原理 總結

【分佈式系統】深入理解一致性 Hash 算法

近年來B2C、O2O等商業概念的提出和移動端的發展,使得分佈式系統流行了起來。分佈式系統相對於單系統,解決了流量大、系統高可用和高容錯等問題。功能強大也意味着實現起來需要更多技術的支持。例如系統訪問層的負載均衡,緩存層的多實例主從複製備份,數據層的分庫分表等。我們以負載均衡爲例,常見的負載均衡方法有很多,但是它們的優缺點也都很明顯:

隨機訪問策略。系統隨機訪問,缺點:可能造成服務器負載壓力不均衡,俗話講就是撐的撐死,餓的餓死。

輪詢策略。請求均勻分配,如果服務器有性能差異,則無法實現性能好的服務器能夠多承擔一部分。

權重輪詢策略。權值需要靜態配置,無法自動調節,不適合對長連接和命中率有要求的場景。

Hash取模策略。不穩定,如果列表中某臺服務器宕機,則會導致路由算法產生變化,由此導致命中率的急劇下降。

一致性哈希策略

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

Hash算法原理

Hash,一般翻譯做散列,也有直接音譯爲哈希,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值,下圖爲Hash算法中常用的除留餘數法。

根據上面的算法,普通的Hash算法均勻地將這些數據項打散到了這些節點上,並且分佈最少和最多的存儲節點數據項數目小於1%。之所以分佈均勻,主要是依賴Hash算法(實現使用的MD5算法)能夠比較隨機的分佈。

使用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的庫上去查詢肯定是得不到查詢結果的。

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

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

一致性hash算法的原理

consistent hashing 是一種 hash 算法,簡單的說,在移除 / 添加一個 cache 時,它能夠儘可能小的改變已存在 key 映射關係,儘可能的滿足單調性的要求。下面就來按照 5 個步驟簡單講講 consistent hashing 算法的基本原理。

環形hash 空間

考慮通常的 hash 算法都是將 value 映射到一個 32 位的 key 值,也即是 0~2^32-1 次方的數值空間;我們可以將這個空間想象成一個首( 0 )尾( 2^32-1 )相接的圓環,如下面圖 1 所示的那樣。

把對象映射到hash 空間

接下來考慮 4 個對象 object1~object4 ,通過 hash 函數計算出的 hash 值 key 在環上的分佈如圖 2 所示。

hash(object1) = key1;

… …

hash(object4) = key4;

把cache 映射到hash 空間

Consistent hashing 的基本思想就是將對象和 cache 都映射到同一個 hash 數值空間中,並且使用相同的 hash 算法。假設當前有 A,B 和 C 共 3 臺 cache ,那麼其映射結果將如圖 3 所示,他們在 hash 空間中,以對應的 hash 值排列。

hash(cache A) = key A;

… …

hash(cache C) = key C;

說到這裏,順便提一下 cache 的 hash 計算,一般的方法可以使用 cache 機器的 IP 地址或者機器名作爲 hash 輸入。

把對象映射到cache

現在 cache 和對象都已經通過同一個 hash 算法映射到 hash 數值空間中了,接下來要考慮的就是如何將對象映射到 cache 上面了。在這個環形空間中,如果沿着順時針方向從對象的 key 值出發,直到遇見一個 cache ,那麼就將該對象存儲在這個 cache 上,因爲對象和 cache 的 hash 值是固定的,因此這個 cache 必然是唯一和確定的。這樣不就找到了對象和 cache 的映射方法了嗎?!依然繼續上面的例子(參見圖 3 ),那麼根據上面的方法,對象 object1 將被存儲到 cache A 上;object2 和 object3 對應到 cache C ;object4 對應到 cache B ;

考察cache 的變動

前面講過,通過 hash 然後求餘的方法帶來的最大問題就在於不能滿足單調性,當 cache 有所變動時, cache 會失效,進而對後臺服務器造成巨大的衝擊,現在就來分析分析 consistent hashing 算法。

1)移除 cache

考慮假設 cache B 掛掉了,根據上面講到的映射方法,這時受影響的將僅是那些沿 cache B 逆時針遍歷直到下一個 cache ( cache C )之間的對象,也即是本來映射到 cache B 上的那些對象。因此這裏僅需要變動對象 object4 ,將其重新映射到 cache C 上即可;參見圖 4 。

2)添加 cache

再考慮添加一臺新的 cache D 的情況,假設在這個環形 hash 空間中, cache D 被映射在對象 object2 和 object3 之間。這時受影響的將僅是那些沿 cache D 逆時針遍歷直到下一個 cache ( cache B )之間的對象(它們是也本來映射到 cache C 上對象的一部分),將這些對象重新映射到 cache D 上即可。因此這裏僅需要變動對象 object2 ,將其重新映射到 cache D 上;參見圖 5

hash 算法並不是保證絕對的平衡,如果 cache 較少的話,對象並不能被均勻的映射到 cache 上,比如在上面的例子中,僅部署 cache A 和 cache C 的情況下,在 4 個對象中, cache A 僅存儲了 object1 ,而 cache C 則存儲了 object2 、 object3 和 object4 ;分佈是很不均衡的。

虛擬節點

考量 Hash 算法的另一個指標是平衡性 (Balance) ,定義如下:平衡性

平衡性是指哈希的結果能夠儘可能分佈到所有的緩衝中去,這樣可以使得所有的緩衝空間都得到利用。

hash 算法並不是保證絕對的平衡,如果 cache 較少的話,對象並不能被均勻的映射到 cache 上,比如在上面的例子中,僅部署 cache A 和 cache C 的情況下,在 4 個對象中, cache A 僅存儲了 object1 ,而 cache C 則存儲了 object2 、 object3 和 object4 ;分佈是很不均衡的。爲了解決這種情況, consistent hashing 引入了“虛擬節點”的概念,它可以如下定義:“虛擬節點”( virtual node )是實際節點在 hash 空間的複製品( replica ),一實際個節點對應了若干個“虛擬節點”,這個對應個數也成爲“複製個數”,“虛擬節點”在 hash 空間中以 hash 值排列。

仍以僅部署 cache A 和 cache C 的情況爲例,在圖 4 中我們已經看到, cache 分佈並不均勻。現在我們引入虛擬節點,並設置“複製個數”爲 2 ,這就意味着一共會存在 4 個“虛擬節點”, cache A1, cache A2 代表了 cache A ;cache C1, cache C2 代表了 cache C ;假設一種比較理想的情況,參見圖 6 。

此時,對象到“虛擬節點”的映射關係爲:objec1->cache A2 ;objec2->cache A1 ;objec3->cache C1 ;objec4->cache C2 ;因此對象 object1 和 object2 都被映射到了 cache A 上,而 object3 和 object4 映射到了 cache C 上;平衡性有了很大提高。引入“虛擬節點”後,映射關係就從 { 對象 -> 節點 } 轉換到了 { 對象 -> 虛擬節點 } 。查詢物體所在 cache 時的映射關係如圖 7 所示。

在這裏插入圖片描述 “虛擬節點”的 hash 計算可以採用對應節點的 IP 地址加數字後綴的方式。例如假設 cache A 的 IP 地址爲 202.168.14.241 。引入“虛擬節點”前,計算 cache A 的 hash 值:Hash(“202.168.14.241”);引入“虛擬節點”後,計算“虛擬節”點 cache A1 和 cache A2 的 hash 值:Hash(“202.168.14.241#1”); // cache A1 Hash(“202.168.14.241#2”); // cache A2

總結

一致性哈希一般在分佈式緩存中使用的也比較多,本篇只介紹了服務的負載均衡和分佈式存儲,對於分佈式緩存其實原理是類似的,讀者可以自己舉一反三來思考下。其實,在分佈式存儲和分佈式緩存中,當服務節點發生變化時(新增或減少),一致性哈希算法並不能杜絕數據遷移的問題,但是可以有效避免數據的全量遷移,需要遷移的只是更改的節點和它的上游節點它們兩個節點之間的那部分數據。另外,我們都知道 hash算法 有一個避免不了的問題,就是哈希衝突。對於用戶請求IP的哈希衝突,其實只是不同用戶被分配到了同一臺服務器上,這個沒什麼影響。但是如果是服務節點有哈希衝突呢?這會導致兩個服務節點在哈希環上對應同一個點,其實我感覺這個問題也不大,因爲一方面哈希衝突的概率比較低,另一方面我們可以通過虛擬節點也可減少這種情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章