一致性哈希在維基百科中,是這麼定義的html
一致哈希是一種特殊的哈希算法。在使用一致哈希算法後,哈希表槽位數(大小)的改變平均只須要對 K/n個關鍵字從新映射,其中K是關鍵字的數量, n是槽位數量。然而在傳統的哈希表中,添加或刪除一個槽位的幾乎須要對全部關鍵字進行從新映射。node
這一篇藉助這個主題,順便來了解下Dynamo在一致性hash上的應用,熟悉其應用場景以及原理。git
dynamo 的中文意思是發電機,意思是像發電機同樣,提供源源不斷的服務。它是Amazon提供的一個分佈式Key/Value存儲的NoSQL 數據庫,徹底託管在雲端,支持文檔和鍵值存儲模型。算法
其主要特色是以下:sql
特色 | 描述 |
---|---|
數據模型靈活 | schema freee,Nosql必然支持的,沒什麼說的 |
效率(速度) | 數據採用SSD來進行存儲,服務端平均延遲一般不超過十毫秒。隨着數據量增多,DynamoDB 會使用自動分區和 SSD 技術來知足吞吐量需求,並針對任意規模的數據庫提供低延遲。 |
高可用 | Dynamo 爲了達到高可用性和持久性,防止因爲節點宕機故障或者數據丟失,將同一份數據在協調者和隨後的 N-1 個節點上備份了屢次 |
高度可擴展 | dynamo 不會對數據規模大小進行限制,會把數據分佈到各個機器上,只需爲其指定目標使用率,容量便會自動根據應用程序請求數量的增長或減小而擴展或縮減,無需擔憂數據庫的縮擴容問題 |
徹底託管 | 無需擔憂數據庫的管理,好比管理集羣,軟硬件的配置與預設以及監控、部署,省去開發者部署、監控、維護數據庫的環境 |
去中心化 | 節點對稱性、去中心化:系統採用P2P架構,每一個節點都是對等的、有相同的責任 |
ACID屬性 | 爲了得到更爲靈活的可水平擴展的數據模型,NoSQL 數據庫一般會放棄傳統關係數據庫管理系統 (RDBMS) 的部分 ACID 屬性,並且在保證ACID的數據存儲時每每有不好的可用性。Dynamo的目標應用程序是高可用性,弱一致性(ACID中的」C」)。 |
我以爲dynamo最吸引人的地方就是高度擴展性,以及徹底託管,這個會節省開發人員大量的運維工做。
固然了很差的地方也是它的數據一致性要求不是很高,在99.94% 左右,並且遇到了不一致問題,都拋給了上層來解決,相似於git merge操做,若是對一致性要求比較高的話,這個仍是挺麻煩的,固然了這個主要看應用的選型需求了,後期再詳細介紹。數據庫
dynamo的高度擴展性,就是採用了一致性hash的原理來實現,咱們來着重分析一下,它是如何採用採用一致性hash而達到高擴展性的。服務器
在dynamo 中建立table的時候,必需要指定一個分區鍵(partition),分區鍵能夠用hash值,也能夠用戶本身指定,作惟一主鍵的時候,不能有重複。架構
對於剛接觸Dynamo的時候不是很明白要如何應用分區鍵。那麼爲什麼要用分區鍵?app
咱們回顧一下,一致性hash的實現原理,一致性hash是把數據均勻的映射到一個線性空間,以保證分配的均勻性,以及提升數據的單調性。同時爲了減小因爲節點數過少致使移動過多的數據項,又加入了虛擬節點。以下:運維
引入了「虛擬節點」後,映射關係就從【object--->node】轉換成了【object--->virtual node---> node】。查詢object所在node的映射關係以下圖所示。
以上virtual node咱們就能夠稱爲 partition,當增長新的node服務器的時候,因爲virtual node沒有變化,數據的hash值也是固定不變的,所以只須要處理一下,virtual node和node的重分配,這個對數據遷移的影響是最小的。
咱們看下代碼實現:
咱們假設有256個node(2^8),有partition數4096(2^12)。因爲MD5碼是32位的,使用PARTITION_SHIFT(等於32- PARTITION_POWER)將數據項的MD5哈希值映射到partition的2^12的空間中。此處引入了partition power 。
ITEMS = 10000000 NODES = 256 PARTITION_POWER = 12 PARTITION_SHIFT = 32 PARTITION = PARTITION_SHIFT - PARTITION_POWER node_stat = [0 for i in range(NODES)] #獲取hash值 def _hash(value): k = md5(str(value)).digest() ha = unpack_from(">I", k)[0] return ha ring = [] part2node = {} #虛擬節點和node節點作映射關係 for part in range(2 ** PARTITION_POWER): ring.append(part) part2node[part] = part % NODES for item in range(ITEMS): #把32位的hash值映射到12位的空間中 h = _hash(item) >> PARTITION #查找最近的partition partition = bisect_left(ring, h) n = partition % NODES node_stat[n] += 1 _ave = ITEMS / NODES _max = max(node_stat) _min = min(node_stat)
這個就是爲什麼dynamo在建立表的時候要指定分區鍵partition,由於要保證其數據的高擴展性,須要把數據分配到不一樣的node數據服務器上。
有了partition,一張表的數據,就能夠分散到不一樣的node上,同時在數據進行擴容增長node的時候,由於數據的partition並無發生變化,只是partition對應的node映射發生了變化,對數據的遷移影響是最小的。
在上述模型中,雖然解決的數據的擴展性問題,但數據的高可用問題,並無去達成,每一個node節點的數據都是單一的,若是這個節點出故障了,數據怎麼處理?
爲了讓系統達到高可用性和持久性,防止因爲節點宕機故障或而形成數據丟失,Dynamo中的數據被複製成N份存於多臺主機中。
除了本地存儲其範圍內的每一個結點將同一份數據在協調者和隨後的 N-1 個節點上備份了屢次,N 是一個能夠配置的值,默認狀況下是3,其理論依據主要來源於NWR策略。
NWR是一種在分佈式存儲系統中用於控制一致性級別的一種策略。在Amazon的Dynamo雲存儲系統中,就應用NWR來控制一致性。每一個字母的涵義以下:
N:同一份數據備份的份數
W:是更新一個數據對象的時候須要確保成功更新的份數
R:讀取一個數據須要讀取的最少節點(備份)的份數
只要知足W+R > N,就能夠保證當存在不超過一臺機器故障的時候,至少能讀到一份有效的數據。若是應用重視讀效率,能夠設置W=N,R=1; 若是應用須要在讀/寫之間權衡,通常可設置成N=3, W=2, R=2。Dynamo推薦使用322的組合。
咱們在這裏稱爲Replica,在分佈式系統中,數據的單點是不容許存在的,即線上正常存在的Replica數量是1的狀況是很是危險的。
由於一旦這個Replica再次錯誤,就可能發生數據的永久性錯誤。假如咱們把N設置成爲2,那麼,只要有一個存儲節點發生損壞,就會有單點的存在。因此N必須大於2。N約高,系統的維護和總體成本就越高。工業界一般把N設置爲3。
好比上圖中,黃色區域的值會存儲在三個節點 A、B 和 C 中,藍色的區域會被 D、E、F 三個節點處理,負責存儲某一個特定鍵值對的節點列表叫作偏好列表(preference list),由於虛擬節點在環中會隨機存在,爲了保證出現節點故障時不會影響可用性和持久性,偏好列表中的所有節點必須都爲不一樣的物理節點。
咱們來看實現方式
咱們在上述代碼中引入replica,數量設置爲3,其中 node_ids記錄的是3個replica存放的node id。part2node[part]是根據partition id 找到對應的node id,也是分區和node節點的映射關係。
ITEMS = 10000000 NODES = 100 PARTITION_POWER = 12 PARTITION_SHIFT = 32 PARTITION = PARTITION_SHIFT - PARTITION_POWER PARTITION_MAX =2**PARTITION_POWER-1 REPLICAS = 3 node_stat = [0 for i in range(NODES)] def _hash(value): k = md5(str(value)).digest() ha = unpack_from(">I", k)[0] return ha ring = [] part2node = {} #虛擬節點和node節點作映射關係 for part in range(2 ** PARTITION_POWER): ring.append(part) part2node[part] = part % NODES for item in range(ITEMS): #把32位的hash值映射到12位的空間中 h = _hash(item) >> PARTITION part = bisect_left(ring, h) node_ids = [part2node[part]] node_stat[node_ids[0]] += 1 #數據replica處理,一份數據存放臨近的3個物理節點 for replica in xrange(1, REPLICAS): while part2node[part] in node_ids: part += 1 if part > PARTITION_MAX: part = 0 node_ids.append(part2node[part]) node_stat[node_ids[-1]] += 1 _ave = ITEMS / NODES* REPLICAS _max = max(node_stat) _min = min(node_stat)
因爲加入了replica,特別是NWR 是322的狀況下,一個讀操做,必須得等待2個節點的數據返回對應結果,才認爲當前請求結束了,也就是說會請求時間會受最慢節點的影響,寫的狀況也是相同。惟一的不一樣是,發現節點中數據出現了衝突,會對衝突嘗試進行解決並將結果從新寫回對應的節點。
Dynamo對數據的一致性要求沒有那麼高,會出現數據不一致狀況,固然了多數狀況下,Dynamo 都不會出現這種狀況,而且即使出現了,Dynamo 可以確保一旦數據之間發生了衝突不會丟失,可是可能會有已被刪除的數據從新出現的問題。
針對這種狀況,Dynamo提供了向量時鐘來解決,每個對象版本中都存在一個或者多個向量時鐘,每次更新數據的時候,都會更新向量時鐘的版本。
若是待更新數據的向量鐘的每一項都不小於本地向量鍾,那麼數據無衝突,新的值能夠被接受。當客戶端再次 請求時就會發現數據出現了衝突,因爲 Dynamo 沒法根據向量時鐘自動解決,因此它須要客戶端合併不一樣的數據版本。就相似git 的merge 操做,把問題拋給了調用方來解決。
在一個節點出現臨時性故障時,數據會自動進入列表中的下一個節點進行寫操做,並標記爲handoff數據,若是在指定的時間內該機器從新提供服務,則其餘機器會經過Gossip協議發現,並將暫存的數據回傳給該臨時性故障的機器。
若是超時了必定的間隔,該機器仍是處理宕機狀態,則會認爲是永久下線了,此時須要從其它副本同步數據。爲了更快地檢測副本之間的不一致性,並減小傳輸的數據量,Dynamo採用了Merkle樹。Merkle樹是一個哈希樹,其葉子結點是各個key的哈希值,樹中較高的父結點均爲其各自孩子節點的哈希,經過這種方式構建的樹形結構可以保證整棵樹不會被篡改,任何的改動都能被馬上發現。如此檢測快,數據傳送的量也小,同步時只同步從根結點到葉子的全部節點值均不相同的文件。
引用:
https://draveness.me/dynamo
http://www.cnblogs.com/yuxc/archive/2012/06/22/2558312.html
end
關注遊戲研發和我的成長,致力於遊戲技術社區的推動,掃描二維碼,關注更多原創文章。