DynamoDB實現原理分析

DynamoDB是Amazon的一個高可用的鍵-值存儲系統。用以提供一個“永遠在線”可用存儲。爲了達到這個級別的可用性,DynamoDB在某些故障場景中將犧牲一致性。它大量使用對象版本和應用程序協助的衝突協調方式以提供一個開發人員可以使用的新穎的接口。

在一個分佈式的存儲系統中,除了數據持久化組件,系統還需要有以下的考慮:

  • 負載均衡
  • 成員(membership)和故障檢測
  • 故障恢復
  • 副本同步
  • 過載處理
  • 狀態轉移
  • 併發性和工作調度
  • 請求marshaling
  • 請求路由
  • 系統監控報警
  • 配置管理
  • 。。。。。

描述解決方案的每一個細節不太可能,所以本文重點是核心技術在分佈式系統DynamoDB中的使用:

  • 劃分(partitioning)
  • 複製(replication)
  • 版本(versioning)
  • 會員(membership)
  • 故障處理(failure handling)
  • 伸縮性(scaling)

以下是DynamoDB使用的技術概要和優勢:

問題技術優勢
劃分(partitioning)一致性哈希增量可伸縮性
寫的高可用性矢量時鐘與讀取過程中的協調(reconciliation)版本大小與更新操作速率脫鉤
暫時性的失敗處理草率仲裁(Sloppy Quorum)並暗示移交(hinted handoff)提供高可用性和耐用性的保證,即使一些副本不可用時。
永久故障恢復使用Merkle樹的反熵(Anti-entropy)在後臺同步不同的副本
會員和故障檢測Gossip的成員和故障檢測協議保持對稱性並且避免了一個用於存儲會員和節點活性信息的集中註冊服務節點

劃分算法(Partitioning)

DynamoDB的關鍵設計要求之一是必須增量可擴展性。這就需要一個機制,用來將數據動態劃分到系統中的節點(即存儲主機)上去。DynamoDB的分區方案依賴於 一致性哈希 將負載分發到多個存儲主機。DynamoDB採用了一致性哈希的變體:每個節點被分配到環多點而不是映射到環上的一個單點。

一致性Hash算法滿足以下幾個方面:

  • 平衡性(Balance):即數據儘可能的平均分佈到系統中的節點上。
  • 單調性(Monotonicity):指如果已經有一些數據通過哈希分派到了相應的節點上,又有新的節點加入到系統中,那麼哈希的結果應能夠保證原有已分配的內容可以被映射到新的節點中去,而不會被映射到舊的節點集合中的其他節點。
  • 分散性(Spread):在分佈式環境中,終端有可能看不到所有的節點,而是隻能看到其中的一部分。當終端希望通過哈希過程將內容映射到節點上時,由於不同終端所見的節點範圍有可能不同,從而導致哈希的結果不一致,最終的結果是相同的內容被不同的終端映射到不同的節點上。這種情況顯然是應該避免的,因爲它導致相同內容被存儲到不同節點中去,降低了系統存儲的效率。分散性的定義就是上述情況發生的嚴重程度。好的哈希算法應能夠儘量避免不一致的情況發生,也就是儘量降低分散性。
  • 負載(Load):負載問題實際上是從另一個角度看待分散性問題。既然不同的終端可能將相同的內容映射到不同的緩衝區中,那麼對於一個特定的緩衝區而言,也可能被不同的用戶映射爲不同的內容。與分散性一樣,這種情況也是應當避免的,因此好的哈希算法應能夠儘量降低緩衝的負荷。
  • 平滑性(Smoothness):平滑性是指緩存服務器的數目平滑改變和緩存對象的平滑改變是一致的。

一致性Hash的基本原理:

簡單來說,一致性哈希將整個哈希值空間組織成一個虛擬的圓環,如假設某哈希函數H的值空間爲0-2^32-1(即哈希值是一個32位無符號整形),整個哈希空間環如下:


整個空間按順時針方向組織。0和2^32-1在零點中方向重合。

下一步將各個服務器使用Hash進行一個哈希,具體可以選擇服務器的ip或主機名作爲關鍵字進行哈希,這樣每臺機器就能確定其在哈希環上的位置,這裏假設將上文中四臺服務器使用ip地址哈希後在環空間的位置如下:


接下來使用如下算法定位數據訪問到相應服務器:將數據key使用相同的函數Hash計算出哈希值,並確定此數據在環上的位置,從此位置沿環順時針“行走”,第一臺遇到的服務器就是其應該定位到的服務器。

例如我們有Object A、Object B、Object C、Object D四個數據對象,經過哈希計算後,在環空間上的位置如下:


根據一致性哈希算法,數據A會被定爲到Node A上,B被定爲到Node B上,C被定爲到Node C上,D被定爲到Node D上。

下面分析一致性哈希算法的容錯性和可擴展性。現假設Node C不幸宕機,可以看到此時對象A、B、D不會受到影響,只有C對象被重定位到Node D。一般的,在一致性哈希算法中,如果一臺服務器不可用,則受影響的數據僅僅是此服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它不會受到影響。

下面考慮另外一種情況,如果在系統中增加一臺服務器Node X,如下圖所示:


此時對象Object A、B、D不受影響,只有對象C需要重定位到新的Node X 。一般的,在一致性哈希算法中,如果增加一臺服務器,則受影響的數據僅僅是新服務器到其環空間中前一臺服務器(即沿着逆時針方向行走遇到的第一臺服務器)之間數據,其它數據也不會受到影響。

綜上所述,一致性哈希算法對於節點的增減都只需重定位環空間中的一小部分數據,具有較好的容錯性和可擴展性。

另外,一致性哈希算法在服務節點太少時,容易因爲節點分部不均勻而造成數據傾斜問題。例如系統中只有兩臺服務器,其環分佈如下,


此時必然造成大量數據集中到Node A上,而只有極少量會定位到Node B上。爲了解決這種數據傾斜問題,一致性哈希算法引入了虛擬節點機制,即對每一個服務節點計算多個哈希,每個計算結果位置都放置一個此服務節點,稱爲虛擬節點。具體做法可以在服務器ip或主機名的後面增加編號來實現。例如上面的情況,可以爲每臺服務器計算三個虛擬節點,於是可以分別計算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,於是形成六個虛擬節點:


同時數據定位算法不變,只是多了一步虛擬節點到實際節點的映射,例如定位到“Node A#1”、“Node A#2”、“Node A#3”三個虛擬節點的數據均定位到Node A上。這樣就解決了服務節點少時數據傾斜的問題。在實際應用中,通常將虛擬節點數設置爲32甚至更大,因此即使很少的服務節點也能做到相對均勻的數據分佈。

寫的高可用性:複製(replication)

爲了實現高可用性和耐用性,DynamoDB將數據複製到多臺主機上。每個數據項被複制到N臺主機。其中N是每實例(per-instance)的配置參數。每個鍵,K,被分配到一個協調器(coordinator)節點。協調器節點掌控其負責範圍內的複製數據項。除了在本地存儲範圍內的每個Key外,協調器節點複製這些Key到環上順時針方向的N-1後繼節點。這樣的結果是,系統中每個節點負責環上的從其自己到第 N 個前繼節點間的一段區域。在下圖中,節點 B 除了在本地存 儲鍵 K 外,在節點 C 和 D 處複製鍵 K。節點 D 將存儲落在範圍(A,B],(B,C]和(C,D]上的所有鍵。


一個負責存儲一個特定的鍵的節點列表被稱爲首選列表(preference list)。該系統的設計讓系統中每一 個節點可以決定對於任意 key 哪些節點應該在這個清單中。出於對節點故障的考慮,首選清單可以包含起過 N 個節點。請注意, 因使用虛擬節點,對於一個特定的 key 的第一個 N 個後繼位置可能屬於少於 N 個物理所節點(即節點可以持有多個第一個 N 個 位置)。爲了解決這個問題,一個 key 首選列表的構建將跳過環上的一些位置,以確保該列表只包含不同的物理節點。

寫的高可用性:版本化數據

DynamoDB提供最終一致性,從而允許更新操作可以異步的傳播到所有的副本。put()調用可能在更新操作被所有的副本執行之前就 返回給調用者,這可能會導致一個場景:在隨後的 get()操作可能會返回一個不是最新的對象。如果沒有失敗,那麼更新操作的 傳播時間將有一個上限。但是,在某些故障情況下(如服務器故障或網絡 partitions),更新操作可能在一個較長時間內無法到達 所有的副本。

在 Amazon 的平臺,有一種類型的應用可以容忍這種不一致,並且可以建造並操作在這種條件下。例如,購物車應用程序要求 一個“添加到購物車“動作從來沒有被忘記或拒絕。如果購物車的最近的狀態是不可用,並且用戶對一個較舊版本的購物車做了 更改,這種變化仍然是有意義的並且應該保留。但同時它不應取代當前不可用的狀態,而這不可用的狀態本身可能含有的變化 也需要保留。請注意在 DynamoDB 中“添加到購物車“和”從購物車刪除項目“這兩個操作被轉成 put 請求。當客戶希望增加一個項 目到購物車(或從購物車刪除)但最新的版本不可用時,該項目將被添加到舊版本(或從舊版本中刪除)並且不同版本將在後來協調 (reconciled)。
爲了提供這種保證,Dynamo 將每次數據修改的結果當作一個新的且不可改變的數據版本。它允許系統中同一時間出現多個版 本的對象。大多數情況,新版本包括(subsume)老的版本,且系統自己可以決定權威版本(語法協調 syntactic reconciliation)。然而,版本分支可能發生在併發的更新操作與失敗的同時出現的情況,由此產生衝突版本的對象。在這種情 況下,系統無法協調同一對象的多個版本,那麼客戶端必須執行協調,將多個分支演化後的數據崩塌(collapse)成一個合併的 版本(語義協調)。一個典型的崩塌的例子是“合併”客戶的不同版本的購物車。使用這種協調機制,一個“添加到購物車”操作是永 遠不會丟失。但是,已刪除的條目可能會”重新浮出水面”(resurface)。 重要的是要了解某些故障模式有可能導致系統中相同的數據不止兩個,而是好幾個版本。在網絡分裂和節點故障的情況下,可 能會導致一個對象有不同的分歷史,系統將需要在未來協調對象。這就要求我們在設計應用程序,明確意識到相同數據的多個 版本的可能性(以便從來不會失去任何更新操作)。

DynamoDB 使用矢量時鐘來捕捉同一不同版本的對象的因果關係。矢量時鐘實際上是一個(node,counter)對列表(即(節點, 計數器)列表)。矢量時鐘是與每個對象的每個版本相關聯。通過審查其向量時鐘,我們可以判斷一個對象的兩個版本是平 行 分 枝 或 有 因 果 順 序 。如果第一個時鐘對象上的計數器在第二個時鐘對象上小於或等於其他所有節點的計數器,那麼第一個是 第二個的祖先,可以被人忽略。否則,這兩個變化被認爲是衝突,並要求協調。

在 dynamo 中,當客戶端更新一個對象,它必須指定它正要更新哪個版本。這是通過傳遞它從早期的讀操作中獲得的上下文對 象來指定的,它包含了向量時鐘信息。當處理一個讀請求,如果 Dynamo 訪問到多個不能語法協調(syntactically reconciled)的分支,它將返回分支葉子處的所有對象,其包含與上下文相應的版本信息。使用這種上下文的更新操作被認爲已 經協調了更新操作的不同版本並且分支都被倒塌到一個新的版本。


爲了說明使用矢量時鐘,讓我們考慮上圖所示的例子。
1)客戶端寫入一個新的對象。節點(比如說 Sx),它處理對這個 key 的寫:序列號遞增,並用它來創建數據的向量時鐘。該系統 現在有對象 D1 和其相關的時鐘[(Sx,1)]。
2)客戶端更新該對象。假定也由同樣的節點處理這個要求。現在該系統有對象 D2 和其相關的時鐘[(Sx,2)]。D2 繼承自 D1, 因此覆寫 D1,但是節點中或許存在還沒有看到 D2 的 D1 的副本。 3)讓我們假設,同樣的客戶端更新這個對象但不同的服務器(比如 Sy)處理了該請求。目前該系統具有數據 D3 及其相關的時鐘 [(Sx,2),(Sy,1)]。
4)接下來假設不同的客戶端讀取 D2,然後嘗試更新它,並且另一個服務器節點(如 Sz)進行寫操作。該系統現在具有 D4(D2 的 子孫),其版本時鐘[(Sx,2),(Sz,1)]。一個對 D1 或 D2 有所瞭解的節點可以決定,在收到 D4 和它的時鐘時,新的數據將覆 蓋 D1 和 D2,可以被垃圾收集。一個對 D3 有所瞭解的節點,在接收 D4 時將會發現,它們之間不存在因果關係。換句話說, D3 和 D4 都有更新操作,但都未在對方的變化中反映出來。這兩個版本的數據都必須保持並提交給客戶端(在讀時)進行語 義協調。
5)現在假定一些客戶端同時讀取到 D3 和 D4(上下文將反映這兩個值是由 read 操作發現的)。讀的上下文包含有 D3 和 D4 時鐘 的概要信息,即[(Sx,2),(Sy,1),(Sz,1)]的時鐘總結。如果客戶端執行協調,且由節點 Sx 來協調這個寫操作,Sx 將更新 其時鐘的序列號。D5 的新數據將有以下時鐘:[(Sx,3),(Sy,1),(Sz,1)]。

關於向量時鐘一個可能的問題是,如果許多服務器協調對一個對象的寫,向量時鐘的大小可能會增長。實際上,這是不太可能 的,因爲寫入通常是由首選列表中的前 N 個節點中的一個節點處理。在網絡分裂或多個服務器故障時,寫請求可能會被不是首 選列表中的前 N 個節點中的一個處理的,因此會導致矢量時鐘的大小增長。在這種情況下,值得限制向量時鐘的大小。爲此, Dynamo 採用了以下時鐘截斷方案:伴隨着每個(節點,計數器)對,Dynamo 存儲一個時間戳表示最後一次更新的時間。當向 量時鐘中(節點,計數器)對的數目達到一個閾值(如 10),最早的一對將從時鐘中刪除。顯然,這個截斷方案會導至在協調時效 率低下,因爲後代關係不能準確得到。不過,這個問題還沒有出現在生產環境,因此這個問題沒有得到徹底研究。

Lamport時間戳與矢量時鐘

Lamport timestamps原理如下:
1. 每個事件對應一個timestamp時間戳,初始值爲0
2. 如果事件在節點內發生,時間戳+1
3. 如果事件屬於發送事件,時間戳+1並在消息中帶上蓋時間戳
4. 如果事件屬於接受事件,時間戳=Max(本地時間戳,消息中的時間戳)+1

Vector clock
Lamport時間戳幫助我們得到時間的順序關係,但還有一種順序關係不能用Lamport時間戳很好的表示出來,那就是同時發生關係(concurrent)。
Vector clock是在Lamport時間戳基礎上嚴謹的另一種邏輯時鐘方法,它通過vector結構不但記錄本節點的Lamport時間戳,同時也記錄了其他節點的Lamport時間戳。
Version vector用於發現數據衝突。

Vector clock只用於發現數據衝突,不能解決數據衝突。如何解決數據衝突因場景而異,具體方法有last write win,交給client端處理,通過quorum決議事先避免數據衝突等。

故障處理:暗示移交(Hinted Handoff)

Dynamo 如果使用傳統的仲裁(quorum)方式,在服務器故障和網絡分裂的情況下它將是不可用,即使在最簡單的失效條件下也 將降低耐久性。爲了彌補這一點,它不嚴格執行仲裁,即使用了“馬虎仲裁”(“sloppy quorum”),所有的讀,寫操作是由首選 列表上的前 N 個健康的節點執行的,它們可能不總是在散列環上遇到的那前N個節點。
考慮在上圖例子中 Dynamo 的配置,給定 N=3。在這個例子中,如果寫操作過程中節點 A 暫時 Down 或無法連接,然後通常 本來在 A 上的一個副本現在將發送到節點 D。這樣做是爲了保持期待的可用性和耐用性。發送到 D 的副本在其原數據中將有一 個暗示,表明哪個節點纔是在副本預期的接收者(在這種情況下A)。接收暗示副本的節點將數據保存在一個單獨的本地存儲 中,他們被定期掃描。在檢測到了 A 已經復甦,D 會嘗試發送副本到 A。一旦傳送成功,D 可將數據從本地存儲中刪除而不會 降低系統中的副本總數。

使用暗示移交,Dynamo 確保讀取和寫入操作不會因爲節點臨時或網絡故障而失敗。需要最高級別的可用性的應用程序可以設 置 W 爲 1,這確保了只要系統中有一個節點將 key 已經持久化到本地存儲 , 一個寫是可以接受(即一個寫操作完成即意味着 成功)。因此,只有系統中的所有節點都無法使用時寫操作纔會被拒絕。然而,在實踐中,大多數 Amazon 生產服務設置了更 高的 W 來滿足耐久性極別的要求。 一個高度可用的存儲系統具備處理整個數據中心故障的能力是非常重要的。數據中心由於斷電,冷卻裝置故障,網絡故障和自 然災害發生故障。Dynamo 可以配置成跨多個數據中心地對每個對象進行復制。從本質上講,一個 key 的首選列表的構造是基 於跨多個數據中心的節點的。這些數據中心通過高速網絡連接。這種跨多個數據中心的複製方案使我們能夠處理整個數據中心 故障。

處理永久性故障:副本同步

Hinted Handoff 在系統成員流動性(churn)低,節點短暫的失效的情況下工作良好。有些情況下,在 hinted 副本移交回原來 的副本節點之前,暗示副本是不可用的。爲了處理這樣的以及其他威脅的耐久性問題,Dynamo 實現了反熵(anti-entropy, 或叫副本同步)協議來保持副本同步。
爲了更快地檢測副本之間的不一致性,並且減少傳輸的數據量,Dynamo 採用 MerkleTree。MerkleTree 是一個哈希樹 (Hash Tree),其葉子是各個 key 的哈希值。樹中較高的父節點均爲其各自孩子節點的哈希。該 merkleTree 的主要優點是樹 的每個分支可以獨立地檢查,而不需要下載整個樹或整個數據集。此外,MerkleTree 有助於減少爲檢查副本間不一致而傳輸的 數據的大小。例如,如果兩樹的根哈希值相等,且樹的葉節點值也相等,那麼節點不需要同步。如果不相等,它意味着,一些 副本的值是不同的。在這種情況下,節點可以交換 children 的哈希值,處理直到它到達了樹的葉子,此時主機可以識別出“不 同步”的 key。MerkleTree 減少爲同步而需要轉移的數據量,減少在反熵過程中磁盤執行讀取的次數。

Dynamo 在反熵中這樣使用 MerkleTree:每個節點爲它承載的每個 key 範圍(由一個虛擬節點覆蓋 key 集合)維護一個單獨的 MerkleTree。這使得節點可以比較 key range 中的 key 是否是最新。在這個方案中,兩個節點交換 MerkleTree 的根,對應 於它們承載的共同的鍵範圍。其後,使用上面所述樹遍歷方法,節點確定他們是否有任何差異和執行適當的同步行動。方案的 缺點是,當節點加入或離開系統時有許多 key rangee 變化,從而需要重新對樹進行計算。通過更精煉 partitioning 方案,這個問題得到解決。

會員和故障檢測

環會員(Ring Membership)

Amazon 環境中,節點中斷(由於故障和維護任務)常常是暫時的,但持續的時間間隔可能會延長。一個節點故障很少意味着一 個節點永久離開,因此應該不會導致對已分配的分區重新平衡(rebalancing)和修復無法訪問的副本。同樣,人工錯誤可能導致 意外啓動新的 Dynamo 節點。基於這些原因,應當適當使用一個明確的機制來發起節點的增加和從環中移除節點。管理員使用 命令行工具或瀏覽器連接到一個節點,併發出成員改變(membership change)指令指示一個節點加入到一個環或從環中刪除 一個節點。接收這一請求的節點寫入成員變化以及適時寫入持久性存儲。該成員的變化形成了歷史,因爲節點可以被刪除,重 新添加多次。一個基於 Gossip 的協議傳播成員變動,並維持成員的最終一致性。每個節點每間隔一秒隨機選擇隨機的對等節點, 兩個節點有效地協調他們持久化的成員變動歷史。

當一個節點第一次啓動時,它選擇它的 Token(在虛擬空間的一致哈希節點) 並將節點映射到各自的 Token 集(Token set)。該 映射被持久到磁盤上,最初只包含本地節點和 Token 集。在不同的節點中存儲的映射(節點到 token set 的映射)將在協調成 員的變化歷史的通信過程中一同被協調。因此,劃分和佈局信息也是基於 Gossip 協議傳播的,因此每個存儲節點都瞭解對等節 點所處理的標記範圍。這使得每個節點可以直接轉發一個 key 的讀/寫操作到正確的數據集節點。

外部發現

上述機制可能會暫時導致邏輯分裂的 Dynamo 環。例如,管理員可以將節點 A 加入到環,然後將節點 B 加入環。在這種情況

下,節點 A 和 B 各自都將認爲自己是環的一員,但都不會立即瞭解到其他的節點(也就是A不知道 B 的存在,B 也不知道 A 的存在,這叫邏輯分裂)。爲了防止邏輯分裂,有些 Dynamo 節點扮演種子節點的角色。種子的發現(discovered)是通過外 部機制來實現的並且所有其他節點都知道(實現中可能直接在配置文件中指定 seed node 的 IP,或者實現一個動態配置服 務,seed register)。因爲所有的節點,最終都會和種子節點協調成員關係,邏輯分裂是極不可能的。種子可從靜態配置或配置 服務獲得。通常情況下,種子在 Dynamo 環中是一個全功能節點。

故障檢測

Dynamo 中,故障檢測是用來避免在進行 get()和 put()操作時嘗試聯繫無法訪問節點,同樣還用於分區轉移(transferring
partition)和暗示副本的移交。爲了避免在通信失敗的嘗試,一個純本地概念的失效檢測完全足夠了:如果節點 B 不對節點 A 的 信息進行響應(即使 B 響應節點 C 的消息),節點 A 可能會認爲節點 B 失敗。在一個客戶端請求速率相對穩定併產生節點間通信 的 Dynamo 環中,一個節點 A 可以快速發現另一個節點 B 不響應時,節點 A 則使用映射到 B 的分區的備用節點服務請求,並 定期檢查節點 B 後來是否後來被複蘇。在沒有客戶端請求推動兩個節點之間流量的情況下,節點雙方並不真正需要知道對方是否可以訪問或可以響應。
去中心化的故障檢測協議使用一個簡單的 Gossip 式的協議,使系統中的每個節點可以瞭解其他節點到達(或離開)。有關去中心 化的故障探測器和影響其準確性的參數的詳細信息,早期 Dynamo 的設計使用去中心化的故障檢 測器以維持一個失敗狀態的全局性的視圖。後來認爲,顯式的節點加入和離開的方法排除了對一個失敗狀態的全局性視圖的需 要。這是因爲節點是是可以通過節點的顯式加入和離開的方法知道節點永久性(permanent)增加和刪除,而短暫的 (temporary)節點失效是由獨立的節點在他們不能與其他節點通信時發現的(當轉發請求時)。
發佈了45 篇原創文章 · 獲贊 19 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章