最近在微信群裏看到 @mwish 分享 Amazon 關于 MemoryDB 的論文: Amazon MemoryDB: A Fast and Durable Memory-First Cloud Database[1],讀完感覺設計思路挺不錯,就將個人在閱讀中認爲論文裏面的一些關鍵要點記錄並分享出來。
MemoryDB 是基于 Redis 開發的內存數據庫,主要的目標是解決社區 Redis 在數據一致性以及持久化方面的缺陷。
從産品定位上,MemoryDB 是作爲數據庫數據庫而不是緩存,這一點也體現在命名上,主要解決 IoT / 金融 / 廣告這類寫入吞吐要求高(每秒百萬級別),且延時敏感的業務場景。論文核心是如何通過分布日志系統來解決社區 Redis 存在的持久化以及一致性問題,對于想要實現類似功能的人,從設計到實現思路都可以作爲不錯的參考。
整體設計MemoryDB 是 Amazon 在 2021 年開發,Redis 代碼還沒調整爲非開源 License,所以在設計時重點考慮了如何最小化改動 Redis 代碼,避免合並 Upstream 代碼過于困難。從以下設計圖可以看到,MemoryDB 主要改動點是主從複制的部分: 數據同步不再是寫入主庫後主動同步給從庫,而是先寫入分布式事務日志系統,再由從庫來主動拉取這些變更。
![](http://image.uc.cn/s/wemedia/s/upload/2024/43c29f2f777630329aab7cf56b8785bc.jpg)
寫入和同步流程如下:
對于 Key 變更寫入追蹤緩沖區(tracker),阻塞寫入 Client。注意,這些寫入變更再收到分布式日志系統 ACK 之前是不可見的。提交變更日志到多 AZ 的分布式事務日志系統寫入日志成功之後,主庫執行執行追蹤緩沖區(tracker)的變更,修改對于 Client 可見並解除寫入 寫入 Client 阻塞從庫異步主動拉取並回放變更來保持主從數據最終一致再回到 MemoryDB 的設計目標: 數據強一致和可靠的持久化,那麽這個設計如何做到的?
數據的可靠持久化機制主要靠強一致的多 AZ 分布式事務日志服務來保障,寫入請求必須等待分布式事務日志服務提交才能返回,從而保障一旦提交就不會丟失。同時主從切換時只有追到最新數據的從庫才可能選爲新主庫。而一致性取決于是否允許讀從庫:
只允許讀主庫,可保證順序強一致允許讀從庫,由于從庫是異步更新,只能保證最終一致只允許讀主庫的場景之所以能夠做到順序強一致,首先是 Redis 命令處理上是單線程,能夠天然保證寫入分布式事務日志的順序性。其次,MemoryDB 也對命令讀寫做了控制優化,變更需要等待提交成功之後才會可見,同時讀請求的 Key 如果還未提交也會等待提交後,拿到更新後的值才會返回,兩者結合才能做讀寫線性強一致。
這一點是 Redis 無法做到的,主要原因是 Redis 沒有 MVCC 機制,變更會立馬生效,其他 Client 會在寫入正式被 ACK 之前看到變更。
例子如下:
Client A 發送請求 SET A 2 請求並通過 WAIT 命令等待全部從庫返回Client B 發送請求 GET A ,這是 WAIT 還沒返回,但 Client B 看到卻是最新值 2也就是 Client B 可以看到 Client A 還沒有真正被確認的更新,MemoryDB 在寫入分布式事務日志系統之前會將變更放到緩沖區(tracker),其他 Client 如果讀到正在寫入 Key 需要等待完成才能返回。
而允許多從庫只能做到最終一致比較好理解,因爲主從複制是通過異步日志同步,所以從庫的數據一定是某個時間窗口的數據,而不是最新數據。
另外,論文裏面也提到一個細節點是: 對于查詢請求會立即執行,但在返回之前需要檢查寫入緩沖區(tracker),如果讀到 Key 剛好正在寫入,則延時到寫入成功後才能返回。舉個例子,Client A 執行 SET A 1 未完成,此時 Client B 請求 Get B 則會立即返回,如果請求 Get A 則需要等待 Client A 寫入成功後才能返回。
on-mutating operations can be executed immediately but must consult the tracker to determine if their results must also be delayed until a particular log write completes. Hazards are detected at the key level.
如何恢複數據設計上可以看到主從同步是通過分布式事務日志系統來實現,那麽如果沒有其他手段的輔助,實例在重啓時就需要全量回放曆史變更日志,顯然從時間和可用性上都是無法接受。MemoryDB 實現方式是定期創建 Snapshot 的方式來減少需要回放的日志量,也可以理解爲 RDB + 增量 AOF:
![](http://image.uc.cn/s/wemedia/s/upload/2024/88f9cc441550fc3b54f22305709afbd4.jpg)
這個過程由外部控制面服務(off-box)來來完成,定時跟據狀態來決定是否創建新的 Snapshot(RDB) 並上傳到 S3, Snapshot 裏會記錄分布式事務日志系統的 offset,數據恢複過程是先加載 Snapshot 再從這個 offset 開始回放增量數據。
性能對比在吞吐方面,只讀壓測場景 MemoryDB 在 2xlarge 以上的機型比社區 Redis 更好,MemoryDB 峰值 OPS 可以到 500K Op/s 而社區 Redis 大約爲 330K Op/s。在只寫壓測場景,社區 Redis 的性能大約是 MemoryDB 的兩倍,主要的原因是 MemoryDB 每次寫入都需要提交到分布式事務日志系統,所以延時會更高一些。
![](http://image.uc.cn/s/wemedia/s/upload/2024/193b1968e3ef916c3a9c9b4a12b04dec.jpg)
在延時方面,MemoryDB 和社區 Redis 在 P50/P99 在只讀延時比較接近(圖 a),只寫時 MemoryDB 的 P99 約爲 6ms,而社區 Redis 是 3ms 左右。混合讀寫場景,社區 Redis 的延時也比 MemoryDB 低不少,由此也可以推斷混合讀寫場景 MemoryDB 性能也會差一些。
![](http://image.uc.cn/s/wemedia/s/upload/2024/1d402e3bc5efe56c22cf8866293d53c8.jpg)
另外,這篇論文還提到了 BGSAVE(生成 RDB 文件) 對于延時的影響,可以看到 BGSAVE 對于平均延時幾乎沒有影響,但 P100 延時有明顯的毛刺點。主要的原因是 BGSAVE 在創建新進程 fork 時內核需要拷貝內存的頁表映射,每 GiB 耗時是 12ms 左右。同時,隨著紅色線上升(COW 導致 SWAP 使用變多) 吞吐和延時都會變差,但實際生産環境建議關閉 SWAP,所以沒有太大的參考價值。
![](http://image.uc.cn/s/wemedia/s/upload/2024/a16013e972da3de6fa83840e083ac9e5.jpg)
除此之外,論文裏也提到關于 Slot 遷移以及選主的設計上的調整,選主不再依賴 Cluster Bus 做探測,而是類似 Raft Lease 的方式,一定時間內沒有收到 Leader 的心跳日志則發起選主。Slot 遷移過程接近主從複制,將 Slot 對應全部 Key 全部序列化後發送到目標節點,再增量同步新寫入的部分。
整體上 MemoryDB 思路是通過分布式事務日志系統來增強 Redis 在數據一致性和持久化的可靠性,同時也犧牲了一些性能。不管是在設計和實現上都很值得借鑒,感興趣還是直接看論文,點擊原文可下載論文。
本文作者 hulk,授權自公衆號 編碼技師
參考資料[1]Amazon MemoryDB: A Fast and Durable Memory-First Cloud Database: https://assets.amazon.science/e0/1b/ba6c28034babbc1b18f54aa8102e/amazon-memorydb-a-fast-and-durable-memory-first-cloud-database.pdf
活動介紹:GIAC全球互聯網架構大會本文由高可用架構轉載。技術原創及架構實踐文章,歡迎通過公衆號菜單「聯系我們」進行投稿