典型分散式系統分析:Dynamo

xybaby發表於2020-11-09

本文是典型分散式系統分析系列的第四篇,主要介紹 Dynamo,一個在 Amazon 公司內部使用的去中心化的、高可用的分散式 key-value 儲存系統。

在典型分散式系統分析系列的第一篇 MapReduce 中提出了本系列主要關心的問題:

  • 系統在效能、可擴充套件性、可用性、一致性之間的衡量,特別是CAP
  • 系統的水平擴充套件是如何實現的,是如何分片的
  • 系統的後設資料伺服器的效能、可用性
  • 系統的副本控制協議,是中心化還是去中心化
  • 對於中心化副本控制協議,中心是如何選舉的
  • 系統還用到了哪些協議、理論、演算法

本文的核心目的也是對這些問題進行解答。

本文地址:https://www.cnblogs.com/xybaby/p/13944662.html

Dynamo簡介

Dynamo 與之前分析過的 Bigtable,或者筆者使用最多的 NoSql MongoDB 的最大區別,我認為是去中心化(decentralized)。在 Dynamo 中使用的各種技術,如 Vector Clock、Consistent Hash、Gossip 等,一定程度上來說都是因為去中心化。

這裡所謂的去中心化,就是在副本集(replicaset)中沒有中心節點,所有節點的地位是平等的,大家都可以接受更新請求,相互通過協商達成資料的一致。其優點在於可用性分廠強,但一致性比較弱,基本上都是最終一致性(eventually-consistent),因此,可以說是在 CAP 中選擇了 A - Availability。

而去中心化的對立面 -- 中心化,已經在文章 帶著問題學習分散式系統之中心化複製集 中做了詳細介紹,簡而言之,副本集的更新由一箇中心節點(Leader、Primary)來執行,最大程度保障一致性,如果中心節點故障,即使有主從切換,也常常導致數十秒的不可用,因此,可以說是在 CAP 中選擇了 C - Consistency 。

如何在一致性與可用性之間權衡,這取決於具體的業務需求與應用場景。Dynamo 是在 Amazon 內部使用的一個分散式 KV 儲存系統,而 Amazon 作為一個電商網站,高可用是必須的,“always-on”。即使偶爾出現了資料不一致的情況,業務也是可以接受的,而且相對來說也比較好處理不一致,比如論文中提到購物車的例子。

這裡需要注意的是,Dynamo 和 DynamDB 是兩個不同的東西。前者是 Amazon 內部使用的kv儲存,在07年的論文 Dynamo: Amazon's Highly Available Key-value Store 面世;而後者是 AWS 提供的 NoSql 資料儲存服務,始於2012。

Dynamo詳解

有意思的是,在論文中,並不存在一張所謂的架構圖,只有一張表,介紹了Dynamo使用的一些通用技術:

focuses on the core distributed systems techniques used in Dynamo: partitioning, replication, versioning, membership, failure handling and scaling.

The main contribution of this work for the research community is the evaluation of how different techniques can be combined to
provide a single highly-available system.

正是通過組合這些廣為人知(well-known)的技術,實現了這麼一個高可用的儲存系統。

接下里就具體看看在 Dynamo 中是如何應用這些技術解決相關的問題。

Consistent hash

Dynamo 採用了一致性雜湊(consistent hash)作為資料分片(data partition)方式,之前在 帶著問題學習分散式系統之資料分片已經介紹過不同的資料分片方式,也包括一致性雜湊。簡而言之,Consistent hash:

  • 是分散式雜湊表(DHT, distribution hash table)的一種具體實現
  • 後設資料(資料到儲存節點的對映關係)較少
  • 在儲存節點加入、退出叢集時,對叢集的影響較小
  • 通過引入虛擬節點(virtual node),能充分利用儲存節點的異構資訊

一般來說,partition 和 replication 是兩個獨立的問題,只不過在 Dynamo 中,二者都是基於 consistent hash。

假設副本集數量為 N,那麼一個 key 首先對映到對應的 node(virtual node),這個節點被稱之為 coordinator,然後被複制到這個coordinator順時針的 N - 1 個節點上,這就引入了另一個概念 preference list, 即一個 key 應該被儲存的節點列表。

如上圖所示, Key K 對應的 coordinator 為 node B,與此同時,Node C, D 也會儲存 key K 的資料,即其 preference list 為 [B、C、D...]

有兩點需要注意:

  • 上述的 node 都是虛擬節點,那麼為了容錯,preference list 應該是不同的物理節點,因此可能跳過環上的某些虛擬節點
  • 副本集為 N 時,preference list 的長度會大於 N,這也是為了達到"always writeable“的目標,後面介紹sloppy quorum 和 hinted handoff 會再提及。

Object versioning(vector clock)

在去中心化副本集中,每個節點都可以接受資料的讀寫,可能是因為某些 client 無法連線上 coordinator,也可能是為了負載均衡。如果多個節點併發接受寫操作,那麼可以使用物件版本化(object versioning)來維護不同節點上資料的一致性檢視。在 Dynamo 中,則是使用了向量時鐘 vector clock 來確定寫操作之間的順序。

vector clock 為每個節點記錄一個遞增的序號,該節點每次操作的時候,其序號加 1,節點間同步資料、以及從節點讀取資料時,也會攜帶對應的版本號。

如上圖所示,副本集由Sx, Sy, Sz三個節點組成,開始的時候序號都為0,接下來對某個 key 有以下操作:

  1. 節點Sx接受寫操作,value 為 D1,對應的 vector clock 為[Sx, 1]
  2. 節點Sx再次接受寫操作,value 變為 D2,對應的 vector clock 為[Sx, 2]
  3. 該資料(連同 vector clock)被複制到了節點 Sy,Sz
  4. 同時發生了一下兩個操作
    • 節點 Sy 接受寫操作,value 變為 D3,對應的 vector clock 為 ([Sx, 2], [Sy, 1])
    • 節點 Sz 接受寫操作,value 變為 D4,對應的 vector clock 為 ([Sx, 2], [Sz, 1])
  5. Sy、Sz 將資料同步到 Sx,D3、D4 對應的版本號不具備偏序關係(所謂的偏序關係,即A向量中的每一維都與大於等於B向量的相應維),那麼就沒法確定這個 key 對應的 value 應該是 D3,還是 D4,因此這兩個值都會被存下。

Both versions of the data must be kept and presented to a client (upon a read) for semantic reconciliation.

  1. 直到在Sx上發生了讀操作,讀操作的返回值就應該是就是 D3、D4 的列表,客戶端決定如何處理衝突,然後將衝突解決後的值 D5 寫 到Sx,向量時鐘為 [(Sx, 3), (Sy, 1), (Sz, 1)]。此時D5 與 D3、D4 間顯然有了偏序關係。

但在上述第六步,如果在節點 Sy 上同時也發生 “讀資料 -- 衝突解決” 的過程,寫入D6 [(Sx, 3), (Sy, 2), (Sz, 1)], 那麼D5、D6又會衝突。

由上可以看到,vector clock 會自動合併有偏序關係的衝突。但邏輯上併發時,vector clock 就無能為力,這時如何解決衝突就面臨兩個問題

第一:在何時解決衝突?

是在寫資料的時候,還是讀資料的時候。與眾不同的是,Dynamo 選擇了在讀資料的時候來解決衝突,以保證永遠可寫(always writeable)。當然,只有在讀資料的時候才來處理衝突,也可能導致資料長期處於衝突狀態 -- 如果遲遲沒有應用來讀資料的話。

第二:誰來解決衝突?

是由系統(及Dynamo自身)還是由應用?系統缺乏必要的業務資訊,很難在複雜的情況下做出合適的選擇,因此只能執行一些簡單粗暴的策略,比如 last write win,而這又依賴於 global time,需要配合NTP 一同使用。Dynamo 則是交由應用開發人員來處理衝突,因為具體的應用顯然更清楚怎麼處理特定環境下的衝突。

那這是否增加了開發人員的負擔?在Amazon的技術架構中,一直都是堅持去中心化、面向服務的設計,“Design for failure ” 的設計原則已經成為了系統架構的基本原則,因此這對 Amazon 的程式來說並沒有額外的複雜度。

Quorum

Quorum 是一套非常簡單的副本管理機制,簡而言之,N 個副本中,每次寫入要求 W 個副本成功,每次讀取從 R 個副本讀取,只要 W+R > N,就能保證讀取到最新寫入的資料。

這個機制很直觀,高中的時候就學過,W 和 R 之間一定會有交集。

當然,在工程實踐中也不是隻操作 W 或 R 個節點,而是隻需等到 W 或 R 個節點的返回即可,比如在 Dynamo 中,操作都會發給 preference list 中的前 N 個活躍的節點。

Dynamo 中,允許通過配置 R、W、N 引數,使得應用可以綜合考慮 可用性、一致性、效能、成本 等因素,選擇最適合具體業務的組合方式。這點在Cassandra、Mongodb中也都有相關體現。

不過需要注意的是,DDIA 中指出,即使使用了 quorum,保證了 W+R > N,也還是會有一些 Corner case 使得讀到過時的資料(stale data)

  • 在不同的節點併發寫,發生了衝突,如果系統自動處理衝突,使用了類似 LWW(Last Write Win) 的仲裁機制,由於不同節點上的時鐘漂移,可能會出現不一致的情況
  • 大多分散式儲存,並沒有隔離性 Isolation,因此讀寫併發時,可能讀到正在寫入的資料
  • 如果寫失敗,即成功寫入的數量少於 W,已經寫入的節點也不會回滾。
  • 還有一種情況,就是 Dynamo 中採用的 Sloppy quorum

Sloppy quorum and hinted handoff

前面提到了preference list,即一個 key 在環中的儲存位置列表,雖然副本集數量為 N,但 preference list 的長度一般會超過 N,這是為了保證 “always writable”。比如N=3,W=2 的情況,如果此時有2個節點臨時故障,那麼按照嚴格(strict) quorum,是無法寫入成功的,但在鬆散(sloppy) quorum下,就可以將資料寫入到一個臨時節點。在 Dynamo 中,這個臨時節點就會加入到 preference list 的尾端,其實就是一致性雜湊環上下一個本來沒有該分片的節點。

當然,這個臨時節點知道這份資料理論上應該存在的位置,因此會暫存到特殊位置。

The replica sent to temp node will have a hint in its metadata that suggests which node was the intended recipient of the replica

等到資料的原節點(home node)恢復之後,在將資料打包發生獲取,這個過程稱之為 hinted handoff

因此,sloppy quorums不是傳統意義上的Quorum,即使滿足 W+R > N,也不能保證讀到最新的資料,只是提供更好的寫可用性。

Replica synchronization

hinted handoff 只能解決節點的臨時故障,考慮這麼一個場景,臨時節點在將資料前一會 home node 之前,臨時節點也故障了,那麼home node 中的資料就與副本集中的其他節點資料不一致了,這個時候就需要先找出哪些資料不一致,然後同步這部分資料。

由於寫入的 keys 可能在環中的任意位置,那麼如何找出哪些地方不一致,從而減少磁碟IO、以及網路傳輸的資料量,Dynamo 中使用了Merkle Tree

Merkle Tree學習 這篇文章中,有比較清晰易懂的介紹,這裡只簡單總結一下要點:

  • 每個葉子節點對應一個data block,並記錄其hash值,非葉子節點記錄其所有子節點的hash值
  • 從根節點開始比較,如果hash值一致,就表示這兩棵樹一致,否則分別比對左右子樹
  • 一致遞迴,直到葉子節點,只需拷貝不一致的葉子節點

具體 Dynamo 是如何應用 Merkle Tree 的呢

  • 物理節點為每個虛擬節點維護一棵獨立的 Merkle Tree
  • 比較時,就按照 Merkle Tree 的作法,從根節點開始比較

Merkle Tree 加速了比對操作,但構建一棵 Merkle Tree 也是較為耗時的過程,那麼在加入或刪除節點的時候,某個 virtual node 所維護的 key range 就會發生變化,那麼就需要重新構建這根 Merkle Tree。

節點加入、刪除的影響不止於此,比如在增加節點時,需要遷移一部分 key range,這就需要對原節點的儲存檔案進行掃描,找出這部分 keys , 然後傳送到目標節點,這會帶來巨量的IO操作(當然,這高度依賴儲存引擎對檔案的組織形式)。這些操作是會影響到線上服務的,因此只能在後臺緩慢執行,效率很低,論文中提到 上線一臺節點需要花費幾乎一整天時間。

Advanced partition scheme

問題的本質,在於資料的 partition 依賴於 virtual node 的位置,兩個相 鄰virtual node 之間的 key range 就是一個 partition,而每一 個partition 對應一個儲存單元。這就是論文中說到的

The fundamental issue with this strategy is that the schemes for data partitioning and data placement are intertwined.

data placement 就是指 data 的存放形式,其實就是 virtual node 在雜湊環上的位置。

那麼解決方案就是解耦 partitioning 和 placement,即資料的 partition 不再依賴 virtual node 在環上的絕對位置。

在Dynamo 中,改進方案如下:將hash環等分為 Q 份,當然 Q 遠大於節點的數量,每個節點均為 Q/S 份partition(S 為節點數量),當節點增刪時,只需要調整partition到節點的對映關係。

這就解決了前提提到的兩個問題:

  • 節點增刪時,資料的轉移以 partition 為單位,可以直接以檔案為單位,無需掃描檔案再傳送,實現 zero copy,更加高效
  • 一個 partition 就是一個 Merkle Tree,因此也無需重建 Merkle Tree

這種方案跟 redis cluter 的方案很像,redis cluter也是先劃分為 16384 個slot,根據節點數量大致均等的slot分配到不同的節點。

Failure Detection

有意思的是,Dynamo 認為節點的故障是短時的、可恢復的,因此不會採取過激的自動容錯,即不會主動進行資料的遷移以實現 rebalance。因此,Dynamo 才用顯式機制(explicit mechanism),通過管理員手動操作,向叢集中增刪節點。

A node outage rarely signifies a permanent departure and therefore should not result in rebalancing of the partition assignment or repair of the unreachable replicas.

這與另外一些系統不一樣,如果過半的節點認為某個節點不可用,那麼這個節點就會被踢出叢集,接下來執行容錯邏輯。當然,自動容錯是複雜的, 要讓過半節點達成某個節點不可用的共識也是複雜的過程。

也許會有疑問,那 Dynamo 這種做法就不能及時響應故障了?會不會導致不可用、甚至丟資料?其實之前提到,Dynamo有 Sloppy quorum 和 hinted handoff 機制,即使對故障的響應有所滯後也是不會有問題的。

因此,Dynamo的故障檢測是很簡單的,點對點,A 訪問不到 B,A 就可以單方面認為 B 故障了。一個例子,假設A在一次資料寫入中充當Coordinator,需要操作到B,訪問不了B,那麼就會在Preference list中找一個節點(如D)來儲存B的內容。

Membership

在Dynamo中,並沒有一個後設資料伺服器(metadata server)來管理叢集的後設資料:比如partition對節點的對映關心。當管理員手動將節點加入、移除hash環後,首先這個對映關係需要發生變化,其次hash環上的所有節點都要知道變動後的對映關係(從而也就知道了節點的增刪),達成一致性檢視。Dynamo使用了Gossip-Based協議來同步這些資訊。

Gossip 協議 這篇文章對Gossip協議有詳細的介紹,這裡就不再贅述的。不過後面有時間可以看看 Gossip 在redis cluster中的具體實現。

Summary

回答文章開頭的問題

  • 系統在效能、可擴充套件性、可用性、一致性之間的衡量,特別是CAP

犧牲一致性,追求高可用

  • 系統的水平擴充套件是如何實現的,是如何分片的

使用一致性hash,先將整個 hash ring 均為為Q 分,然後均等分配到節點

  • 系統的後設資料伺服器的效能、可用性

無額外的後設資料伺服器,後設資料通過Gossip 協議在節點間廣播

  • 系統的副本控制協議,是中心化還是去中心化

去中心化

  • 對於中心化副本控制協議,中心是如何選舉的

  • 系統還用到了哪些協議、理論、演算法

Quorum、 Merkle Tree、Gossip

在論文中介紹 Merkle Tree 時,用到了術語 anti-entropy,翻譯一下為 反熵,讀書時應該學習過 熵 這個概念,不過現在已經忘光了。不嚴謹的解釋為 熵為混亂程度,反熵就是區域穩定、一致,gossip 也就是一個 anti-entropy protocol。

非常有同感的一句話: 系統的可靠性和伸縮性取決於如何管理應用相關的狀態

The reliability and scalability of a system is dependent on how its application state is managed.

是無狀態 Stateless,還是自己管理,還是交給第三方管理(redis、zookeeper),大大影響了架構的設計。

References

Dynamo: Amazon's Highly Available Key-value Store

[譯] [論文] Dynamo: Amazon's Highly Available Key-value Store(SOSP 2007)

Merkle Tree

Gossip 協議

Distributed systems for fun and profit

Designing Data-Intensive Applications

相關文章