1. 程式人生 > >分散式雜湊表DHT和一致性雜湊

分散式雜湊表DHT和一致性雜湊

分散式雜湊(DHT)兩個key point:每個節點只維護一部分路由;每個節點只儲存一部分資料。從而實現整個網路中的定址和儲存。
DHT
只是一個概念,提出了這樣一種網路模型。並且說明它是對分散式儲存很有好處的。但具體怎麼實現,並不是DHT的範疇。一致性雜湊:
DHT
的一種實現。本質還是一個雜湊演算法。回想平時我們做負載均衡,按querystring簽名對後端節點取模是最簡單也是最常用的演算法,但節點的增刪所造成的問題顯而易見:原有的請求幾乎都落不到同一臺機器上。優化一點的是carp演算法,只讓1/n的資料受到影響。一致性雜湊,似乎最早提出是在分散式快取裡面的,讓節點震盪的時候,影響最小。不過現在已經應用在分散式儲存和

p2p系統裡面。一致性雜湊也只是提出四個概念和原則,並沒有提及具體實現:

1
balance:雜湊結果儘可能的平均分散到各個節點上,使得每個節點都能得到充分利用。

2
Monotonicity:上面也說了,如果是用簽名取模演算法,節點變更會使得整個網路的對映關係更改。如果是carp,會使得1/n的對映關係更改。一致性雜湊的目標,是節點變更,不會改變網路的對映關係。

3
spread:同一份資料,儲存到不同的節點上,換言之就是系統冗餘。一致性雜湊致力於降低系統冗度。

4
load:負載分散,和balance其實是差不多的意思,不過這裡更多是指資料儲存的均衡,balance是指訪的均衡。


Chord
演算法:
一致性雜湊有多種實現演算法,最關鍵的問題在於如何定義資料分割策略和節點快速查詢。

chord算是最為經典的實現。cassandra中的DHT,基本是chord的簡化版。

網路中每個節點分配一個唯一id,可以通過機器的mac地址做sha1,是網路發現的基礎。

假設整個網路有N 個節點,並且網路是呈環狀。兩個節點間的距離定義為每個節點會儲存一張路由表(finger表),表內順時針按照離本節點2、4、8、16、32.……2i的距離選定log2N個其他節點的ip資訊來記錄。

儲存方面:資料被按一定規則切割,每一份資料也有一個獨立id(查詢key),並且和節點id的值域是一樣的。然後查詢節點,如果存在和資料id一樣的節點id,則將這份資料存在該節點上;如果不存在,則儲存到離該資料id距離最近的節點上。同時,為了保證資料的可靠性,會順時針往下找K個冗餘節點,儲存這份資料。一般認為K=3是必須的。

查詢方面:先從自己的路由表中,找一個和資料id距離最近、並且存活在網路中的節點next。如果該節點的id巧合和資料id相等,那麼恭喜你。如果不相等,則到next進行遞迴查詢。一般或需要經過多次查詢才能找到資料所在的節點,而這個次數是可以被證明小於等於log2N的。

在這個查詢的過程中就體現了路由表的選取優勢了,其實是實現了一個二分查詢,從每個節點來觀察網路,都是將網路分成了log2N塊,最大一塊裡面有N/2個節點。路由表裡面其實是記錄了每一塊的第一個節點。這樣每一次查詢,最少排除了一半的節點。保證在log2N次內找到目標節點。

新增一個節點i,需要預先知道網路中已經存活的一個節點j,然後通過和節點j互動,更新自己和其他節點的路由表。並且,需要將離自己距離最近的節點中的資料copy過來,以提供資料服務。

損失一個節點,路由演算法會自動跳過這個節點,並且依靠資料的冗餘來持續提供服務。

KAD演算法(Kademlia)個人覺得,kad演算法其實是在chord上做的優化。主要是兩個點:
1
、用二進位制(32/64/128)表示一個節點的id,兩節點的id異或運算得到節點間的距離。
2
 每個節點保持的路由資訊更豐富,同樣是將整個網路按照劃分成log2N份,在chord中,是保持log2N個路由節點,但在kad裡面,是儲存了 log2N個佇列。每個佇列長度為配置值K,記錄網路中對應節點區域的多個節點,並且根據活躍時間對這些節點進行換入換出。第一點是方便進行網路劃分,節點按照二進位制中每一bit01建成一棵二叉樹。

第二點是使得節點查詢更迅速。從分割情況我們就可以得知,最壞情況不會差於chord,但儲存更多的節點使得命中概率更高。另外佇列中根據活躍時間進行換入換出,更有利於在p2p這種節點變更頻繁的網路中快速找到有效的節點。

 在大型web應用中,快取可算是當今的一個標準開發配置了。在大規模的快取應用中,應運而生了分散式快取系統。分散式快取系統的基本原理,大家也有所耳聞。key-value如何均勻的分散到叢集中?說到此,最常規的方式莫過於hash取模的方式。比如叢集中可用機器適量為N,那麼key值為K的的資料請求很簡單的應該路由到hash(K) mod N對應的機器。的確,這種結構是簡單的,也是實用的。但是在一些高速發展的web系統中,這樣的解決方案仍有些缺陷。隨著系統訪問壓力的增長,快取系統不得不通過增加機器節點的方式提高叢集的相應速度和資料承載量。增加機器意味著按照hash取模的方式,在增加機器節點的這一時刻,大量的快取命不中,快取資料需要重新建立,甚至是進行整體的快取資料遷移,瞬間會給DB帶來極高的系統負載,設定導致DB伺服器宕機。那麼就沒有辦法解決hash取模的方式帶來的詬病嗎?看下文。

一致性雜湊(Consistent Hashing):

      選擇具體的機器節點不在只依賴需要快取資料的key的hash本身了,而是機器節點本身也進行了hash運算。

(1) hash機器節點

首先求出機器節點的hash值(怎麼算機器節點的hash?ip可以作為hash的引數吧。。當然還有其他的方法了),然後將其分佈到0~2^32的一個圓環上(順時針分佈)。如下圖所示:
 
 

叢集中有機器:A , B, C, D, E五臺機器,通過一定的hash演算法我們將其分佈到如上圖所示的環上。

(2)訪問方式

如果有一個寫入快取的請求,其中Key值為K,計算器hash值Hash(K), Hash(K) 對應於圖 – 1環中的某一個點,如果該點對應沒有對映到具體的某一個機器節點,那麼順時針查詢,直到第一次找到有對映機器的節點,該節點就是確定的目標節點,如果超過了2^32仍然找不到節點,則命中第一個機器節點。比如 Hash(K) 的值介於A~B之間,那麼命中的機器節點應該是B節點(如上圖)。

(3)增加節點的處理

如上圖 – 1,在原有叢集的基礎上欲增加一臺機器F,增加過程如下:

計算機器節點的Hash值,將機器對映到環中的一個節點,如下圖:

 

增加機器節點F之後,訪問策略不改變,依然按照(2)中的方式訪問,此時快取命不中的情況依然不可避免,不能命中的資料是hash(K)在增加節點以前落在C~F之間的資料。儘管依然存在節點增加帶來的命中問題,但是比較傳統的 hash取模的方式,一致性hash已經將不命中的資料降到了最低。

Consistent Hashing最大限度地抑制了hash鍵的重新分佈。另外要取得比較好的負載均衡的效果,往往在伺服器數量比較少的時候需要增加虛擬節點來保證伺服器能均勻的分佈在圓環上。因為使用一般的hash方法,伺服器的對映地點的分佈非常不均勻。使用虛擬節點的思想,為每個物理節點(伺服器)在圓上分配100~200個點。這樣就能抑制分佈不均勻,最大限度地減小伺服器增減時的快取重新分佈。使用者資料對映在虛擬節點上,就表示使用者資料真正儲存位置是在該虛擬節點代表的實際物理伺服器上。
下面有一個圖描述了需要為每臺物理伺服器增加的虛擬節點。


  

x軸表示的是需要為每臺物理伺服器擴充套件的虛擬節點倍數(scale),y軸是實際物理伺服器數,可以看出,當物理伺服器的數量很小時,需要更大的虛擬節點,反之則需要更少的節點,從圖上可以看出,在物理伺服器有10臺時,差不多需要為每臺伺服器增加100~200個虛擬節點才能達到真正的負載均衡。
 

===

 直到現在為止,一致性雜湊也沒有一個非常明確的定義,多數文獻還是從其應用場景之上對一致性雜湊進行描述。“雜湊”想必大家都已經瞭解,問題是何為“一致性”?

  1. 一致性

  在討論一致性雜湊之前,先認識下“非一致性雜湊”,顯然HashMap屬於此列。

  當使用HashMap時,key被均勻地對映到陣列之上,對映方法就是利用key的hash與陣列長度取模(通過&運算)。

  當put的資料超過負載因子loadFactor×2Len時,HashMap會按照2被的容量擴容。新put進來的資料會通過與新陣列的長度取模的方式進行對映。那之前已經對映的資料該怎麼辦?通過檢視HashMap程式碼的resize方法會發現,每次擴容都會把之前的key重新對映。

  所以對HashMap而言要想獲得較好的效能必須要提前估計所放資料集合的大小,以設計合適的初始化容量和負載因子。

  2. 定義

 但不是每個場景都像HashMap這麼簡單,比如在大型的P2P網路中存在上百萬臺Server,資源與Server的關係是以Key的形式對映而成,也就是說是一個大的HashMap,維護著每個Key在哪個Server之上,如果有新的節點加入或退出P2P網路,跟HashMap一樣,也會導致對映關係的變化,顯然不可能把所有的Key與Server的對映關係都調整一遍。這就需要一種方法,在雜湊項發生變化是,不需要調整所有的節點,而達到繼續維護雜湊對映的關係。因此一致性雜湊定義為:

"Consistent hashing is a scheme that provides hash table functionality in a way that the addition or removal of one slot does not significantly change the mapping of keys to slots".(http://en.wikipedia.org/wiki/Consistent_hashing)

就是說,”一致性雜湊,就是提供一個hashtable,它能在節點加入離開時不會導致對映關係的重大變化“。

3.實現

一致性雜湊的定義除了描述一個定義或者一種想法並沒有給出任何實現方面的描述,所有細化的問題都留給開發者去思考。但一般的實現思路如下:

  • 假定雜湊的均勻Key分佈在一個環上,比如所有節點都通過SHA-1或MD5進行雜湊對映
  • 所有的節點也都分佈在同一環上(比如Server的IP地址經過SHA-1)
  • 每個節點只負責一部分Key,當節點加入、退出時隻影響加入退出的節點和其鄰居節點或者其他節點只有少量的Key受影響

假如有n個節點,m個key,當節點增加時大約有O(m/n)的節點需要移動。但一般一致性雜湊需要滿足下面幾個條件才對實際系統有意義:

  • 平衡性(Balance):就是指雜湊演算法要均勻分佈,不能有明顯的對映規律,這對一般的雜湊實現也是必須的
  • 單調性(Monotonicity):就是指有新節點加入時,已經存在的對映關係不能發生變化
  • 分散性(Spread):就是避免不同的內容對映到相同的位置和相同的內容對映到不同的位置

其實一致性雜湊(雜湊)有個明顯的優點就是負載均衡,只要雜湊函式設計得當,每個點就是對等的可以均勻地分佈系統負載。

4.Memcached

看了上面的定義和實現可能還是比較迷茫,那就舉個實際例子。

Memcached對大家應該不陌生,通過把Key對映到Memcached Server上,實現快速讀取。我們可以動態對其節點增加,並未影響之前已經對映到記憶體的Key與memcached Server之間的關係,這就是因為使用了一致性雜湊。

因為Memcached的雜湊策略是在其客戶端實現的,因此不同的客戶端實現也有區別,以Spymemcache、Xmemcache為例,都是使用了KETAMA作為其實現。

KETAMA實現方式如下:

  • 把Server的IP地址和埠進行MD5雜湊,MD5的結果為一個160bit的數字,取其前32位作為一個Integer
  • 把快取物件的Key做MD5雜湊,同樣得到一個整數
  • 可以設想,Server的整數會根據大小形成一個數字環,而Key的雜湊則分佈在這些數字上或中間
  • 如果Server的雜湊等於Key的雜湊,則把Key存放在該Server上;否則,尋找第一個大於Key雜湊的Server,用於存放Key
  • 但有Server增加、刪除時,只要變動周邊的Server對映關係即可,不用全部重新雜湊。之所以有這樣優良的特性是因為,Server和Key採用了同樣的值域

但是這樣做的效果並不理想,原因是雜湊雖然是隨機的,但往往隨機的不如人意,尤其是在Server節點數量上的情況下,Server不會均勻分佈在雜湊環上,這會導致雜湊不均勻,某些Server會承擔很多的Key,而另一些會很少,如圖:

絕大多數Key會對映到Server1,因此KETAMA引入了虛節點的概念,就是假象每個Server對映到N個節點(根據測試N在100~200時較優化),但Key的雜湊對映到這N個節點時實際都有該Server來託管。這樣做的意義在於,使因為實際節點少而導致大片未被對映的區別有虛節點去填充,從而使實節點有了處理本不屬於自己區間的Key。有虛節點後的環如下:

新增的同名節點即為虛節點。

還有最後一個問題,虛節點是如何產生的呢?也非常簡單,就是在每個Server加個字尾,在做MD5雜湊,取其32位。