一致哈希算法詳解

如果我們通過 Raft 算法實現了 KV 存儲,雖然領導者模型簡化了算法實現和共識協商,但寫請求只能限制在領導者節點上處理,導致了集羣的接入性能約等於單機,那麼隨着業務發展,集羣的性能可能就扛不住了,會造成系統過載和服務不可用,這時該怎麼辦呢?

其實這是一個非常常見的問題。在我看來,這時我們就要通過分集羣,突破單集羣的性能限制了。

你可能會說,分集羣還不簡單嗎?加個 Proxy 層,由 Proxy 層處理來自客戶端的讀寫請求,接收到讀寫請求後,通過對 Key 做哈希找到對應的集羣就可以了啊。

是的,哈希算法的確是個辦法,但它有個明顯的缺點:當需要變更集羣數時(比如從 2 個集羣擴展爲 3 個集羣),這時大部分的數據都需要遷移,重新映射,數據的遷移成本是非常高的。那麼如何解決哈希算法,數據遷移成本高的痛點呢?答案就是一致哈希(Consistent Hashing)

爲了更好地理解如何通過哈希尋址實現 KV 存儲的分集羣,除了瞭解哈希算法尋址問題的本質之外,還會講一下一致哈希是如何解決哈希算法數據遷移成本高這個痛點,以及如何實現數據訪問的冷熱相對均勻。

在正式開始學習之前,我們先看一道思考題。

假設我們有一個由 A、B、C 三個節點組成(爲了方便演示,我使用節點來替代集羣)的KV 服務,每個節點存放不同的 KV 數據:
在這裏插入圖片描述

那麼,使用哈希算法實現哈希尋址時,到底有哪些問題呢?帶着這個問題,讓我們開始今天的內容吧。

使用哈希算法有什麼問題?

通過哈希算法,每個 key 都可以尋址到對應的服務器,比如,查詢 key 是 key-01,計算公式爲 hash(key-01) % 3 ,經過計算尋址到了編號爲 1 的服務器節點 A(就像圖 2 的樣子)。

在這裏插入圖片描述

但如果服務器數量發生變化,基於新的服務器數量來執行哈希算法的時候,就會出現路由尋址失敗的情況,Proxy 無法找到之前尋址到的那個服務器節點,這是爲什麼呢?

想象一下,假如 3 個節點不能滿足業務需要了,這時我們增加了一個節點,節點的數量從3 變化爲 4,那麼之前的 hash(key-01) % 3 = 1,就變成了 hash(key-01) % 4 = X,因爲取模運算髮生了變化,所以這個 X 大概率不是 1(可能 X 爲 2),這時你再查詢,就會找不到數據了,因爲 key-01 對應的數據,存儲在節點 A 上,而不是節點 B:

在這裏插入圖片描述

同樣的道理,如果我們需要下線 1 個服務器節點(也就是縮容),也會存在類似的可能查詢不到數據的問題。

而解決這個問題的辦法,在於我們要遷移數據,基於新的計算公式 hash(key-01) % 4 ,來重新對數據和節點做映射。需要你注意的是,數據的遷移成本是非常高的。

爲了便於理解,我舉個例子,對於 1000 萬 key 的 3 節點 KV 存儲,如果我們增加 1 個節點,變爲 4 節點集羣,則需要遷移 75% 的數據。

遷移成本是非常高昂的,這在實際生產環境中也是無法想象的。

那我們如何通過一致哈希解決這個問題呢?

如何使用一致哈希實現哈希尋址?

一致哈希算法也用了取模運算,但與哈希算法不同的是,哈希算法是對節點的數量進行取模運算,而一致哈希算法是對 2^32 進行取模運算。你可以想象下,一致哈希算法,將整個哈希值空間組織成一個虛擬的圓環,也就是哈希環:
在這裏插入圖片描述

從圖 4 中你可以看到,哈希環的空間是按順時針方向組織的,圓環的正上方的點代表 0,0點右側的第一個點代表 1,以此類推,2、3、4、5、6……直到 2 ^ 32-1,也就是說 0 點左側的第一個點代表 2^32-1。

在一致哈希中,你可以通過執行哈希算法(爲了演示方便,假設哈希算法函數爲“c-hash()”),將節點映射到哈希環上,比如選擇節點的主機名作爲參數執行 c-hash(),那麼每個節點就能確定其在哈希環上的位置了:
在這裏插入圖片描述

當需要對指定 key 的值進行讀寫的時候,你可以通過下面 2 步進行尋址:

  • 首先,將 key 作爲參數執行 c-hash() 計算哈希值,並確定此 key 在環上的位置;
  • 然後,從這個位置沿着哈希環順時針“行走”,遇到的第一節點就是 key 對應的節點。

爲了幫助你更好地理解如何通過一致哈希進行尋址,我舉個例子。假設 key-01、key-02、key-03 三個 key,經過哈希算法 c-hash() 計算後,在哈希環上的位置就像圖 6 的樣子:
在這裏插入圖片描述

那麼根據一致哈希算法,key-01 將尋址到節點 A,key-02 將尋址到節點 B,key-03 將尋址到節點 C。講到這兒,你可能會問:“老韓,那一致哈希是如何避免哈希算法的問題呢?”

彆着急,接下來我分別以增加節點和移除節點爲例,具體說一說一致哈希是如何避免上面的問題的。假設,現在有一個節點故障了(比如節點 C):
在這裏插入圖片描述

你可以看到,key-01 和 key-02 不會受到影響,只有 key-03 的尋址被重定位到 A。一般來說,在一致哈希算法中,如果某個節點宕機不可用了,那麼受影響的數據僅僅是,會尋址到此節點和前一節點之間的數據。比如當節點 C 宕機了,受影響的數據是會尋址到節點 B和節點 C 之間的數據(例如 key-03),尋址到其他哈希環空間的數據(例如 key-01),不會受到影響。

那如果此時集羣不能滿足業務的需求,需要擴容一個節點(也就是增加一個節點,比如D):

在這裏插入圖片描述

你可以看到,key-01、key-02 不受影響,只有 key-03 的尋址被重定位到新節點 D。一般而言,在一致哈希算法中,如果增加一個節點,受影響的數據僅僅是,會尋址到新節點和前一節點之間的數據,其它數據也不會受到影響。

讓我們一起來看一個例子。使用一致哈希的話,對於 1000 萬 key 的 3 節點 KV 存儲,如果我們增加 1 個節點,變爲 4 節點集羣,只需要遷移 24.3% 的數據:

你看,使用了一致哈希後,我們需要遷移的數據量僅爲使用哈希算法時的三分之一,是不是大大提升效率了呢?

總的來說,使用了一致哈希算法後,擴容或縮容的時候,都只需要重定位環空間中的一小部分數據。也就是說,一致哈希算法具有較好的容錯性和可擴展性。

需要注意的是,在哈希尋址中常出現這樣的問題:客戶端訪問請求集中在少數的節點上,出現了有些機器高負載,有些機器低負載的情況,那麼在一致哈希中,有什麼辦法能讓數據訪問分佈的比較均勻呢?答案就是虛擬節點。

在一致哈希中,如果節點太少,容易因爲節點分佈不均勻造成數據訪問的冷熱不均,也就是說大多數訪問請求都會集中少量幾個節點上:

在這裏插入圖片描述

你能從圖中看到,雖然有 3 個節點,但訪問請求主要集中的節點 A 上。那如何通過虛擬節點解決冷熱不均的問題呢?

其實,就是對每一個服務器節點計算多個哈希值,在每個計算結果位置上,都放置一個虛擬節點,並將虛擬節點映射到實際節點。比如,可以在主機名的後面增加編號,分別計算“Node-A-01” “Node-A-02” “Node-B-01” “Node-B-02” “Node-C-01” “Node-C-02” 的哈希值,於是形成 6 個虛擬節點:
在這裏插入圖片描述

你可以從圖中看到,增加了節點後,節點在哈希環上的分佈就相對均勻了。這時,如果有訪問請求尋址到“Node-A-01”這個虛擬節點,將被重定位到節點 A。你看,這樣我們就解決了冷熱不均的問題。

最後想說的是,當節點數越多的時候,使用哈希算法時,需要遷移的數據就越多,使用一致哈希時,需要遷移的數據就越少:

當我們向 10 個節點集羣中增加節點時,如果使用了哈希算法,需要遷移高達 90.91% 的數據,使用一致哈希的話,只需要遷移 6.48% 的數據。我希望你能注意到這個規律,使用一致哈希實現哈希尋址時,可以通過增加節點數降低節點宕機對整個集羣的影響,以及故障恢復時需要遷移的數據量。後續在需要時,可以通過增加節點數來提升系統的容災能力和故障恢復效率。

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