本文內容不僅僅侷限於 Dynamo
什麼是 Dynamo
亞馬遜在業務發展期間面臨一些問題,主要受限於關係型數據庫的可擴展性和高可用性,希望研發一套新的、基於 KV
存儲模型的數據庫,將之命名爲 Dynamo
。
相較於傳統的關係型數據庫 MySQL
,Dynamo
的功能目標與之有一些細小的差別,例如 Amazon
的業務場景多數情況並不需要支持複雜查詢,卻要求必要的單節點故障容錯性、數據最終一致性(即犧牲數據強一致優先保障可用性)、較強的可擴展性等。
可以肯定的是,在上述功能目標的驅使下,Dynamo
需要解決以下幾個關鍵問題:
- 它要在
CAP
中做出取捨,Dynamo
選擇犧牲特定情況下的強一致性(這也是大多數新興數據庫的權衡)優先保障可用性 - 它需要引入多節點,通過異步數據流複製完成數據備份和冗餘,從而支持單節點故障切換、維持集羣高可用
- 它需要引入某種 “再平衡(
rebalance
)” 算法來完成集羣的自適應管理和擴展操作,Dynamo
選擇了一致性哈希算法
Dynamo 和 MySQL 的關係?
有的人有這種疑問,其實二者沒有什麼關係,Dynamo
敘述的是一種 NoSQL
數據庫的設計思想和實現方案,它是一個由多節點實例組成的集羣,其中一個節點稱之爲 Instance
(或者 Node
其實無所謂),這個節點由三個模塊組成,分別是請求協調器、Gossip
協議檢測、本地持久化引擎,其中最後一個持久化引擎被設計爲可插拔的形式,可以支持不同的存儲介質,例如 BDB
、MySQL
等。
數據分片
數據分片的實現方式
數據分片實在是太常見了,因爲海量數據無法僅存儲在單一節點上,必須要按照某種規則進行劃分之後分開存儲,在 MySQL
中也有分庫分表方案,它本質上就是一種數據分片。
數據分片的邏輯既可以實現在客戶端,也可以實現在 Proxy
層,取決於你的架構如何設計,傳統的數據庫中間件大多將分片邏輯實現在客戶端,通過改寫物理 SQL
訪問不同的 MySQL
庫;而 NewSQL
數據庫倡導的計算存儲分離架構中呢,通常將分片邏輯實現在計算層,即 Proxy
層,通過無狀態的計算節點轉發用戶請求到正確的存儲節點。
Redis 集羣的數據分片
Redis
集羣也是 NoSQL
數據庫,它是怎麼解決哈希映射問題的呢?它啓動時就劃分好了 16384
個桶,然後再將這些桶分配給節點佔有,數據是固定地往這 16384
個桶裏放,至於節點的增減操作,那就是某些桶的重新分配,縮小了數據流動的範圍。
Dynamo 的數據分片
Dynamo
設計之初就考慮到要支持增量擴展,因爲節點的增減必須具備很好的可擴展性,儘可能降低期間的數據流動,從而減輕集羣的性能抖動。Dynamo
選擇採用一致性哈希算法來處理節點的增刪,一致性哈希的算法原理細節這裏不再贅述,只是提一下爲什麼一致性哈希能解決傳統哈希的問題。
我們想象一下傳統哈希算法的侷限是什麼,一旦我給定了節點總數 h
,那數據劃分到哪個節點就固定了(x mod h
),此時我一旦增減 h
的大小,那麼全部數據的映射關係都要發生改變,解決辦法只能是進行數據遷移,但是一致性哈希可以在一個圓環上優先劃分好每個節點負責的數據區域。這樣每次增刪節點,影響的範圍就被侷限在一小部分數據。
下圖藍色小圓 ABCD 的代表四個實際節點,橙色的小圓代表數據,他們順時針落在第一個碰到的節點上
一致性哈希的改進
一致性哈希是存在缺點的,如果僅僅是直接將每個節點映射到一個圓環上,可能造成節點間複雜的範圍有大有小,造成數據分佈和機器負載不均衡。
因此一致性哈希有個優化舉措,就是引入虛擬節點,其實就是我再引入一箇中間層解耦,虛擬節點平均落在圓環上,然後實際節點的映射跟某幾個虛擬節點掛鉤,表示我這臺物理節點實際負責這些虛擬節點的數據範圍,從而達到平衡負載的作用。
數據複製
數據複製是提升數據庫高可用的常見手段,從實現方式上可分爲同步複製、異步複製、半同步複製等,從使用場景上又可分爲單向複製、雙向複製、環形複製等。
Dynamo
的設計中爲了保證容災,數據被複制到 N
臺主機上,N
就是數據的冗餘副本數目,還記得我們說過 Dynamo
中每個節點有一個模塊叫做請求協調器麼,它接收到某個數據鍵值 K
之後會將其往圓環後的 N - 1
個節點進行復制,保證該鍵值 K
有 N
個副本,因此 Dynamo
中實際上每個節點既存儲自己接收的數據,也存儲爲其他節點保留的副本數據。
Dynamo 的讀寫流程
Dynamo
會在數據的所有副本中選取一個作爲協調者,由該副本負責轉發讀寫請求和收集反饋結果。通常情況下,該副本是客戶端從內存中維護的 數據 - 節點 映射關係中取得的,將請求直接發往該節點。
對於寫請求,該副本會接收寫請求,並記錄該數據的更新者和時間戳,並將寫請求轉發給其他副本,待 W
個副本反饋寫入完成後向客戶端反饋寫入操作成功;讀取流程類似,轉發讀請求至所有副本,待收到 R
個副本的結果後嘗試選取最新的數據版本,一旦發現數據衝突則保留衝突反饋給客戶端處理。
顯而易見的是,由於協調者是處理讀寫請求的唯一入口,因此該副本所在節點的負載肯定會飆高。
數據一致性和衝突解決
在數據存在 N
個冗餘副本的情況下,想要保證強一致需要等待所有副本寫入完成才能返回給客戶端寫入成功,但這是性能有損的,實踐中通常不這麼做。Dynamo
允許用戶設置至少寫入 W
個副本才返回,而讀取的時候需要從 R
個副本上讀到值才能返回,因此只要 W + R > N
,就能保證一定能讀到正確的值。
但是這有個問題是如何判斷返回的 R
個值中哪個是最新的呢,即每個數據都應該有一個版本信息。Dynamo
爲了解決這個問題引入向量時鐘的概念,簡單來說就是每次寫入操作,寫入的副本會爲這條數據變更新增一個更新者和版本號的向量組 <updater, version>
作爲版本信息,在後續的複製流程中也會帶上這部分信息。
例如副本 A
接收到了對鍵值 K
的更新請求,隨機爲鍵值 K
新增版本信息 K : <A, 1>
,等待之後再次更新 K
時更改爲 K : <A, 2>
,因此後者版本更新。
假設集羣中的網絡沒有問題,那麼對於某個鍵值 K
的讀取一定能讀取到時間戳最新的版本返回給客戶端。但是遺憾的是,分佈式場景下網絡是一定會出問題的,各種問題。。
假設客戶端在第二次更新時選擇了另一個副本 B
作爲協調者,那麼 B
會爲鍵值 K
保存 K : <B, 1>
,這時客戶端讀取鍵值 K
,協調者發現無法決定哪個版本是最新的,就類似於 Git Merge
出現衝突,只能保留這種衝突返回給客戶端,由具體業務邏輯覺得采用哪個值。
Dynamo 集羣成員狀態監測
Dynamo
想要做到 HA
(高可用),除了數據複製之外,還需要定時探測集羣節點的可用性,有的業界產品依賴外部服務統一處理,例如 MySQL
的 MHA
,RocketMQ
的 NS
,TiDB
的 PD
等,也有的依賴於節點間自適應管理,例如 Redis
集羣和 Dynamo
,這二者均採用了 Gossip
協議作爲集羣間節點信息交換的解決方案,無需引入外部服務,是完全的去中心化的架構。
想要了解什麼是 Gossip
協議,建議從 Redis
集羣的架構中去學習,往往使用 Gossip
協議的集羣實現都比較複雜,而且容易出錯,另外 Gossip
協議本身由於數據包龐大,也極易造成性能抖動問題。
總結
最近重讀了一遍 Dynamo
論文,加上之前看過幾遍的 《Design Data-intensive Applications》
,感覺很多分佈式系統設計的概念都可以很好地銜接上,對知識梳理很有幫助,大家感興趣也可以去看看。