土法炼钢兴趣小组的算法知识备份

【FoundationDB 内核】Storage Server:Shard、版本读取与读路径

文章导航

分类入口
databasestorage
标签入口
#foundationdb#storage-server#shard#mvcc#read-path#data-distributor#tlog#sigmod2021

目录

第 10 篇 把耐久性留在 TLog;本篇看数据平面的读服务者。问题:客户端读为什么可以绕开 Proxy→Resolver→TLog,Storage 又如何在异步 apply 的前提下提供按版本的一致快照

SIGMOD 2021 §2.3.2:StorageServers 构成分布式 B-Tree 的主体,每个托管一组连续 key range(shard);集群中多数进程是 Storage。官方 Architecture:客户端直接向持有该 range 的 Storage 发读;Commit Proxy 提供 range→server 映射供客户端缓存。

本文是「FoundationDB 内核」系列第 11 篇(共 18 篇)。上一篇Transaction Log下一篇SSD Engine 与 Redwood→ 系列目录

版本锚定:FoundationDB 7.x。引擎实现(SQLite 派生 SSD / Redwood)细节放第 12 篇;本篇聚焦角色职责与数据路径。


一、Shard 与团队复制

1.1 数据单位

论文用语:shard = contiguous key ranges。每个 shard 异步复制到 \(k=f+1\) 个 StorageServer,构成 team。一个 Storage 进程通常托管许多 shard,以便在多个 team 间打散负载与故障域(§2.5)。

与 TiKV Region 的对照(机制,不比性能):两者都是 range 分片;TiKV 用 Multi-Raft 做复制与提交,FDB Storage 复制是异步的,提交已在 TLog 完成。因此「某个 Storage 落后」首先是读新鲜度 / 重试问题,不必等于「写无法提交」——除非落后演变为集群限流或迁移风暴(第 16–17 篇)。

1.2 映射如何到达客户端

系统元数据(\xff 前缀)在提交路径上经 Resolver 扩散到 Commit Proxy(Architecture)。客户端缓存 range 定位;若问错 Storage,会清空缓存并向 Proxy 拉取更新列表。这避免每次读都打 Proxy,又允许拓扑变化。

flowchart TD
  Client["Client"] -->|"get range map (cached)"| CP["Commit Proxy"]
  Client -->|"versioned read"| SS1["Storage Server A"]
  Client -->|"versioned read"| SS2["Storage Server B"]
  TLog["TLog"] -.->|"async mutations"| SS1
  TLog -.->|"async mutations"| SS2

1.3 与「主从可读」直觉的差别

许多系统让客户端「读 Leader」以换取新鲜度。FDB Storage 的多个副本都可以服务读,新鲜度由「该副本是否已应用到 read version」决定,而不是由「是不是 Raft Leader」决定——因为根本没有 per-shard Raft 提交。副本之间允许短暂版本差;客户端库负责等待或换副本。这把负载分散到 team 内多个进程,也把「读到旧快照」的风险关进版本与重试协议,而不是关进单一主副本。


二、版本读取:5 秒 MVCC 窗口

Architecture Storage Servers

SIGMOD 2021 补充:当时主引擎是改过的 SQLite B-Tree——未版本化的磁盘结构 + 内存中的多版本 redo;只有离开 MVCC 窗口的已提交数据写入 SQLite。恢复时回滚主要是丢弃内存多版本(§2.4.4)。Redwood 把多版本推进引擎内部(第 12 篇),但客户端可见的 5 秒契约仍由事务系统窗口约束(Known Limitations)。

读请求携带 read version \(r\)。Storage 返回在 \(r\) 下可见的值。若该副本尚未应用到 \(r\),论文描述客户端可等待、改打另一副本,或超时后重试事务。

sequenceDiagram
  participant C as Client
  participant SS as StorageServer
  participant L as TLog

  C->>C: GRV -> read version r
  L-->>SS: mutations ... r ...
  C->>SS: get(key) at r
  alt version ready
    SS-->>C: value
  else not yet applied
    C->>SS: retry / other replica
  end

三、拉日志:正常路径与恢复路径

3.1 正常路径

Background Work:Storage 从 transaction logs 拉 mutation,写入存储引擎。Architecture 还提到:数据在日志落盘前就可转发到负责的 Storage,以降低多版本读等待。Storage 本地 durable 后从 TLog pop。

论文测量某生产集群上 Storage 相对 Log 的平均与尾部滞后(Figure 3),用以说明「通常很小」——那是特定集群观测,本系列不把它写成你的 SLA。机制结论只保留:设计意图是滞后远小于 5 秒窗口,否则严格可串行化读会大量重试。

3.2 恢复路径

世代切换后,Storage 被告知 RV,丢弃 \(>\) RV 的内存状态,再从新 TLog 拉 \(>\) 某起点的数据(论文 §2.4.4)。因此 Storage 必须能容忍「应用了后来被宣布无效的半提交前缀」——这是积极拉取的代价,由恢复协议收口。


3.3 读放大与范围扫的责任边界

Storage 对范围读返回 read version 下可见的键值序列。大范围扫仍受 5 秒窗口与事务大小限制约束:把「一次性扫全表」塞进单事务会同时触碰时间上限与 10 MB 受影响数据上限(Known Limitations)。官方建议用 limit / 迭代器分段读取。分段之后,严格可串行化不再自动覆盖「整个逻辑大扫」——那是应用用多个短事务拼出来的较弱协议,需要 Layer 自己维持不变量(第 15 篇)。

热点小 key 的读聚合上限见 Known Limitations:均衡只在现有副本间发生,不会自动加副本。应用若把全局计数器做成单 key,读路径会先于写路径触顶。

四、迁移与 Data Distributor

Cluster Controller 招募 Data Distributor 单例(Architecture;自 6.2 起生命周期不再绑在 Master 同进程的历史耦合上)。职责:

Storage 失败时,Distributor 把受影响 team 上的数据迁到健康 team(论文 §2.5)。TLog 上发往失败 Storage 的堆积在责任迁移完成后丢弃(Architecture)。层次化 team 放置还要满足机架 / 可用区等故障域约束,避免「任意 \(k\) 进程同挂即丢数据」的组合爆炸(论文对比 Copyset 动机并给出分层策略)。

本篇不展开调度算法细节;运维侧热点与容量见 第 16 篇

迁移期间,同一 key range 的「责任」在切换,客户端可能短暂打到旧定位并触发刷新。写路径仍以 TLog 为准:迁移拷贝的是 Storage 上的物化状态与追平,不把「迁移完成」定义成新的提交点。理解这一点可避免在运维窗口误判「双写」或「写丢失」。

flowchart LR
  DD["Data Distributor"] -->|"reassign shards"| SSHealthy["Healthy Storage"]
  SSFail["Failed Storage"] -.->|"teams dissolved"| DD
  SSHealthy -->|"catch up from TLog / peers"| Ready["Serve reads"]

五、读路径为何不经过提交流水线

这是 Unbundled 的可读性结论,值得单独钉死:

步骤 写事务 读(默认)
取版本 提交时向 Sequencer 要 commit version 开始时向 GRV Proxy 要 read version
冲突检测 Resolver 无(只读可本地结束)
耐久性 TLog 不写
数据平面 不经 Storage 返回成功 只经 Storage

因此:

Ratekeeper 通过降低 GRV 发放速率做过载保护——读版本获取变慢,从而间接限制新事务进入,而不是在 Storage 上加一把全局读锁。


5.1 一张责任表

组件 对读的责任 不对读承担的责任
GRV Proxy 发放合法 read version 返回 key 的值
Commit Proxy 提供 range 定位元数据 执行点读/范围读
Resolver 无(读不经此)
TLog 间接:供 Storage 追平 服务客户端读
Storage Server 按版本返回数据 分配全局版本
Data Distributor 间接:布局健康 参与每次读 RPC

把这张表贴进排障手册,可以减少「读变慢就去重启 Sequencer」一类误操作。读延迟首先查 Storage 与定位缓存,其次才查 GRV 是否被 Ratekeeper 限速。

六、读路径上的客户端义务

一次成功的严格可串行化读,在 Storage 侧只是「按版本返回」;客户端仍须:

  1. 从 GRV Proxy 取得合格的 read version(含日志存活检查,见第 9 篇);
  2. 使用缓存的 range 定位,并在 wrong_shard / 类似重定位错误时刷新;
  3. 在约 5 秒内完成需要访问数据库的读,或接受 transaction_too_old
  4. 在副本尚未应用到 \(r\) 时重试或换副本,而不是假定「提交成功 ⇒ 任意副本瞬时可读」。

这些义务把 Unbundled 的性能优势(读不经 Resolver)与一致性负担一起交给客户端库——语言绑定通常封装了重试,但 Layer 作者仍需理解错误类,避免把定位错误当成业务失败。

系统 key(\xff 前缀)上的元数据变更会经提交路径扩散到 Proxy,从而更新定位表。普通用户数据的迁移则由 Data Distributor 驱动 Storage 之间的拷贝与责任切换;客户端只看到定位变化,不应自己实现「手工迁 shard」。


与 TiKV Region 调度的另一处差别:FDB 的 Data Distributor 不负责「为写提供 Raft 法定人数」,只负责数据放置与均衡。写法定在 TLog 世代内已经满足。因此「调度器卡住」更常表现为不均衡、迁移缓慢或读热点,而不直接等价于「整个集群无法 commit」——除非限流或磁盘被迁移打满。排障时先问 TLog/Proxy 世代是否健康,再问 Distributor,顺序反了会浪费时间。

七、学术谱系、工程间隙与开放问题

谱系:把「事务执行」与「数据服务」拆开的系统(Deuteronomy 等)常争论 MVCC 状态放在哪一侧。FDB 明确把 MVCC 放在 SS,使 TS 无状态、易恢复;代价是 SS 必须理解版本与短窗口。这与「事务层持有所有版本、存储只认最新」的拆法相反。

工程间隙:Known Limitations 写明:读负载均衡会在持有副本的服务器间进行,但不会自动提高热点小 key 的复制因子;极端热点读受限于持有该 key 的少数 Storage 进程聚合能力。workaround 是应用层多 subspace 复制。这是架构诚实边界,不是调一个旋钮就消失。

开放问题:在 Redwood 持久化更多历史版本以支持更长只读事务的方向上(引擎动机之一,见第 12 篇),系统级 5 秒契约与引擎能力如何重新对齐,仍属产品演进问题;以所用 7.x 文档的 Limitations 为准,不把论坛愿景写成已交付保证。


八、常见误解

误解 事实
「读也要走 Commit Proxy」 数据读直达 Storage;Proxy 主要提供定位与提交
「Storage 落后则集群不能提交」 提交在 TLog;落后影响读与堆积,严重时经 Ratekeeper 间接限流
「三副本 Storage 等于 Raft 已提交」 Storage 复制异步;已提交语义在 TLog
「可以读任意旧版本」 默认约 5 秒窗口,超出 transaction_too_old
「Data Distributor 参与每次读」 Distributor 管布局;读路径不经它
「刷新定位表需要停写」 客户端按需拉 Proxy;与写暂停无必然绑定

九、小结

三句话小结

  1. Storage Server 按 shard/team 保存数据,按 read version 服务快照读,并从 TLog 异步应用 mutation。
  2. 读路径绕开提交流水线,使读扩展与写耐久扩展可以分开规划;版本仍来自 GRV/Master 叙事。
  3. 迁移由 Data Distributor 驱动;引擎换 Redwood 不改变「读不经 Resolver」这一角色边界。

下一篇进入 SSD Engine 与 Redwood:从 SQLite 派生引擎到版本化 B-Tree 的谱系与系统边界。


上一篇Transaction Log

下一篇SSD Engine 与 Redwood

返回 系列目录


备份与灾难恢复若依赖 Storage 文件拷贝,必须理解拷贝点与 TLog 提交点的关系:只拷 Storage 快照而不协调日志,可能丢最近窗口内已提交更新。具体备份产品路径见第 16 篇边界,本篇只提醒读路径角色不是备份真相来源。

文末核对提示

本节补记:机制结论以 SIGMOD 2021 与 FoundationDB 7.x 官方 Architecture / Developer Guide / Known Limitations 为准;若源码字段名随小版本重命名,以对应 tag 的 fdbserver 实现为准,不把过时幻灯片当契约。

阅读本篇时建议同时打开系列 index 中的依赖图:本篇上承写路径与版本分配,下接恢复与排障;不要孤立地把单角色文档当成完整正确性证明。

本节补记:机制结论以 SIGMOD 2021 与 FoundationDB 7.x 官方 Architecture / Developer Guide / Known Limitations 为准;若源码字段名随小版本重命名,以对应 tag 的 fdbserver 实现为准,不把过时幻灯片当契约。

阅读本篇时建议同时打开系列 index 中的依赖图:本篇上承写路径与版本分配,下接恢复与排障;不要孤立地把单角色文档当成完整正确性证明。

站内对读时请同时打开 第 7 篇写流水线第 9 篇严格可串行化,以免把日志或存储角色误当成隔离机制本身。

参考资料

论文(A 级)

  1. Zhou et al. FoundationDB…. SIGMOD 2021(§2.3.2 SS、§2.4.3–2.4.4 拉日志与回滚、§2.5 team 复制)。

官方文档(A 级)

  1. FoundationDB 7.x, Architecture — Storage Servers、Data Distributor、Clients、Background Work。
  2. FoundationDB 7.x, Known Limitations — transaction_too_old、有限读负载均衡。

站内对读

  1. 第 10 篇 TLog第 12 篇 Redwood
  2. tikv-htap 第 3 篇 Region(range 分片对照)。
  3. rocksdb 系列(LSM 对照放到第 12 篇)。

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .