Amazon 針對小對象的分佈式鍵值存儲 ——Dynamo



Dynamo 是一個高可用的 KV 存儲系統。爲了保證高可用和高性能,Dynamo 採用了最終一致性模型,它對開發人員提供一種新型 API,使用了版本機制,並經過用戶側輔助解決衝突。Dynamo 目標是提供不間斷的服務,同時保證性能和可擴展性。因爲亞馬遜大量採用了去中心化、高度解耦微服務架構,所以對微服務狀態的存儲系統的可用性要求尤爲高。node


概覽

S3 (Simple Storage Service)是 Amazon 另外一款有名的存儲服務,雖然也能夠理解爲 KV 存儲,但它和 Dynamo 的目標場景並不一致。S3 是面向大文件的對象存儲服務,主要存儲二進制文件,不提供跨對象的事務。而 Dynamo 是一款面向小文件的文檔存儲服務,主要存儲結構化數據(如 json),而且能夠對數據設置索引,且支持跨數據條目的事務。算法

相對於傳統的關係型數據庫,Dynamo 能夠認爲是隻提供主鍵索引,從而獲取更高的性能和更好的擴展性。數據庫

爲了實現可擴展性和高可用性,並保證最終一致性,Dynamo 綜合使用瞭如下技術:json

  1. 使用一致性哈希對數據進行分片(partition)和備份(replicate)。數組

  2. 使用版本號機制(Vector Clock)處理數據一致性問題。安全

  3. 使用多數票(Quorum)和去中心化同步協議來維持副本間的一致性(Merkle Tree)。服務器

  4. 基於 Gossip Protocol 進行失敗檢測和副本維持。微信

實現上來講,Dynamo 有如下特色:網絡

  1. 徹底去中心化,沒有中心節點,全部節點關係對等。架構

  2. 採用最終一致性,使用版本號解決衝突,甚至要求用戶參與解決衝突。

  3. 使用哈希值進行數據分片,組織數據分佈,均衡數據負載。

背景

目標和假設

不一樣的設計假設和要求會致使徹底不一樣的設計,Dynamo 的設計目標有如下幾個:

查詢模型。使用 Dynamo 只會使用主鍵進行查詢,通常沒有跨數據條目,所以不須要關係模型。此外,Dynamo 假設其存儲的數據都相對較小,一般小於 1M。

ACID 特性。傳統關係型數據庫(DBMS)爲了保證事務的正確性和可靠性,一般須要具有 ACID 特性。但對 ACID 的支持會極大下降數據的性能,爲了高可用性,Dynamo 只提供弱一致性(C),不提供隔離性(I),不容許單個 key 的併發更新。

效率。Amazon 中大部分服務對延遲有着嚴格的要求,爲了可以知足此類服務的 SLA,Dynamo 須可配置,讓用戶本身在性能、效率、可用性和持久化間進行選擇。

其餘。Dynamo 只用在 Amazon 內部服務中,所以能夠不考慮安全性。此外,不少服務會使用獨立的 Dynamo 實例,所以最初針對可擴展性的目標在百臺機器級別。

SLA

因爲採用微服務架構,Amazon 購物網站的每一個頁面的渲染一般會涉及到上百個服務。爲了保證用戶體驗,必須對每一個服務的延遲作嚴格限制。Amazon 採用三個九(99.9% 的請求須要小於 300ms)的 SLA。而服務的狀態存儲環節則是提供該 SLA 的關鍵節點,爲此 Dynamo 的一個關鍵設計是讓服務可按需定製持久化和一致性等參數,以在性能、成本和正確性間進行抉擇。

設計考量

對於多副本系統,高可用性和強一致性是一對矛盾。傳統商用系統多爲了保證強一致性而犧牲部分可用性,但 Dynamo 爲高可用而生,所以選擇了異步同步策略。可是因爲網絡和服務器故障的頻發特性,系統必須處理這些故障所致使的不一致,或者說是衝突。這些衝突如何解決,主要包括兩方面:在何時解決,以及,誰來解決。

什麼時候解決。傳統存儲系統爲了簡化讀取,一般在寫入側解決衝突,即當存在衝突的時候,拒絕寫入。但 Dynamo 爲了保證商城業務對用戶任意時刻可用(好比隨時能將商品加購物車,畢竟相似過程的體驗稍微一降低,就會影響大把的收入),須要提供」 永遠可寫」(always writable)的保證,所以須要將解決衝突的複雜度推遲到讀取時刻。

誰來解決。是由 Dynamo 來解決,仍是應用側來解決。若是是 Dynamo 系統來解決,一般會無腦選擇」 後者勝 (last write win)」,即便用較新的更改覆蓋偏舊的更改。若是交由應用來解決,則能夠依據應用需求便宜行事,好比能夠合併屢次屢次加購物車操做返回給用戶。固然,這個是可選的,畢竟不少應用使用通用策略(」last write win」)就足夠了。

其餘關鍵設計原則還有:

增量擴展(incremental scalability)。支持節點的動態增刪,而最小化對系統和運維的影響。

對稱性(Symmetry)。系統中的每一個節點職責相同,沒有特殊節點,以簡化構建和維護成本。

去中心化(Decentralization)。沒有中心控制節點,使用點對點的技術以使系統高可用、易擴展。

異構性(Heterogeneity)。系統須要可以充分利用資源異構的節點,來按節點容量進行負載分配。

系統架構

圍繞分區算法、備份策略、版本機制、成員組織,錯誤處理和可擴展性等分佈式技術進行展開。

系統接口

Dynamo 暴露兩個接口:put() 和 get():

get(key):返回 key 對應的單個 object,或者有有版本衝突的 object 列表。

put(key, context, object):根據 key 選出 object 要放的副本機器,並將數據落盤。context 會包含一些對調用者透明的系統元信息,好比 object 的版本號信息。context 會和 object 一塊存儲以驗證 put 請求的合法性。

Dynamo 將 key 和 value 都視爲字節數組,而且對 key 進行 MD5 算法以生成一個 128 位的標識符,以進行存儲節點的選擇。

分區算法 (Partitioning algorithm)

爲了支持增量式擴容,Dynamo 使用一致性哈希算法進行負載分配。但基本版的一致性哈希算法有兩個缺點:

  1. 不可以均勻的分攤負載。

  2. 照顧不到不一樣節點的資源差別。

爲了解決些問題,Dynamo 使用了一致性哈希的變種:引入虛擬節點。具體算法爲:

  1. 節點在接入系統時,根據其容量大小生成相應數量的虛擬節點,每一個虛擬節點隨機分配一個節點編號。

  2. 全部虛擬節點按編號的大小組織成一個首尾相接環狀結構。

  3. 當有請求到來時,在與節點一樣的編號空間內使用 key 加某種哈希算法生成一個數據編號。

  4. 根據此編號繞着虛擬節點環順時針查找,找到第一個虛擬節點所對應的物理節點,將請求路由過去。

  5. 當有節點離開時,只須要移除其對應的虛擬節點便可,負載便會自動從新繞着環遷移。

其中,經過分配虛擬節點的數量來照顧到不一樣節點的容量差別,經過生成虛擬節點編號的隨機算法保證節點增刪時的流量均攤。

爲了照顧節點的增刪、備份的方便,Dynamo 前後使用了三種 Partition 策略:

1. 每一個節點分配 T 個隨機的數值編號(token),每一個虛擬節點一個 token,哈希環中相鄰兩個虛擬節點的 token 所卡出的區間即爲一個 partition。

這種最初的策略有如下幾個缺點:

能夠看出,這種策略的根本問題在於,數據分區(partition)和數據歸置(placement)是耦合在一塊的。這樣咱們就沒法單獨的對節點進行增刪而不影響數據分區。所以,一個很天然的改進想法是,將數據分區與數據歸置獨立開來。

    遷移掃描。當有新節點加入系統時,須要從其餘節點偷過來一些數據。這須要掃描新增虛擬節點後繼幾個節點中全部數據條目以獲得須要遷移的數據(猜想爲了 serve get 請求,節點上的數據通常是按用戶 key 進行索引組織的,而不是 key 的 hash 值,所以要獲取某個 hash 值段的數據,須要全盤掃描)。這個操做挺重的,爲了保證可用性須要下降遷移進程的運行權重,但這會使得遷移過程持續好久。

    Merkle Tree 從新計算。Merkle Tree 下面會講到,可粗理解爲以分區爲單位對數據進行層次化簽名。當有節點加入 / 離開集羣時,會致使 key range 的拆分 / 合併,進而引發對應 Merkle Tree 的從新計算,這也是一個計算密集型操做,會致使很重的額外負載,在線上系統中不能忍受。

    難以全局快照。因爲數據在物理節點中的分佈是按 key 的哈希值進行切分的,所以在 key 空間中是散亂的,很難在 key 空間中作全局快照,由於這要求全部節點上的數據進行全局歸併排序,效率低下。

2. 每一個節點仍隨機分配 T 個編號,可是將 哈希空間等分做爲分區

在此策略下,節點的編號(token)只是用來構建虛擬節點的哈希環,而再也不用來切分分區。咱們將哈希空間等分爲 Q 份,Q >> S*T,其中 S 是物理節點數。也就是說每一個虛擬節點能夠放不少分區。這種策略能夠從另外一種角度來理解,即節點 host 的最小單位再也不是 key,而是一個分區,每次節點增刪時,分區會總體進行移動。這樣就解決了在節點增刪時,遷移掃描和 Merkle Tree 從新計算的問題。

對於 key 的放置策略爲,每次 key 進行路由時,首先算出其哈希值,依據哈希值所在分區(key range)的最後一個哈希值,在哈希環中查找。順時針遇到的前 N 個物理節點做爲偏好列表。

3. 每一個節點 Q/S 個隨機編號,哈希空間等分做爲分區。

這種策略在上一種的基礎上,強制每一個物理節點擁有等量的分區。因爲 Q 數量,甚至每一個節點承載的分區數 (Q/S) 的數量遠大於節點數(S),所以在節點離開時,很容易將其承載的節點數分配給其餘節點,而仍然能維持該性質;當有節點加入時,每一個節點給他勻點也容易。

備份策略 (Replication)

Dynamo 會將每條數據在 N 個節點上進行備份,其中 N 是能夠配置的。對於每一個 key,會有一個協調節點(coordinator)來負責其在多個節點的備份。具體來講,協調節點會負責一個鍵區段 (key range)。

在進行備份時,協調節點會選擇一致性哈希環上,順時針方向的後繼 N - 1 節點,連同其自己,對數據條目進行 N 副本存儲,如圖二所示。這 N 個節點被稱爲偏好列表(preference list)。其中:

  1. key 到節點的映射根據上述三種不一樣的分區策略而不一樣。

  2. 節點可能會宕機重啓,偏好列表有時候可能會多於 N 個節點。

  3. 因爲使用的是虛擬節點,若是不加干涉,這 N 個節點可能會對應小於 N 個物理機。爲此,咱們在選擇節點的時候須要進行跳選,以保證 N 個節點處於 N 臺物理機上。

版本機制 (Data Versioning)

Dynamo 提供最終一致性保證,從而容許多副本進行異步同步,提升可用性。若是沒有機器和網絡故障,多副本將會在有限時間內同步完畢;若是出現故障,可能有些副本(replica)將永遠沒法正常完成同步。

Dynamo 提供任意時刻的可用性,若是最新的數據不能用,須要提供次新的。爲了提供這種保證,Dynamo 將每一個修改視爲一個新版本、不可變數據。它容許多個版本的數據並存,大多數狀況下,新版本數據可以對舊版本的進行覆蓋,從而讓系統能夠自動的挑選出權威版本(syntactic reconciliation,語法和解)。但當發生故障或者存在並行操做時,可能會出現互相沖突的版本分支,此時系統沒法自動進行合併,就須交由客戶端來進行合併(collapse)多個版本數據(語義和解,semantic reconciliation)。

Dynamo 使用一種叫作矢量時鐘 (vector clock的邏輯時鐘來表達同一數據多個版本間的因果關係(causality)。矢量時鐘由一組 <節點, 計數> 序列組成,分別對應同一數據的同步版本。能夠通多個數據版本的矢量時鐘來肯定這些數據版本間的關係:是並行發生(parallel branches)仍是存在因果(casual ordering):

  1. 若是矢量時鐘 A 中的計數小於矢量時鐘 B 中全部節點的計數,則 A 是 B 的前驅,能夠被丟棄。好比,A 爲 [<node1, 1>],B 爲 [<node1, 1>, <node2, 2>, <node3, 1>]

  2. 若是 A 不是 B 的前驅,B 也不是 A 的前驅,則 A 和 B 存在版本衝突,須要被和解。

在 Dynamo 中,客戶端更新數據對象時,必須指明所要更新的數據對象的版本。具體方式爲將以前從 Get 中得到的同一數據對象的版本信息(vector clock)傳入更新操做中的 context。一樣的,客戶端在讀取數據時,若是系統不可以進行自動合併(語法和解),則會將多個版本信息經過 context 返回給客戶端,一旦客戶端用此信息進行後續更新,系統就認爲客戶端對這多個版本進行了合併(語義和解)。下圖是一個詳細例子。

其中有幾點須要注意:

  1. 每一個服務器節點維護一個自增的計數器,當其處理更改請求前,更新計數器的值。

  2. 爲了防止矢量時鐘的尺寸無限增加,尤爲是出現網絡分區或者服務器失敗時,Dynamo 的策略是,矢量時鐘序列超過必定閾值時(好比說 10),將序列中最先的一個時鐘對丟棄。

get () 和 put ()

本小節描述系統不產生故障時的交互。主要分爲兩個過程:

  1. 用某種方式選擇一個 coordinator。

  2. coordinator 使用 quorum 機制進行數據多副本同步。

選擇 coordinator

Dynamo 經過 HTTP 方式對外暴露服務,主要有兩種策略來進行 coordinator 的選擇:

  1. 使用一個負載均衡器來選出一個負載較輕的節點。

  2. 使用能夠進行分區感知的客戶端,直接路由到負責該 key 的相應 coordinator (即偏好列表中的第一個)。

第一種方式客戶端不用保存服務器節點信息,第二種方式不須要轉發,延遲更低。

對於第一種方式,若是是 put() 請求,選出的節點 S 不在首選列表 N 個節點中,S 會將請求轉發到偏好列表中一個機器做爲 coordinator。若是是 get() 請求,無論 S 在不在偏好列表中,均可以直接做爲 coordinator。

Quorum 機制

Quorum 讀寫機制是一種有意思的讀寫方式,有兩個關鍵配置參數 R 和 W,一般 R 和 W 須要知足 1.R + W > N 2. W > N/2,其中 N 是集羣備份數。理解時能夠從兩個角度理解,一個是類比讀寫鎖,即系統不能同時有多個寫寫、讀寫,可是 R 設置的小一些能夠同時有多個讀;另外一個是須要半數以上寫成功,以知足數據的持久化特性。可是在 Dynamo 這些都沒有硬性要求,用戶能夠根據需求靈活配置。

當一個 put() 請求到達時,coordinator 爲新數據生成一個新的 vector clock 版本信息,並將其寫入本地,而後將數據發給 N 個偏好的 replica 節點,等到 W-1 節點回復,便可認爲請求成功。

當一個 get() 請求到達時,coodinator 向保有該 key N 個首選節點(包括 / 不包括它本身)發送請求,等到其中 R 個節點返回時,將多版本結果列表返回給用戶。而後經過 vector clock 規則進行語法和解,並將和解後的版本寫回。

故障處理:Hinted Handoff

若是使用嚴格的 Quorum 機制處理讀寫,那麼即便只有少許節點宕機或者網絡分區也會使得系統不可用,所以 Dynamo 使用一種」 粗略仲裁」(sloppy quorum)算法,能夠選擇一致性哈希環中首選列表的前 N 個健康節點。

而且當首選 coordinator (好比說 A)故障時,請求在路由到其餘節點(D)時,會在元信息中帶上第一選擇(A 的信息),D 後臺會有個常駐線程,檢測到 A 從新上線時,會將這些有標記的數據移到對應機器上,而且刪除本機相應副本。Dynamo 經過這種 hinted handoff 的方式,保證有節點或網絡故障時,也能正常完成請求。

固然,服務爲了高可用,能夠將 W 設置 1,這樣首選列表中任何節點可用,均可以寫成功。但在實踐中爲了保證持久化,通常都不會設這麼低。後面章節將會詳述 N,R 和 W 的配置問題。

此外,爲了處理數據中心級別的故障,Dynamo 經過配置使得首選節點列表跨越不一樣中心,以進行容災。

永久故障處理:副本同步

Hinted Handoff 只能處理偶然的、臨時的節點宕機問題。爲了處理其餘更嚴重的故障帶來的一致性問題,Dynamo 使用了去中心化的反熵算法(anti-entropy)來進行分片副本間的數據同步。

爲了快速檢測副本間數據是否一致、而且可以精肯定位到不同的區域,Dynamo 使用 Merkle Tree (也叫哈希樹,區塊鏈中也用)來以分片爲單位對分片中全部數據進行層層簽名。全部葉子節點是真實數據的 hash 值,而全部中間節點是其孩子節點的哈希值。這樣的樹有兩個好處:

  1. 只要比對根節點,就能夠知道分片的兩個副本數據是否一致。

  2. 每一箇中間節點都表明某個範圍的全部數據的簽名,只要其相等,則對應數據一致。

  3. 若是隻有少許不一致,能夠從根節點出發,迅速定位到不一致的數據位置。

Dynamo 對每一個數據分片(key range or shard,shard 是最小的邏輯存儲單位)維護一個 Merkle Tree,藉助 Merkle Tree 的性質,Dynamo 能夠很快比較兩個數據分片的副本數據是否一致。若是不一致,能夠經過定位不一致位置,最少化數據傳輸。

這樣作的缺點是,若是有節點加入或者離開集羣,會引發大量的 key range 的變更,從而須要對變化的 key range 從新計算 Merkle Tree。固然,前面也討論了,改進後的分區策略改進了這個問題。

成員關係和故障檢測

顯式管理成員關係。在 Amazon 的環境中,因爲故障或人爲失誤形成的節點離開集羣一般不多,或者不會持續太長時間。若是每次有節點下線都當即自動調整數據分片的放置位置,會引發沒必要要的數據震盪遷移。所以 Dynamo 採用顯式管理成員的方式,提供相應接口給管理員對物理節點進行上下線。即,因爲故障致使節點下線不會引發數據分片的移動。

類 Gossip 算法廣播元信息。成員關係變更首先由處理成員增刪請求的節點感知到,持久化到本地,而後利用類 Gossip 算法進行廣播,每次隨機選擇一個節點進行傳播,最終使得全部成員對此達成共識。此外,該算法也用於節點在剛啓動時交換數據分片信息和數據分佈信息。

每一個節點剛啓動時,只知道本身的節點信息和 token 信息,隨着各個節點漸次啓動,並經過算法互相交換信息,增量的在每一個節點分別構建出整個哈希環的拓撲(key range 到虛擬節點,虛擬節點到物理節點的映射)。從而,當某個請求到來的時候,能夠直接轉發到對應的處理節點。

種子節點避免邏輯分區。引入功能性的種子節點作服務發現,每一個節點都會直連種子節點,以使得每一個加入的節點快速爲其餘節點所知,避免因爲同時加入集羣,互不知曉,出現邏輯分區。

故障檢測。爲了不將 put/get 請求和同步元信息請求持續轉發到不可達節點,僅使用局部的故障檢測就足夠了。即若是 A 發向 B 的請求獲得不到迴應,A 就將 B 標記爲故障,而後開啓心跳,以感知其恢復。若是 A 收到應該轉向 B 的請求,而且發現 B 故障,就會在該 key 對應的首選節點列表中選擇一個替代節點。

能夠看出,Dynamo 將節點的永久離開暫時離開分開處理。使用顯示接口來增刪永久成員,並將成員拓撲經過 gossip 算法進行廣播;使用簡單標記和心跳來處理偶發故障,合理進行流量轉發。在故障較少的環境裏,如此分而治之,能大大提升達成一致的效率,最大限度避免節點偶發故障和網絡陣法抖動引發的沒必要要的數據搬遷。

增刪節點

以下圖,考慮三副本(N=3)而且採用最簡單的分區策略的狀況下,當在在節點 A 和 B 間加入一個節點 X 時,X 將會負責 Key Range: (F,G],(G, A],(A, X] ,同時 B 將再也不負責 (F,G],C 將再也不負責 (G, A],D 將再也不負責 (A, X] ,Dynamo 經過 B,C,D 主動向 X 推送相關 Key Range 的方式來適應 X 的加入。在推送前有個等待 X 確認階段,以免重複推送。

實現

Dynamo 中每一個節點主要包括四個組件:請求協調(request coordination),成員管理(membership),故障檢測(failure detection)和一個本地的持久化引擎(local persistence engine)。全部組件都是用 Java 實現的。

Dynamo 的本地持久化組件,容許選擇多種引擎,包括 Berkeley Database(BDB),MySQL 和一個基於內存 + 持久化的存儲。用戶能夠根據業務場景進行選擇,大部分的生產環境使用 BDB 。

請求協調組件使用 Java NIO 通道實現,採用事件驅動模型,將一個消息的處理過程被分爲多個階段。對於每一個到來的讀寫請求都會初始化一個狀態機來處理。好比對於讀請求來講,實現瞭如下狀態機:

  1. 發送請求到包含 key 所在分片的副本的全部節點。

  2. 等待讀請求最小要求的節點數(R)個節點返回。

  3. 在設定時限內,沒有收集到 R 個請求,返回客戶端失敗消息。

  4. 不然收集全部版本數據,並決定須要返回的版本數據。

  5. 若是啓用了版本控制,就會進行語法和解,並將和解後版本寫入上下文。

在讀的過程當中,若是發現某些副本數據過時了,會順帶將其更新,這叫作讀修復(read repair)。

對於寫請求,將會由首選 N 個節點中的一個做爲協調者進行協調,一般是第一個。但爲了提升吞吐,均衡負載,一般這 N 個節點均可以做爲協調者。尤爲是,大部分數據在讀取以後,一般會緊跟着寫入(讀取獲取版本,而後使用對應版本進行寫入),所以常將寫入調度到上次讀取中回覆最快的節點,該節點保存了讀取時的上下文信息,從而能更快響應,提升吞吐。

引用

s3 和 Dynamo 對比:https://serverless.pub/s3-or-dynamodb/

樂觀複製:https://en.wikipedia.org/wiki/Optimistic_replication




不妨一讀

WiscKey —— SSD 介質下的 LSM-Tree 優化


簡單聊聊對 CAP 的理解



掃描二維碼

獲取更多文章

分佈式點滴


本文分享自微信公衆號 - 分佈式點滴(distributed-system)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索