---
# System prepended metadata

title: '7-3 Dynamo: Amazon’s Highly Available Key-value Store'

---

# 7.3 Dynamo: Amazon’s Highly Available Key-value Store

## 參考資料
- 翻譯
    - https://arthurchiao.art/blog/amazon-dynamo-zh/
- 導讀
    - https://blog.csdn.net/plm199513100/article/details/121731269
    - https://blog.csdn.net/plm199513100/article/details/121915208
## Introduction
- 亞馬遜是遍布全球的電商平台，任何一丁點的損失都會造成大量市值流失，因此須著重在可靠性以及可用性，同時及為了迅速擴展，高擴展性也很重要
- 對於大型的分散式系統，故障是隨時都會發生的，因此軟體要試故障為可預期狀態，並且不影響其可用性或效能
- Dynamo 是用於管理對可靠性要求極高的服務之狀態，而這些服務也對 dynamo 的可靠性, 一致性, cost-effectiveness, 效能有著極高的控制程度
- 亞馬遜當中有些服務是使用簡單的 key-value 形式存取，像是暢銷排行榜, 購物車等等。假如使用 RDBMS，反而降低效率, 效能，限制擴充性 可用性
- Dynamo 是透過成熟技術來做到高擴展性, 高可用性
    - consistent hashing: 做到 partition 和 replicated
    - object versioning: 資料版本控制，實現一致性
    - qurom-based, decentralized replica synchronization protocol: 達成一致性
    - gossip based distributed failure detection and membership protocol: 錯誤偵測
- 重點: 本篇著重在高可用性, 高可靠性, 高擴展性, 極低延遲

## Background
- 對於簡單 key-value 存取的資料來說，如果是使用傳統的 RDBMS 需要特殊的硬體和訓練有素的工程師來操作，反而降低效率，也沒有簡單達到目的，並且傳統 RDBMS 更講求一致性，反而損失了可用性，也因為其複雜的結構，擴展性不高
- 對於運行 dynamo 的系統，有以下假設
    - Query Model: 資料是簡單的鍵值操作，並且沒有複雜的資料關係 (data-relation)
    - ACID Properties: 為了最大化可用性，犧牲一點一致性，並且不提供隔離機制，並只允許對單鍵操作
    - Efficiency: 服務跟存儲系統達到嚴格的 SLA (等會介紹)，也就是要有一定程度的延遲跟吞吐量，並且擁有動態配置 dynamo 節點的能力，使服務在任何狀況下都能夠達到目標延遲以及吞吐
    - Security: 因為是內部使用，不著重資安疑慮。
    - Scalability: 每個服務都有自己的 dynamo 系統，要有一定程度的擴展性
- Service Level Agreements (SLA)
    - 服務在一定請求程度下的效能保證
        - 某個服務向客戶端保證，在 500 QPS 的負載下，它處理 99.9% 的請求 所花的時間都在能 300ms 以內。
    - 在以往的研究中，是用 平均值, 中位數 或 標準差來表示 SLA，這樣可能只能表達大部分請求的狀況，但在亞馬遜，必須是幾乎所有請求都要達到理想狀況，因此直接設定在 99.9% 的請求率，後續也都以此做為數據表示基準
- 設計考量
    - 前面提過，dynamo 為了最大化可用性，因此選擇弱一致性，並達到最終一致性。而採用弱一致性，就會有可能遇到版本衝突，延伸出兩個議題:
        - 哪個階段去解決衝突: 讀 or 寫?
            - 傳統研究是在寫的時候去處理衝突，讀的話盡可能簡單。
            - 常見的就 qurom-based，然而只要有大部分的節點沒有通過，寫就失敗。這違反 dynamo 的不管怎樣都要有地方可以寫
            - 因此選擇在讀的時候處理衝突
        - 誰去解決衝突: 資料庫 和 應用程序
            - 如果是交由資料庫處理，就採用最簡單的 last write wins
            - 如果是交由應用程序處理，可以依照應用自身做彈性決定
            > 像是購物車，可以將現有版本跟衝突版本合併在一起給使用者
    - 持續擴充: 要能持續地擴充，且不影響原有運行的資料節點
    - 對稱性: 每個節點負責的任務都一樣，不會有人突然是 leader，或是有不一樣的任務。對稱性簡單化系統
    - 去中央化: 對稱性延伸出來的概念，可以為擴充性 可用性帶來更多可能
    - 異構性: 不同節點的運算能力可以不同，而能力越強的，其負載就要更多

## 與其餘研究不同之處
1. Dynamo 追求"總是可寫"，不會因為沒達到大部分或是結點失效等問題導致無法寫入
2. Dynamo 作為亞馬遜專用的內部基礎設施，不必著重資安議題
3. 使用 Dynamo 的應用不會有複雜的層級命名空間(hierarchical namespaces)和複雜的資料關係(relation schema)
4. Dynamo 要確保 99.9% 的 讀/寫請求都能夠在極度嚴苛的目標(幾百毫秒以內)完成。因此不採用多層路由的架構，而是在每個節點保存足夠量的節點資訊，一步直接導到目標節點

## 系統架構
- 針對核心的分散式技術說明，包括資料分區、副本複製、版本控制、資料處理過程、錯誤處理 以及 擴展
- dynamo 對外提供兩個方法
    - get(key): 返回屬於該 key 的 object，或是同樣都是該 key，但是有版本衝突的多個 object
    - put(key): 將 key 透過一致性 hash 算法，決定 object 存入哪個節點，同時會協帶 context，包含 object 的一些 metadata，像是版本，讓 dynamo 可以確定此 request 的合法性

### 資料分散算法
- 系統將多個節點組成的存儲空間當作一個環，透過對一個 key 做 hash，他會落在任兩個節點之間或某節點之上。然後沿著順時針下去，找到的第一個比 hash 值大的節點 (想成順時針第一個遇到的節點)就是目標存儲位置。
    - 此方法的好處是，在節點的加入或移除，只會影響其相鄰節點
    - 此方法是比較初步的 hash 算法，這會導致附載不平衡，以及沒有針對節點的不同算力有效分配任務
- 提出虛擬節點的概念，一個物理結點內部會有多個虛擬節點，而虛擬節點可以做為一般結點在環上使用
    - 好處1: 添加或刪除節點，數據變動較為平均，因為都是由同個物理節點管理
    - 好處2: 可以根據物理節點的算力，決定要給予的虛擬節點數量

### 副本複製
- 透過多個節點副本，可以提高可用性以及持久性。這邊決定出一個參數 N，代表此 key-value object 在整個系統裡，應該要至少有 N 個副本
- 範例，假如 N 為 3，環的樣式如下，有一個 key k 被 hash 到 A 和 B 之間，因此該 object 就會在 B, C, D 各有一個副本。進而延伸，也可以看出 D 節點就會保存 (A, B]、(B, C] 和 (C, D] 的資料
![image](https://hackmd.io/_uploads/Bk9Fqzo7C.png)
- 對於一個 key，有存儲該 key 的所有節點可以組成一個列表叫做 preference list (優先列表)，而為了避免有結點失效，其長度會大於 N

### 版本控制
- 為了達成 always writable，假如遇到網路分區的狀況，就會有版本衝突的問題，需要透過調和(reconciliation)處理
    > 購物車目前有 A B C: (v1, ABC)
    > 使用者把 C 移走的這個行為沒有成功寫入: (v1, ABC), (v2-1, AB)
    > 使用者加入 D，但因為系統並不知道 C 被移走了，因此對上個版本 v1 做操作: (v1, ABC), (v2-1, AB), (v2-2, ABCD)
    > 系統回傳所有衝突的 v2 版本，由購物車應用處理衝突，最終呈現 ABCD
- 兩種調和方法
    - syntactic reconciliation: 新版本正常汰換老版本
    - semantic reconciliation: 因為某些原因導致版本出現分支，需透過應用程序協助處理
    - 用購物車例子對照上述的調和方法，v1 因為 syntactic reconciliation 被汰換，而兩個衝突的 v2 透過購物車應用做 semantic reconciliation，對兩個分支做合併回傳結果
- dynamo 透過向量時間來記錄版本，確認兩版本之間有無因果性
    - 向量時間: (node, counter)
        - node 代表哪個 node 執行此次 write
        - counter 代表該 node 對該 object 做過更改的次數
- 範例
    ![image](https://hackmd.io/_uploads/BkYOZ7oXA.png)
    - 第一個請求被 node Sx 處理，產生 object D1，其向量時間為 [(Sx, 1)]
    - 第二個請求還是被 Sx 處理，產生 obejct D2，而 D2 明顯是 D1 的後代，因此可以完全覆蓋 D1，其向量時間為 [(Sx, 2)]
    - 第三個請求被 Sy 處理，產生 object D3，向量時間變成 [(Sx, 2), (Sy, 1)]
    - 第四個請求被 Sz 處理，但是因為 D3 的更新還沒寫到 Sz，Sz 不知道 D3 的存在，所以用 D2 去改，產生 object D4，向量時間為 [(Sx, 2), (Sz, 1)]
    - 假如今天有個節點本身有 D1, D2 的紀錄，在收到 D4 後，就可以知道 D4 是 D1, D2 的後代，直接用 D4 覆蓋過去 (syntactic reconciliation)
    - 假如今天有個節點只知道 D3，在收到 D4 後，從向量時間無法直接判定誰先誰後<b><u>(兩個 node，Sy,Sz 的 counter 無法直接比較)</u></b>，因此兩者都保留下來，使用者 call get() 就回傳 D3, D4，交由使用者處理 (semantic reconciliation)
    - 使用者那邊更新完後，發送第五個請求，產生 object D5，向量時間為 [(Sx, 2), (Sy, 1), (Sz, 1)]。至此系統就可以知道 D5 會是 D1, D2, D3, D4 的後代

### 資料處理過程
- 當收到操作請求，有兩種處理方式
    1. 請求路由給 load balancer，由 balancer 選擇一個合適節點
        - 優: 應用端不用了解 dynamo 底層的實作
    2. 透過 partition-aware 的函式庫，直接導向一個合適的節點
        - 優: 延遲低，不用經過一次轉發
- Preference list 的第一個叫做 coordinator
- 為了達成一致性，對於讀/寫有設定門檻值R/W，代表執行相關操作最少須獲得的票數，且 R+W > N
- put 過程，coordinator 生成向量時間，並將副本保存在 preference list 中，前 N 個健康節點裡面(sloppy quorum)，其中如果有 W 個寫成功，便認為此寫入成功
- get 過程，coordinator 對 preference list 中，前 N 個健康結點做讀取，假如至少 R 個有讀成功，便返回結果。如果結果中，有多個不相干的衝突版本，也一併傳給應用端，由應用端做 reconcile

### 錯誤處理: Hinted Handoff
![image](https://hackmd.io/_uploads/Bk9Fqzo7C.png)
- 此種錯誤處理是針對短暫的節點失效，將請求轉交給其他結點暫為保存
- 前面提過，讀寫操作都是針對前 N 個健康節點，假設 N 為 3，本來應該要給 A 的請求，會寫入 A, B, C，但因為 A 壞掉了，往後變成 B, C, D。而給 D 的請求中，會特別提醒 (hint) 此請求本來應該要給 A 的，之後 D 會一直嘗試 ping A，假如 A 回來了，就把數據傳送回去給 A，而 D 本身就不會應用此請求。

### 錯誤處理: Replica synchronization
- 遇到長時間的節點失效，一個節點也沒辦法暫時保存太多東西，因此改採用直接與其餘節點進行數據同步操作，稱做反熵 (anti-entropy)
- 為了能夠快速對比兩個節點的數據不同，透過 merkle tree 比較。
    - 葉子節點為數據本身，而每兩個數據可以 hash 出一個值，此值為這兩個數據葉子節點的父節點，以此向上推進，直到根結點
    - 每個 node 針對不同的 key range 都有對應的 merkle tree，比較 2 個 node 中，同個 key range 的數據差異，先從根結點看起，假如一樣就是數據副本皆相同，假如不一樣，在一層層往下找，直到發現不同的地方為止
![image](https://hackmd.io/_uploads/BJfDROsQC.png)

### Membership (節點之間的關係) and Failure Detection
- 當有結點加入或是離開，為了讓其他結點也可以快速知道這件事情，採用 gossip 算法傳遞。一個節點剛開始只知道自己目前負責的 key range，之後會一直持續地與多個節點進行此消息兌換，最終節點就會知道自身的 key range 也由哪些節點負責。只要一有結點離開或加入，這些信息都能夠快速更新
- (TODO) 種子節點
- 在有應用端發送請求的時候，兩個節點才會去看對方可不可以達到。而為了快速負責請求，假如 A 節點收不到 B 節點的回復，就算 B 結點跟其他節點是正常的，A 就直接認為 B 失效了，並由與 B 節點管理同個 key range 的其餘節點負責請求，然後定期檢查 B 可不可以正常連線

### Adding/Removing Storage Nodes
- 新結點加入，要把一些 key range 轉交由他負責；刪除也同理，只是反過來做
![image](https://hackmd.io/_uploads/Bk9Fqzo7C.png)
- 假如有一個 X 結點加入 A 和 B 之間，X就須負責 (F, G], (G, A] and (A, X] 之間的 key，而原本 B, C, D 上就不用再負責重複的 range 了。像是 (A, X] 原本會由 B 負責，X 加入後，B 要把這段交由 X 處理，而他自己就不用管了。
- 確保 key range 的均勻分布

## 實作
- 每個節點包含 3 個組件
    - request coordination
    - Membership and Failure Detection (沒特別提及)
    - 本地持久存儲引擎 (資料庫)
- 本地持久存儲引擎
    - dynamo 支持不同種類引擎以插件的方式作為資料庫，包括
        - Berkeley Database (BDB) Transactional Data Store2
        - BDB Java Edition
        - MySQL
        - an in-memory buffer with persistent backing store
    - 透過插件的方式，不同面向的應用可以選擇最適合的引擎。如 BDB 可以負責幾十KB的資料，MySQL 可以處理大一點的
- request coordination
    - 前面有提過 coordinator，dynamo 接收到應用端的請求後，由他負責主導讀寫操作
    - read coordination
        1. 發送讀取請求給節點
        2. 等待所需的最少數量響應 (R)
        3. 如果在規定的上限時間內收到的回應數量太少，認定請求失敗
        4. 否則，收集物件的所有版本，確定應該傳回哪些
        5. 如果開啟了版本化（versioning）配置，執行 syntactic reconciliation，產生一個不透明的寫入上下文（context），其中包含了合併之後的版本對應的的 vector clock
        - 其中，如果有讀到過期版本，coordinator 會把有過期版本的節點，用新版本去覆蓋他，稱作 read repair，可以減少逆熵的工作
    - write coordination
        - 前面有提過 coordinator 是 preference list 的第一個，但是如果請求都一直由她來作，這會造成附載不平衡。因此 preference list 中任一個都可以當作寫操作的 coordinator
        - 在寫之前，通常都會先讀過一遍，因此可以把前一次讀操作返回最快的那個節點作為下次寫操作的 coordinator，降低了請求處理的抖動性

## 