跳至主要內容
論文精煉 · Distributed SQL

FoundationDB: A Distributed Unbundled Transactional Key Value Store(FoundationDB:一套分散式、拆解式的交易型 key-value 儲存系統)

把資料庫拆解成互不耦合的元件,在 NoSQL 規模上提供可序列化的 ACID 交易,並用確定性模擬驗證每一項功能。

作者Jingyu Zhou、Meng Xu、Alexander Shraer、Bala Namasivayam 等 21 位作者,分屬 Apple Inc.、Snowflake Inc. 與 antithesis.com 發表於SIGMOD 2021(ACM SIGMOD 國際資料管理研討會),2021 年 6 月 年份2009;2018 年開源
閱讀原始論文 PDF 所有論文

一口氣講完 — 整篇論文的濃縮

雲端服務需要一套既能像 NoSQL 那樣擴充、又能提供跨多個 key 之 ACID 交易的儲存系統,而當時沒有任何現成系統能同時做到,除非依賴特殊硬體或大量的鎖。FoundationDB 把資料庫拆解成一個由 Paxos Coordinators 組成的控制平面,加上一個切成三塊的資料平面:無狀態的交易系統(TS)、由複寫且分片的持久化佇列組成的日誌系統(LS),以及以 SQLite 為引擎的 StorageServers 組成的儲存系統(SS),讀取容量與寫入容量因此可以各自獨立擴充。嚴格可序列化來自兩件事的結合:由單一 Sequencer 發放的 MVCC 讀取版本,以及在依 key 範圍切分的 Resolvers 上執行的樂觀並行控制;commit version 同時就是 log sequence number,直接定義了交易的序列順序。FDB 不用 quorum 去遮蔽故障,而是一旦偵測到交易系統中任何一處失效,就主動把整個交易系統關掉、在新的 epoch 重建,因此只要 f+1 個複本就能容忍 f 個故障,復原時間中位數也只有 3.08 秒。每一項功能都在具備隨機故障注入的確定性離散事件模擬器裡跑過,這正是 CloudKit 累積超過 50 萬個磁碟年、卻從未發生任何一次資料毀損的原因。

在這篇論文之前 — 它所降落的世界

到 2000 年代末,雲端服務底下那層可擴充的儲存幾乎清一色是 NoSQL:Bigtable、Dynamo、PNUTS、Cassandra、MongoDB、CouchDB。這些系統為了撐到數十億使用者而放棄交易語意,改為提供最終一致性,等於把「並行操作的更新如何交錯」這件事整包丟回給每一位應用開發者去推理。另一條路的真資料庫則把儲存引擎、資料模型與查詢語言綁成一包,想要交易的團隊只能三個全收或全不收;而 Percolator、Tephra、Omid 這類在 key-value store 之上疊一層交易 API 的作法,也只做到快照隔離。真正提供分散式交易的系統,例如用 TrueTime 的 Spanner、用 hybrid-logical clock 的 CockroachDB,以及 Deuteronomy 這類已走拆解路線的設計,全都是在取得所有鎖的那一刻決定序列順序,而且要用 2f+1 個複本換容錯。同時,整個業界測試分散式系統靠的仍是非確定性的故障注入,而那正是最沒辦法重現自己剛剛抓到的那個 bug 的技術;模型檢查雖然能驗證協定,卻驗證不到真正出貨的實作。

問題 — 當時真正壞掉的地方

  • NoSQL 系統是用放棄交易語意換來擴充能力的,只提供最終一致性,迫使應用開發者自己去推敲並行操作之間更新交錯的所有可能。
  • 多數資料庫把儲存引擎、資料模型與查詢語言綁在一起,使用者只能三個全要或全不要;某些功能對部分應用有益,卻讓不需要它、或只需要略有不同版本的應用被迫繞路。
  • 分散式資料庫慣用 quorum 遮蔽故障,代價是要容忍 f 個故障就得放 2f+1 個複本,在 PB 級的資料量下這是很大的資源倍數。
  • 若試圖在交易路徑上逐一處理每一種故障情境,就會長出大量極少被執行的錯誤處理程式碼,而極少被執行的程式碼,通常也就是極少是正確的程式碼。
  • ARIES 一脈的復原機制依賴週期性檢查點,並在復原時重放 redo 與 undo 日誌,因此復原時間會隨日誌大小成長,偏偏那正是客戶端在等資料庫回來的時刻。
  • 非預期的行程與網路故障、訊息重排以及其他非確定性來源,會引出極難重現、也極難除錯的微妙錯誤;而資料庫是有狀態的,這類錯誤造成的資料毀損可能好幾個月都沒被發現。需要特定當機順序才會發作的 deep bug 更是打得過一般的端到端測試,而模型檢查驗證的是模型,不是真正的實作。

核心概念 — 主要貢獻,以及它們為何成立

控制平面與資料平面的拆解

FDB 把叢集切成兩半:控制平面把關鍵系統中繼資料存在組成 Active Disk Paxos 群組的 Coordinators 上,資料平面則負責真正的工作。資料平面本身又再拆成三塊:由 Sequencer、Proxies 與 Resolvers 組成、完全無狀態並在記憶體中處理交易的交易系統;持有 write-ahead log 的 LogServers 所組成的日誌系統;以及持有資料、負責服務讀取的 StorageServers 所組成的儲存系統。每一種角色都是獨立的行程型態,維運者可以各自佈署與擴充,甚至把不同性質的角色放到不同規格的機器上以最佳化成本。這套設計成立的關鍵是:ClusterController、Sequencer、DataDistributor、Ratekeeper 這些單例只做有限的中繼資料操作,從不坐在資料路徑上,因此不會變成瓶頸。

刻意極簡的功能集,複雜度交給 layer

FDB 在一個有序的 key 空間上只提供 get、set、getRange 與 clear,沒有結構化語意、沒有查詢語言、沒有資料模型與 schema 管理,也沒有次要索引。論點不是這些功能沒用,而是提供它們雖然嘉惠部分應用,卻會讓其他不需要、或只需要稍微不同形式的應用被迫繞開它們。更豐富的東西一律以 layer 的形式建構:一個疊在上面的無狀態應用,負責提供資料模型,並且免費繼承 FDB 的交易能力。這個賭注的證據就是實際被造出來的東西有多廣:提供關聯結構與索引的 FoundationDB Record Layer、Document Layer、給圖資料庫用的 JanusGraph 儲存轉接器,以及在最新版本中被重新實作成 FDB layer 的 CouchDB。

OCC 加 MVCC 得到可序列化快照隔離

一筆交易會拿到一個保證不小於它開始前任何已發出 commit version 的讀取版本,在該版本上讀取一致的 MVCC 快照,寫入則先緩衝在客戶端。提交時它會拿到一個大於所有既有讀取版本與 commit version 的 commit version,而這個數字同時定義了序列歷史,並直接被系統其他部分當作 Log Sequence Number 使用。因為這筆交易看得到先前所有已提交交易的結果,而且被排在它們之後,得到的是嚴格可序列化,而不只是可序列化。衝突偵測因此退化成 Resolvers 上單純的讀寫檢查,全程沒有任何鎖,大幅簡化了交易系統與儲存系統之間的互動;代價是要在 Resolver 的記憶體裡保留近期的提交歷史,並且接受交易不保證能提交。

先發版本,再做衝突偵測

write-snapshot isolation 是先檢查讀取集合、才指派時間戳,FDB 反過來:先決定 commit version,之後才做衝突解析。這個順序上的反轉正是批次化能成立的原因:一個 Proxy 可以把許多客戶端交易併成一批,向 Sequencer 一次要一個 commit version,再把整批送進 Resolvers,於是版本指派與衝突偵測的成本都被攤平。Sequencer 以每秒一百萬個版本的速率推進版本號,而批次化讓 Proxies 每秒能提交數萬筆交易而不對 Sequencer 造成明顯壓力。批次大小還會動態調整:系統負載輕時縮小以壓低提交延遲,系統忙碌時放大以維持高吞吐量。

把故障當成常態

FDB 完全不打算原地扛過故障。Sequencer 監看 Proxies、Resolvers 與 LogServers,只要任何一個失效、或資料庫組態發生變更,它就直接結束自己;ClusterController 察覺後招募新的 Sequencer,整個交易管理系統就在新的 epoch 裡重建。於是所有故障處理都收斂成單一個復原操作,成為一條常被走到、也被充分測試的程式碼路徑,而不是一叢極少被執行的特例。這個策略只有在復原夠快時才成立,這也是 FDB 死命壓低 Mean-Time-To-Recovery 的理由;而它的回報有兩層:正常交易處理的邏輯變簡單,而且因為故障是被復原而非被遮蔽,FDB 只需要 f+1 個複本、而非 2f+1 個,就能容忍 f 個故障。

沒有 redo、也沒有 undo 的復原

復原被刻意做得非常便宜:沒有檢查點,也不重放日誌。關鍵洞見是 redo 日誌的處理其實與正常的日誌前推路徑一模一樣——StorageServers 本來就持續從 LogServers 拉日誌並在背景套用——所以 redo 被完全從復原中解耦,而不是復原必須扛的工作。復原因此只需要判定 redo 日誌在哪裡結束,新的交易系統甚至可以在舊 LogServers 還沒被抽乾之前就開始接受交易。undo 同樣便宜,因為只有離開五秒 MVCC 視窗的變動才會寫進 SQLite B-tree,所以回滾就只是丟掉記憶體中的多版本資料。結果是復原時間只受系統中繼資料大小約束,與資料量或日誌大小無關。

先有確定性模擬器,才有資料庫

在動手寫資料庫之前,團隊先寫了一個確定性的離散事件模擬器,能在單一實體行程內跑起多台 FDB 伺服器,網路、磁碟、時鐘與亂數產生器全部是模擬的。為了讓這件事可行,所有資料庫程式碼都必須是確定性的,也徹底避開多執行緒並行——每個核心只佈署一個資料庫節點——並改用 Flow 表達並行,那是一套為 C++ 加上 async/await 式 actor 的語法擴充。它的價值不只在於能注入故障,更在於找到的每個 bug 都能精準重現:加上額外的日誌通常不會擾動事件順序,所以同一個失效可以重播到被弄懂為止。模擬還能比真實時間跑得更快,只要 CPU 使用率低就把時鐘快轉,而那正好是分散式系統 bug 最愛躲藏的漫長安靜時段。

運作方式 — 具體的機制

端到端的提交路徑

客戶端先向某個 Proxy 索取讀取版本;Proxy 向 Sequencer 要一個不小於先前所有已發出 commit version 的版本,再回傳給客戶端。接著客戶端直接對 StorageServers 在該版本上讀取,寫入則緩衝在本地,read-your-writes 語意靠把資料庫查詢結果與本地未提交寫入合併來維持。提交時,客戶端把讀取集合與寫入集合(都以 key 範圍表示)送給某個 Proxy;Proxy 向 Sequencer 取得 commit version,把交易送往依範圍切分的 Resolvers,全體放行後再把日誌訊息廣播給 LogServers。等到所有被指定的 LogServers 都回報持久化完成,交易才算提交;Proxy 隨後把已提交版本回報給 Sequencer,好讓後續的讀取版本排在它之後,最後才回覆客戶端。唯讀交易根本不走這條路——它在自己的讀取版本上就是可序列化的,客戶端可以在本地直接提交,完全不必聯絡叢集。

Resolver 的衝突偵測

每個 Resolver 維護一份 lastCommit,把近期被修改的 key 範圍對應到修改它的 commit version,實作上是一個帶版本資訊的機率式 SkipList。對交易 Tx 而言,讀取集合 Rr 中的每個範圍都會與 lastCommit 取交集,只要有任何相交項目的 commit version 大於 Tx 的讀取版本,交易就中止;由於檢查的單位是範圍而非個別 key,這同時也擋掉了幻讀。若沒有衝突,就把寫入集合 Rw 中每個範圍蓋上 Tx 的 commit version,並放行這筆交易。整個 key 空間被切給多個 Resolver 以便平行執行,而交易必須被所有 Resolver 放行才能提交——這也意味著被某個 Resolver 拒絕的交易,可能已經污染了另一個 Resolver 的 lastCommit,替後來的交易製造偽陽性衝突。實務上這是可以忍受的,因為一筆交易的 key 範圍通常落在單一個 Resolver 內,而過期項目會隨五秒的 MVCC 視窗被淘汰;Resolver 之間的範圍邊界也會動態調整以平衡負載。

日誌協定與變動標記

Proxy 先查自己記憶體中的 shard map,找出哪些 StorageServers 擁有被修改的 key 範圍,然後把這些 StorageServer 標記附在該筆變動上。每個標記都有一個偏好的 LogServer,因此變動的內容只送往那些偏好的 LogServers,再加上為了滿足複寫度所需的額外幾台;其餘 LogServers 仍會收到訊息,但內容是空的,好讓所有 LogServer 都維持在 LSN 的順序上。訊息標頭帶著 LSN、前一個 LSN,以及該 Proxy 的 Known Committed Version;當所有複本 LogServer 都確認某個 LSN 已持久化,Proxy 就把 KCV 推進到該 LSN。把 redo 日誌從 LogServers 送往 StorageServers 並不屬於提交路徑:StorageServers 會積極地拉取,甚至在資料還沒在日誌系統上持久化之前就先拉,這讓多版本讀取維持很低的延遲,代價是那些半提交的更新日後可能需要回滾。也因為持久性已經由 LogServers 保證,StorageServers 可以先在記憶體中緩衝,再以較長的延遲整批寫到磁碟,藉由合併更新提升 I/O 效率。

決定 Recovery Version

新 epoch 的 Sequencer 先從 Coordinators 讀出前一代交易系統的狀態並上鎖,避免另一個 Sequencer 同時進行復原,接著停掉所有舊的 LogServers。每台 LogServer 回覆兩個數字:Durable Version,也就是它已持久化的最大 LSN;以及它從任何 Proxy 收到的最大 Known Committed Version。假設有 m 台舊 LogServer、複寫度為 k,只要收到超過 m-k 份回覆,Sequencer 就取所有 KCV 的最大值作為前一個 epoch 的結束版本(PEV),因為在此之下的資料都已完整複寫;再取所有 DV 的最小值作為 Recovery Version(RV)。新 epoch 從 PEV+1 開始,PEV+1 到 RV 這段日誌會從舊 LogServers 複製到新的上面,以修補複寫度——那只有幾秒鐘的資料量,所以複製成本極小。新 epoch 的第一筆交易是一個特殊的復原交易,把 RV 告知 StorageServers,它們就靠丟掉記憶體中版本高於 RV 的多版本資料完成回滾,然後開始從新的 LogServers 拉資料。

重新組態、epoch 與 bootstrap

FDB 不依賴任何外部服務。所有使用者資料與大部分系統中繼資料(以 0xFF 為前綴的 key)都存在 StorageServers 裡;關於 StorageServers 的中繼資料存在 LogServers 裡;而日誌系統本身的組態則存在每一台 Coordinator 上。伺服器把 Coordinators 當作 disk Paxos 群組,在沒有 ClusterController 時搶著當選;當選的 ClusterController 招募一個 Sequencer,該 Sequencer 從 Coordinators 讀出舊日誌系統的組態,生出新的交易系統與日誌系統,讓 Proxies 從舊日誌系統復原系統中繼資料,最後把新的日誌系統組態寫回所有 Coordinator,才開始接受交易。每一個這樣的世代就是一個 epoch,由其唯一的 Sequencer 行程標識。由於 Proxies 與 Resolvers 都是無狀態的,它們的復原不需要任何額外工作;全部成本都在正確地停下並抽乾舊的 LogServers。

複寫與感知故障域的 team

三種複寫策略同時並存。控制平面的中繼資料以 Active Disk Paxos 存在 Coordinators 上,只要多數存活就能復原。日誌紀錄同步複寫到 k = f+1 台 LogServer,而且必須全部 k 台都持久化成功,Proxy 才會回覆客戶端,所以只要一台 LogServer 失效就會觸發整個交易系統的復原。每個 shard 則非同步複寫到 k = f+1 台 StorageServer,這組機器稱為一個 team;一台 StorageServer 通常隸屬許多 team,資料因此均勻散開,而 DataDistributor 會把資料從失去成員的 team 搬到健康的 team。放置策略比 Copysets 更講究:FDB 同時在主機層與行程層建構複本集合,確保同一個複本群組不會有兩個行程落在同一個故障域,也就是說唯有被選中的主機群組整組同時失效才會遺失資料。

模擬器與它的故障注入

模擬器在單一個離散事件模擬中生出許多台透過模擬網路互相溝通的 FDB 伺服器,而正式版本只是把同一組介面接到真正系統呼叫的薄薄一層轉接。工作負載同樣以 Flow 撰寫,負責驅動這些伺服器,內含故障注入指令、模擬應用、資料庫組態變更,以及直接呼叫內部功能;它們可以互相組合,讓測試案例像積木一樣拼出來。注入的故障包括機器、機架與資料中心層級的 fail-stop 失效與重開機,各種網路故障、網路分割與延遲問題,磁碟的異常行為(例如重開機時未同步寫入被毀損),以及事件時間的隨機化;注入比率經過仔細調校,避免故障率過高反而把系統壓進很小的狀態空間。在這之上還有 buggification:程式碼裡有許多位置刻意讓模擬器有機會注入不尋常但不違反合約的行為,例如讓通常會成功的操作回傳錯誤、在通常很快的操作裡插入延遲、或替調校參數挑一個奇怪的值,這同時也保證不會有某個特定調校值悄悄變成正確性的必要條件。swarm testing 則把叢集規模、組態、工作負載、故障注入參數、調校參數,以及啟用哪一組 buggification 點全部隨機化;而條件式覆蓋率巨集讓開發者可以斷言某個罕見情境確實在眾多模擬回合中被走到。

論文證明了什麼 — 量測數據與證明

  • 被量測的 Apple 生產環境跨區叢集共 58 台機器(主資料中心與遠端資料中心各 25 台,兩個衛星站各 4 台)、862 個 FDB 行程外加 55 個備援行程,並在 464 顆 SSD 上存放 292 TB 資料;一個月內平均每秒 390.4K 次讀取操作、138.5K 次寫入操作,以及讀出 1.467M 個 key。
  • 在該叢集上,讀取延遲平均約 1 毫秒、99.9 百分位約 19 毫秒;提交延遲平均約 22 毫秒、99.9 百分位約 281 毫秒——提交的平均值之所以低於 60.6 毫秒的跨區 WAN 延遲,正是因為跨區複寫是非同步的。當月平均交易衝突率為 0.73%。
  • 機器數從 4 台擴到 24 台(Proxies 與 LogServers 從 2 個增至 22 個,複寫度為 3)時:每筆交易 100 個操作的寫入吞吐量由 67 MBps 成長到 391 MBps(5.84 倍),500 個操作時由 73 MBps 成長到 467 MBps(6.40 倍);讀取吞吐量由 2,946 MBps 成長到 10,096 MBps(3.43 倍)、由 5,055 MBps 成長到 21,830 MBps(4.32 倍);90/10 讀寫混合負載由每秒 593k 個操作成長到 2,779k(4.69 倍)。寫入尖峰時 LogServers 的 CPU 飽和,讀取尖峰時輪到 StorageServers,混合負載時則是 Resolvers 與 Proxies。
  • 在 24 台機器、配置 2 個 Resolver、22 個 Proxy、22 個 LogServer 與 336 個 StorageServer 的組態下,每秒操作數低於 10 萬時,平均讀取一個 key 約 0.35 毫秒、取得讀取版本約 1 毫秒、提交約 2 毫秒;當每秒操作數達到 200 萬時,Resolvers 與 Proxies 飽和,提交延遲飆到 368 毫秒。
  • 從存放數百 TB 資料的生產叢集蒐集到的 289 筆重新組態紀錄顯示,復原時間中位數為 3.08 秒、90 百分位為 5.28 秒,原因是復原時間只取決於系統中繼資料大小,而非資料量或日誌大小;被量測的生產叢集在 2020 年 8 月只發生一次復原,歷時 8.61 秒,相當於五個 9 的可用性,而且整個過程中客戶端讀取完全不受影響,因為 StorageServers 照常服務。
  • 其他佐證:微基準測試中,單執行緒的 Resolver 可穩定處理每秒 280K 筆交易;生產環境 12 小時內,StorageServer 落後 LogServers 的延遲,其平均值的 99.9 百分位為 3.96 毫秒、最大值的 99.9 百分位為 208.6 毫秒;CloudKit 已部署 FDB 超過 50 萬個磁碟年而未發生任何一次資料毀損事件,持續進行的複本比對也從未發現過不一致的複本。

限制與取捨 — 論文自承的,以及後來被發現的

  • 論文自承:樂觀並行控制無法保證交易終究能提交,而且 Resolvers 必須把近期的提交歷史留在記憶體裡。更麻煩的是,一筆被某個 Resolver 中止的交易,可能已經被其他 Resolver 放行並蓋上 lastCommit,於是替毫不相干的交易製造偽陽性衝突。FDB 的辯護是:只有在其多租戶生產工作負載的衝突率低於 1% 的前提下,這才可以接受。
  • 論文自承:五秒的 MVCC 視窗是交易存續時間的硬上限,設這個值是為了限制 Resolvers 與 StorageServers 的記憶體用量,隨之而來的還有 10 KB 的 key、100 KB 的 value 與 10 MB 的交易大小限制。長時間的工作必須人工拆成許多小交易,這個模式常見到 FDB 得把它包成一個叫 TaskBucket 的抽象,供自家的備份系統使用。
  • 論文自承:模擬無法可靠地偵測效能問題,例如不夠好的負載平衡演算法;也無法測試第三方函式庫,或任何不是以 Flow 撰寫的自家程式碼;更看不見相依系統本身的錯誤——FDB 有好幾個 bug 就來自作業系統真正的合約比團隊以為的更弱。這也逼使 FDB 盡量不依賴外部系統,包括刪掉對 Zookeeper 的依賴,改用 Flow 從頭寫一套自己的 Paxos。
  • 論文自承:由於日誌紀錄必須抵達全部 k = f+1 台 LogServer 才會回覆提交,任何一台 LogServer 失效都會觸發整個交易系統的復原;而這套不用 quorum 的立場,論文也明講最適合本地或都會區範圍的佈署。若整個區域同時失效而必須手動跨區切換,得到的是 ACID 中的 A、C、I,但可能出現持久性(D)的失敗。
  • 部分自承、部分是後來才浮現:真正的天花板在寫入路徑,因為每一個讀取版本與 commit version 都出自單一個 Sequencer、以每秒一百萬個版本推進,而論文自己的擴充性實驗就顯示 Resolvers 與 Proxies 會先飽和、提交延遲飆到 368 毫秒。第 6 節已經列出正在進行的緩解措施——把 Proxies 拆成取讀取版本的 proxy 與提交用的 proxy、加入儲存快取——後續的 FDB 版本也沿著同一條路走下去,就像它把修改版 SQLite B-tree 引擎換成 RocksDB 那樣。

它後來變成什麼 — 繼承這個想法的系統

Apple 在 2018 年將 FoundationDB 開源,而論文中的架構至今仍是實際出貨的樣貌:Coordinators、Sequencer、Proxies(後來正如第 6 節所預告的,被拆成取讀取版本的 proxy 與提交用的 proxy)、Resolvers、LogServers 與 StorageServers,儲存引擎則在修改版 SQLite 之外加入了 RocksDB。layer 這個賭注被實際建出來的東西證明了:FoundationDB Record Layer 讓 CloudKit 在同一個 key-value 核心上得到關聯結構、索引與查詢規劃器;還有 Document Layer、JanusGraph 的儲存轉接器、把最新版本重建成 FDB layer 的 CouchDB,以及後來的 Deno KV 與 Tigris Data。Snowflake 用 FoundationDB 當作它的交易型中繼資料儲存,於是同一套基底同時撐著一個雲端資料倉儲與一個行動裝置同步服務,這差不多就是論文想論證的事。而它最能被移植出去的想法,最後證明是那套測試方法論:本篇論文有三位作者掛的是 antithesis.com,他們創辦 Antithesis 把確定性模擬測試變成一項服務;TigerBeetle、RisingWave、Convex 以及 Rust 的 madsim 與 turmoil 生態系,也都照著 FDB 的樣子打造自己的模擬器,而 FDB 的 swarm testing 框架則以 Joshua 之名開源。至於那個拆解後的形狀——無狀態的交易層之上、複寫日誌之下、再由儲存層去追那份日誌——正是雲端原生的儲存與運算分離最後收斂到的同一種分解方式;而 f+1、寧可復原也不遮蔽故障的立場,至今仍是「容錯必須在提交路徑上取得 quorum 共識」這個假設最鋒利的反例。

論文原文 — 逐字引用

“FoundationDB adopts an unbundled architecture that decouples an in-memory transaction management system, a distributed storage system, and a built-in distributed configuration system.”

摘要

“instead of fixing all possible failure scenarios, the transaction system proactively shuts down when it detects a failure. As a result, all failure handling is reduced to a single recovery operation, which becomes a common and well-tested code path.”

§2.1 設計原則

“In FDB, the recovery is purposely made very cheap—there is no checkpoint, and no need to re-apply redo or undo log during recovery.”

§2.4.4 交易系統復原

術語 — 依本篇論文的用法

Unbundled architecture(拆解式架構)
承 Lomet 等人的說法,指把交易元件(TC)與資料元件(DC)拆成不同系統的資料庫。FDB 拆得更徹底:交易系統、日誌系統與儲存系統是三個能各自擴充的層級,連交易日誌都從交易元件中解耦出來。
Layer
疊在 FDB 之上的無狀態應用,負責提供 FDB 刻意不做的資料模型、查詢能力或其他資料庫功能。layer 直接繼承 FDB 的嚴格可序列化交易,這也是關聯式儲存、文件儲存與圖資料庫能共用同一個 key-value 核心的原因。
Sequencer
負責替每一筆交易指派讀取版本與 commit version 的單例行程,版本以每秒一百萬個的速率推進。它發出的 commit version 定義了資料庫的序列歷史,並被直接當作 Log Sequence Number 使用。
Resolver
執行 FDB 樂觀並行控制的無狀態行程,做法是拿交易的讀取範圍去比對一份近期被修改 key 範圍的歷史。key 空間被切分給多個 Resolver,交易必須被每一個 Resolver 都放行才能提交。
LogServer
日誌系統的成員,扮演複寫、分片、分散式的持久化佇列,替特定的 StorageServers 保存 write-ahead log 資料。必須全部 k = f+1 台指定的 LogServer 都回報紀錄已持久化,提交才會被確認。
Storage team
非同步複寫同一個 shard 的那 k = f+1 台 StorageServer。一台 StorageServer 會隸屬許多 team,讓它的資料廣泛散開;一旦某個 team 失去成員,DataDistributor 就會把 shard 搬走。
Known Committed Version(KCV)
某個 Proxy 已確認提交的最大 LSN,也就是所有複本 LogServer 都已回報持久化的那個位置。Proxies 會把自己的 KCV 夾帶在日誌訊息中,而復原時會取所有 LogServer 上 KCV 的最大值作為前一個 epoch 的結束版本。
Recovery Version(RV)
為失效的 epoch 選定的 redo 日誌結尾,計算方式是取舊 LogServers 上 Durable Version 的最小值。舊 LogServers 與 StorageServers 中版本高於 RV 的資料一律丟棄,這就是 FDB 用來取代 undo 日誌處理的全部機制。
Buggification
一種故障注入手法:程式碼本身在許多位置留下讓模擬器注入不尋常、但不違反合約之行為的機會,例如讓通常成功的操作回傳錯誤、替快速操作插入延遲,或挑一個奇怪的調校參數值。它讓罕見狀態變得常見,也確保沒有任何調校值會悄悄成為正確性的必要條件。

在時間軸上的位置 — 這篇論文在整段故事中的座標

在時間軸上查看