本文是典型分布式系統分析系列的第四篇,主要介紹 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 有以下操作:
- 節點Sx接受寫操作,value 為 D1,對應的 vector clock 為[Sx, 1]
- 節點Sx再次接受寫操作,value 變為 D2,對應的 vector clock 為[Sx, 2]
- 該數據(連同 vector clock)被復制到了節點 Sy,Sz
- 同時發生了一下兩個操作
- 節點 Sy 接受寫操作,value 變為 D3,對應的 vector clock 為 ([Sx, 2], [Sy, 1])
- 節點 Sz 接受寫操作,value 變為 D4,對應的 vector clock 為 ([Sx, 2], [Sz, 1])
- 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.
- 直到在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)