前几篇把写路径拆到 TLog 提交点 与 Storage Server 异步应用。生产里真正决定可用性窗口的,往往不是「某个角色挂了怎么修」,而是 FoundationDB 如何把几乎所有事务系统故障收敛到同一条恢复路径。本文按 Zhou et al., SIGMOD 2021(下称 FDB paper)拆:角色招募、恢复 epoch、Recovery Version(RV)/ Previous Epoch end Version(PEV)、写暂停与读边界、旧角色隔离。
版本锚定:机制以 FoundationDB: A Distributed Unbundled Transactional Key Value Store(SIGMOD 2021)与 FoundationDB 7.x 官方 Architecture / Administration 文档为准。论文给出的生产恢复耗时是作者部署观测,不是本站实测。
一、设计前提:把失败做成常态路径
FDB paper 把事务系统的故障策略写成两句原则:
- Make failure a common case:检测到故障时,事务管理系统主动关闭,而不是为每一种故障写独立修复分支。
- Fail fast and recover fast:可用性目标落在缩短 Mean-Time-To-Recovery(MTTR)——检测、主动停写、恢复——而不是无限拉长局部自愈。
因此,Proxy、Resolver、Log Server(TLog)乃至 Sequencer 自身的许多失败,最终都落到:ClusterController 招募新 Sequencer → 新 Sequencer 走同一套 bootstrapping / recovery 步骤 → 开启新的 epoch。配置变更也走同一路径:改配置会终止当前 Sequencer,再由新 Sequencer 重建事务系统。
flowchart TD
FAULT["Fault or config change"]
FAULT --> DET["ClusterController detects Sequencer / role failure"]
DET --> KILL["Current Sequencer terminates"]
KILL --> RECRUIT["Recruit new Sequencer"]
RECRUIT --> EPOCH["New epoch: lock Coordinators, stop old TLogs, recruit roles"]
EPOCH --> ACCEPT["Accept commits in new epoch"]
这条路径和 Coordinator 配置篇 的职责边界一致:Coordinator 持久化的是事务系统拓扑与配置,不是用户数据;用户数据仍在 Storage / Log 一侧。
二、Epoch:一代事务系统的边界
每个 epoch 对应一代事务管理系统,由唯一的 Sequencer 进程标识。同一时刻只应有一个 Sequencer 持有 Coordinator 上的锁并推进恢复;否则会出现两套 Proxy/TLog 同时收写。
| 概念 | 含义 | 本系列位置 |
|---|---|---|
| Epoch | 一代 TS/LS(Sequencer + Proxy + Resolver + TLog) | 本篇 |
| Commit version / LSN | Sequencer 分配的提交序 | 第 6 篇、第 7 篇 |
| KCV | Proxy 已知已提交的最大 LSN | 本篇 §四 |
| DV | LogServer 已持久化的最大 LSN | 本篇 §四 |
| PEV | 上一 epoch 已确认提交的上界(max KCV) | 本篇 §四 |
| RV | 恢复版本(min DV);高于 RV 的未充分确认数据可丢弃 | 本篇 §四 |
Proxy 与 Resolver 无持久用户状态:招募新实例即可,恢复步骤几乎没有额外工作。难的是 LogServer:必须保证「客户端已经收到 commit 成功」的日志,在配置的复制度下仍可被 Storage Server 取回。
三、恢复步骤:从锁定到接受新提交
FDB paper §2.4.4 给出 Sequencer 在每个 epoch 上的恢复顺序。下图按论文步骤组织(图内英文标签):
sequenceDiagram
participant CC as ClusterController
participant SEQ as New Sequencer
participant COORD as Coordinators
participant OLD as Old LogServers
participant NEW as New Proxies Resolvers TLogs
participant SS as StorageServers
CC->>SEQ: Recruit Sequencer
SEQ->>COORD: Read prior TS state and lock
SEQ->>OLD: Stop accepting new transactions
OLD-->>SEQ: Reply with DV and KCV
SEQ->>NEW: Recruit Proxies Resolvers LogServers
SEQ->>COORD: Write new TS state unlock
SEQ->>SS: Special recovery txn carries RV
SEQ->>SEQ: Accept new commits from PEV+1
逐步对照:
读并锁定 Coordinator 状态
Sequencer 读取上一世代的事务系统配置(含旧 LogServer 集合),并对协调状态加锁,防止另一 Sequencer 并行恢复。停止旧 LogServer 收写
旧 TLog 不再接受 Proxy 的新日志;响应中带回各自的 DV 与 KCV,供计算 RV / PEV。招募新 Proxy、Resolver、LogServer
由新 Sequencer 完成(论文写明 Sequencer「for historical reasons」也负责招募这些角色)。Proxy / Resolver 无状态;新 TLog 需要承接日志职责。写回新拓扑并解锁
当前 epoch 的角色集合持久化到 Coordinator 后,恢复路径才算完成「配置世代」切换。接受新提交
第一条特殊恢复事务把 RV 告知 StorageServer,使其丢弃内存中高于 RV 的多版本数据;随后正常写路径从新 epoch 的版本区间继续。
论文强调:StorageServer 始终在后台从 LogServer 拉日志并应用——这与正常前向路径相同。因此恢复不需要 ARIES 式从 checkpoint 重放 redo/undo;恢复只需要找到 redo 日志的终点(RV),重放仍由 Storage 异步完成。新事务系统甚至可以在旧 LogServer 上全部数据尚未被消化完之前就开始接受新事务。
四、RV 与 PEV:提交有界与旧日志隔离
Proxy 向 LogServer 写日志时捎带自身的 Known Committed Version(KCV)。每个 LogServer 维护:
- 收到的最大 KCV;
- Durable Version(DV):本地已持久化的最大 LSN。
恢复时,设日志复制度为 \(k\)。Sequencer 在收到足够多(论文写「多于 \(k-1\)」;另可用 Copyset 把门槛降到 \(k/2\))的旧 LogServer 应答后:
\[ \mathrm{PEV} = \max_i \mathrm{KCV}_i,\qquad \mathrm{RV} = \min_i \mathrm{DV}_i \]
| 版本 | 语义 |
|---|---|
| PEV | 上一 epoch 已向客户端确认提交的上界;此前数据已按复制策略落稳 |
| 新 epoch 起始 | \(\mathrm{PEV}+1\) |
| RV | 恢复点;区间 \([\mathrm{PEV}+1,\mathrm{RV}]\) 上的日志需从旧 TLog 拷到新 TLog,以补齐复制度 |
| \(> \mathrm{RV}\) | 可能未充分确认;Storage 侧丢弃内存 MVCC 中高于 RV 的部分 |
拷贝区间通常只覆盖「数秒」量级的日志后缀(论文定性描述,非本站计时)。这与 第 9 篇 的 5 秒事务窗口同一数量级:冲突历史与未落稳日志都不允许无限变长。
flowchart LR
subgraph versions ["Version axis"]
PEV["PEV = max KCV"]
RV["RV = min DV"]
NEXT["New epoch starts at PEV+1"]
end
PEV --> NEXT
NEXT --> COPY["Copy PEV+1..RV to new TLogs"]
RV --> DROP["Discard SS in-memory data above RV"]
旧角色隔离由此落地:旧 LogServer 在停止收写并交出 DV/KCV 之后,不再参与新 epoch 的提交;若进程仍存活,其「继续写」路径已被显式关闭。旧 Proxy / Resolver 随世代切换被新实例替代,其内存冲突历史也不跨 epoch 复用——跨 epoch 的读写事务本来就不能存活(第 9 篇 的时间上限与世代切换一致)。
4.1 为何可以「先开写、后追日志」
传统 ARIES 路径里,恢复完成前通常要先把 redo 推到数据页一致点。FDB 把「找到 redo 终点」与「把 redo 应用到 Storage」拆开:
| 步骤 | 是否在恢复临界路径 | 承担者 |
|---|---|---|
| 锁定 Coordinator、停旧 TLog、算 RV/PEV | 是 | 新 Sequencer |
| 招募新 Proxy / Resolver / TLog | 是 | 新 Sequencer |
| 特殊恢复事务广播 RV | 是(短) | 新世代写路径首笔 |
| Storage 拉齐 \([\mathrm{PEV}+1,\ldots]\) 日志 | 否(后台) | StorageServer |
| 旧 TLog 数据最终可退役 | 否 | 追平后的运维/GC 语义 |
因此 MTTR 主要由控制面与日志终点发现决定,而不是由数据体积上的全量重放决定。这是 Unbundled 在恢复维度上的直接收益,也是第 14 篇确定性模拟必须反复覆盖的路径:临界路径短,但分支多(应答子集、复制门槛、拷贝后缀失败等)。
4.2 复制策略与「谁失败会触发恢复」
FDB paper §2.5 区分三类复制:
- 元数据:Coordinator 上的 disk Paxos;多数存活即可恢复配置。
- 日志:Proxy 写日志时对每个分片日志做同步复制;未满复制成功不得向客户端确认 commit。单个 LogServer 失败会触发事务系统恢复。
- 存储:shard 异步复制到 team;Storage 失败由 DataDistributor 搬迁,不必然等于一次 Sequencer epoch 切换。
运维含义:TLog 故障与 Storage 故障的用户可见面不同——前者常表现为写暂停窗口,后者常表现为迁移流量与局部读压力。混为一谈会找错排障入口(见 第 17 篇)。
五、写暂停、读边界与可用性窗口
5.1 写路径
恢复进行中,旧 TLog 已停写,新 Sequencer 尚未
Accept commits
时,提交路径不可用。客户端表现为提交超时、连接重置或需要重试的错误;正确的客户端应走官方文档中的自动重试语义(第
2 篇),而不是假定「部分提交已成功却无响应」。
论文的生产观测(FDB paper §5.3,作者部署数据):端到端恢复耗时通常 小于约 5 秒。这是论文数字,部署拓扑与负载不同时不可外推成本站结论。
5.2 读路径
读请求直接打到分片后的 StorageServer(第 11 篇)。恢复设计刻意让 redo 应用与恢复解耦,因此:
- 已在 Storage 上、且不超过可读版本的数据,读路径不依赖「当前是否正在招募 Proxy」;
- 需要新的 read version 的事务,仍依赖 Grv Proxy / Sequencer;事务系统未就绪时,新快照读也会卡住或失败。
工程上应把「写暂停」与「读是否完全可用」分开描述:恢复窗口里常见的是 提交中断、读降级或读版本获取失败,而不是整库进程全部停机。跨区域复制与卫星站点上的 failover 另见论文 §3;本篇只钉单集群事务系统恢复。
5.3 与 Storage 落后的关系
Storage 落后于 TLog 是常态异步间隙(论文 Figure 3 给出某一生产集群上的 lag 分布)。恢复不要求先把 Storage 追平;追平仍是后台拉日志。排障时若「恢复刚结束但读到旧数据」,应区分:RV 回滚与 apply 延迟——前者由恢复事务界定,后者是 第 17 篇 的 Storage 落后类问题。
5.4 客户端在窗口内应遵守的契约
结合 第 2 篇 的 API 契约,恢复窗口内的安全默认是:
- 把提交失败当未提交,除非客户端已收到明确的 commit 成功;
- 依赖官方重试循环,而不是在应用层对同一业务键做「补偿双写」;
- 区分
transaction_too_old(窗口/时长)与恢复导致的临时不可用——后者应在世代稳定后收敛,前者要改事务切分。
确定性模拟(第 14 篇)把「恢复中插入提交与重试」当成一等 workload,正是因为生产客户端几乎总会在窗口内重试。
六、学术谱系、工程间隙与开放问题
6.1 谱系
| Work | 会议·年份 | 与本篇关系 |
|---|---|---|
| Mohan et al., ARIES | TODS 1992 | 传统 WAL + checkpoint 恢复;FDB 明确对照并拒绝在恢复时同步重放 redo |
| Helland et al. / Deuteronomy 等 unbundled 思路 | CIDR 等 | 事务管理与存储解耦;FDB 把 MVCC 放在 Storage 侧而非 TS |
| Zhou et al., FoundationDB | SIGMOD 2021 | 本篇主锚:epoch、RV/PEV、fail-fast recovery |
| Ongaro & Ousterhout, Raft | USENIX ATC 2014 | Coordinator 用 disk Paxos;本系列不把 Raft 选举细节搬进 FDB 恢复 |
6.2 工程间隙
论文实验与叙述假设:恢复路径被确定性模拟充分覆盖(见 第
14 篇),且生产 MTTR
以作者集群为样本。公开资料没有给出「任意硬件
+ 任意复制配置」下可外推的恢复 SLA
公式。运维侧能核对的是:fdbcli status
是否显示恢复中、角色是否重新招募、客户端重试是否收敛——而不是对照一张通用秒表。
6.3 开放问题
- 恢复窗口内的读服务等级如何对外契约化? 论文描述机制边界,但各厂商托管产品对「RPO/RTO / 读是否只读旧版本」的对外表述并不统一。
- Copyset 与分层故障域降低同时故障丢数据概率,但与「简单 \(k\) 副本」在运维复杂度上的长期权衡,缺少独立受控对比研究。
- 多区域卫星配置下自动/手动 failover 的一致性—延迟谱(论文 §3)仍是部署选择题,不是单一最优解。
七、常见误解
误解一:Sequencer
是单点,挂了就永久不可用。
Sequencer 是逻辑单例角色,失败会触发
ClusterController 招募新实例并开新
epoch;高可用来自快速恢复与 Coordinator
仲裁,而不是「同一进程永不挂」。
误解二:恢复要像传统 RDBMS 一样从 checkpoint
重放全部 redo。
FDB 恢复只定位 RV;Storage
的拉日志与日常前向路径相同,不在恢复临界路径上同步重放整段历史。
误解三:恢复期间整个集群完全不可读。
写路径会停;读是否可用取决于是否还要拿新 read version,以及
Storage
上已有版本。把「提交不可用」等同于「所有读失败」会误判故障面。
八、小结
三句话小结:
- FoundationDB 把事务系统故障收敛到「停当前世代 → 新 Sequencer 锁定 Coordinator → 停旧 TLog → 招募新角色 → 算 RV/PEV → 开新 epoch」同一条路径。
- Proxy/Resolver 无状态易替;LogServer 恢复的核心是用 KCV/DV 界定已确认提交与可丢弃后缀,并用特殊恢复事务隔离 Storage 内存中高于 RV 的数据。
- 写暂停是恢复窗口的硬边界;读边界取决于版本获取与 Storage 可见性,MTTR 数字只引用论文部署观测,不外推为通用 SLA。
参考资料
核心论文
- Zhou J., et al. FoundationDB: A Distributed Unbundled Transactional Key Value Store. SIGMOD 2021. §2.4.4 Recovery;§2.5 Replication;§5.3 恢复耗时观测。
- Mohan C., et al. ARIES: A Transaction Recovery Method Supporting Fine-Granularity Locking and Partial Rollbacks Using Write-Ahead Logging. ACM TODS 1992.(对照谱系)
官方文档 / 源码
- FoundationDB 7.x Architecture、Administration(Coordinator、角色与状态)。
- FoundationDB 源码树
fdbserver/(恢复与角色招募相关 actor;版本以所用 tag 为准)。
多区域卫星与 failover 细节以论文 §3 与当前版本文档为准,本篇为控制篇幅只给单集群事务系统恢复主线。
站内联动
上一篇:SSD Engine 与
Redwood
下一篇:确定性模拟:FDB
的工程落点
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【FoundationDB 内核】Coordinator 与集群配置:Paxos 边界、招募与恢复入口
说明 Coordinators 如何用磁盘 Paxos 持久化事务系统配置、Cluster Controller 如何招募角色,并划清『配置共识』与『用户 mutation 共识』的边界。
【FoundationDB 内核】Sequencer:read/commit version、逻辑单点与恢复
说明 Sequencer 如何分配 read version 与 commit version、为何是逻辑单点而非无高可用,以及世代恢复如何与版本推进衔接。
【FoundationDB 内核】Unbundled 全景:事务、日志与存储为何拆开
以 SIGMOD 2021 为锚点,厘清 FoundationDB 控制面与数据面角色分工,说明一次事务穿过 Proxy、Sequencer、Resolver、TLog、Storage 的路径,并与站内 distributed/39、tikv-htap 划清边界。
【FoundationDB 内核】角色拓扑与数据流:读写总图与扩展边界
用一张读写总图串联 FoundationDB 各角色,标明哪些可水平扩展、哪些是逻辑单例,并与 TiKV 多组件分离作机制对照。