如何分配請求
現在有那么多個節點,要如何分配客戶端的請求呢?
最簡單的方式,引入一個中間的負載均衡層,讓它將外界的請求「輪流」的轉發給內部的集群。但是加權輪詢算法是無法應對「分布式系統」的,因為分布式系統中,每個節點存儲的數據是不同的。
因此,我們要想一個能應對分布式系統的負載均衡算法。
使用哈希算法有什么問題?
我們想到了哈希算法。因為對同一個關鍵字進行哈希計算,每次計算都是相同的值,這樣就可以將某個 key 確定到一個節點了,可以滿足分布式系統的負載均衡需求。
哈希算法最簡單的做法就是進行取模運算,比如分布式系統中有3個節點,基於 hash(key) % 3 公式對數據進行了映射。
但是有一個很致命的問題,如果節點數量發生了變化,也就是在對系統做擴容或者縮容時,必須遷移改變了映射關系的數據,否則會出現查詢不到數據的問題。
假設總數據條數為M,哈希算法在面對節點數量變化時,最壞情況下所有數據都需要遷移,所以它的數據遷移規模是 O(M),這樣數據的遷移成本太高了。
一致性哈希算法
一致性哈希算法就很好地解決了分布式系統在擴容或者縮容時,發生過多的數據遷移的問題。
一致哈希算法也用了取模運算,但與哈希算法不同的是,哈希算法是對節點的數量進行取模運算,而一致哈希算法是對 2^32 進行取模運算,是一個固定的值。
數據的id通過哈希函數轉換成的哈希值在0 ~ 2^32-1的數字空間中,現在把這些數字頭尾相連,想象成一個閉合的環形,那么一個數據id在計算出哈希值之后認為對應到環中的一個位置上。
接下來想象有三台機器也處在這樣一個環中,這三台機器在環中的位置根據機器id計算出的哈希值來決定。
確定一條數據歸屬哪台機器,首先把該數據的id用哈希函數算出哈希值,並映射到環中的相應位置,然后順時針找尋離這個位置最近的機器,那台機器就是該數據的歸屬。
比如下圖中的key-01映射的位置,往順時針的方向找到第一個節點就是節點A。
對指定key的值進行讀寫的地址:
- 首先,對key進行哈希計算,確定此key在環上的位置;
- 然后,從這個位置沿着順時針方向走,遇到的第一節點就是存儲key的節點。
增加一個節點或者減少一個節點
假設節點數量從3增加到了4,新的節點D經過哈希計算后映射到了下圖中的位置:
可以看到key-01、key-03都不受影響,只有key-02需要被遷移節點D。
假設節點數量從3減少到了2,比如將節點A移除:
可以看到只有key-01需要被遷移節點B。
總結:因此在一致哈希算法中,如果增加或者移除一個節點,僅影響該節點在哈希環上順時針相鄰的后繼節點,其它數據也不會受到影響。
使用一致性哈希算法有什么問題?
一致性哈希算法並不保證節點能夠在哈希環上分布均勻,這樣就會帶來一個問題,會有大量的請求集中在一個節點上。
下圖中3個節點的映射位置都在哈希環的右半邊:
這時候有一半以上的數據的尋址都會找節點A,也就是訪問請求主要集中的節點A上。在這種節點分布不均勻的情況下,進行容災與擴容時,哈希環上的相鄰節點容易受到過大影響,容易發生雪崩式的連鎖反應。
上圖中如果節點A被移除了,其上數據應該全部遷移到相鄰的節點B上,會導致節點B的數據量、訪問量都會迅速增加很多倍,一旦新增的壓力超過了節點B的處理能力上限,就會導致節點B崩潰,進而形成雪崩式的連鎖反應。
所以一致性哈希算法雖然減少了數據遷移量,但是存在節點分布不均勻的問題。
通過虛擬節點提高均衡度
要想解決節點能在哈希環上分配不均勻的問題,就是要有大量的節點,節點數越多,哈希環上的節點分布的就越均勻。這個時候我們可以加入虛擬節點,也就是對一個真實節點做多個副本。
具體做法:不再將真實節點映射到哈希環上,而是將虛擬節點映射到哈希環上,並將虛擬節點映射到實際節點,所以這里有「兩層」映射關系。
比如對每個節點分別設置3個虛擬節點:
- 對節點A加上編號來作為虛擬節點:A-01、A-02、A-03
- 對節點B加上編號來作為虛擬節點:B-01、B-02、B-03
- 對節點C加上編號來作為虛擬節點:C-01、C-02、C-03
引入虛擬節點后,哈希環上變成有9個虛擬節點映射到哈希環上,哈希環上的節點數量多了3倍,節點在哈希環上的分布就相對均勻了。
當某個節點被移除時,對應該節點的多個虛擬節點均會移除,而這些虛擬節點按順時針方向的下一個虛擬節點,可能會對應不同的真實節點,即這些不同的真實節點共同分擔了節點變化導致的壓力。因此虛擬節點除了會提高節點的均衡度,還會提高系統的穩定性。
有了虛擬節點后,還可以為硬件配置更好的節點增加權重(更多的虛擬機節點),所以帶虛擬節點的一致性哈希方法不僅適合硬件配置不同的節點的場景,而且適合節點規模會發生變化的場景。