客户端几乎从不直连 Sequencer。拿读版本找 Grv Proxy,提交找 Commit Proxy——在 SIGMOD 2021 论文里二者还常合称 Proxies;FoundationDB 7.x 官方架构文档已拆成两个可独立扩展的角色。把它们当成「无状态 RPC 网关」不算错,但会漏掉两件关键工作:read version 的安全含义,以及 提交三步编排(要版本 → Resolver → TLog)。
本文是系列第 4 篇,只钉 Proxy 层:分工、发版本、提交入口、批处理与 Ratekeeper 的交互。Sequencer 如何选号见第 6 篇;Resolver / TLog 细节见第 8、10 篇;端到端写流水线见第 7 篇。
本文是「FoundationDB 内核」系列第 4 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 3 篇 · Coordinator 配置与招募 第 4 篇 · Proxy Grv / Commit、read version、批处理 第 5 篇 · 角色拓扑 读写总图
版本锚定:FoundationDB 7.x Architecture(GRV Proxies、Commit Proxies、Determine Read Version、Transaction Commit);Zhou et al., SIGMOD 2021 §2.3.2、§2.4.1(Proxies 职责与提交流程)。命名:正文用 Grv Proxy / Commit Proxy;引用论文时保留 Proxies 统称。
一、为什么拆成两个 Proxy
官方 Architecture 把读版本发放与提交编排拆开,缩放轴不同:
| 角色 | 客户端主要用途 | 与 Sequencer 的关系 | 状态 |
|---|---|---|---|
| Grv Proxy | getReadVersion |
询问当前最大已提交版本等 | 无持久状态;挂在一代事务系统上 |
| Commit Proxy | commit |
申请 commit version;汇报已提交版本 | 无持久用户状态;持有内存中的 shard 映射等 |
论文时代的描述是「Proxies offer MVCC read versions … and orchestrate transaction commits」(§2.3.2)。7.x 拆分后,读多写少的负载可以多布 Grv Proxy,而不把提交路径的 CPU 与连接数绑在同一进程画像上。
Cluster Controller 通过 Coordinators 路径让客户端刷新 Proxy 列表(第 3 篇)。Proxy 随事务系统世代替换:旧 Proxy 在恢复后不应再安全地发放「新鲜」读版本——下一节的日志存活检查就是为此。
flowchart LR
client["Client"]
grv["Grv Proxy"]
commit["Commit Proxy"]
seq["Sequencer"]
res["Resolver"]
tlog["Log Servers"]
ss["Storage Servers"]
client -->|"get read version"| grv
grv --> seq
client -->|"reads"| ss
client -->|"commit"| commit
commit --> seq
commit --> res
commit --> tlog
二、Grv Proxy:read version 不是随便取的整数
官方 Determine Read Version 一节给出 Grv Proxy 发版本时的要点:
- 向 Master/Sequencer 询问当前最大已提交版本(Master 汇总各 Commit Proxy 的 committed version)。
- 同时检查满足复制策略的一组 Transaction Log 仍存活(未被停止)。
- 将得到的版本作为 read version 返回客户端。
- Ratekeeper 可通过压低发版本速率做过载保护。
第二条容易被忽略。Grv Proxy 是无状态、按世代招募的。若恢复已发生、旧 Grv Proxy 仍存活,它可能继续发版本,导致只读事务看到陈旧世界;带写的事务稍后多半会 abort,但只读路径更危险。通过确认「本世代日志集仍有效」,Proxy 把「我是否已被新世代取代」绑到日志系统生命期上。
官方还解释了为何客户端不能人人直连 Sequencer 要版本:Sequencer/Master 难以水平扩展,直连会把它变成连接与批处理热点;Grv Proxy 负责汇聚、批处理与执行 Ratekeeper 预算。
sequenceDiagram
participant Client
participant Grv as Grv Proxy
participant Seq as Sequencer
participant TLog as Log Servers
Client->>Grv: request read version
par Version and liveness
Grv->>Seq: latest committed version
Seq-->>Grv: version V
Grv->>TLog: check logs still live
TLog-->>Grv: ok / failed
end
Note over Grv: Ratekeeper may delay / throttle
Grv-->>Client: read version V
2.1 客户端拿到版本之后
客户端用该版本向 Storage Server 读(第 11 篇)。写入仍留在客户端缓冲(第 2 篇)。Grv Proxy 不代理普通用户读流量——这是 Unbundled 读扩展的关键:读路径不经过提交管线(论文 §2.3.3)。
三、Commit Proxy:提交三步
官方 Transaction Commit 与论文 §2.4.1 一致,Commit Proxy 对一笔事务:
- 向 Sequencer 申请 commit version(严格大于已知读/提交版本;论文称 Sequencer 以约每秒一百万版本的速率推进——这是版本空间设计,不是本站实测吞吐)。
- 把读/写冲突范围送给 Resolver(们);按 commit version 排序做 OCC。任一 Resolver 冲突则整笔失败,向客户端返回未提交错误。
- 无冲突则把 mutation 写入 Log Servers;待复制策略要求的日志 durable 后,向客户端返回成功,并向 Sequencer/Master 汇报 committed version,以便后续 read version 赶上。
sequenceDiagram
participant Client
participant CP as Commit Proxy
participant Seq as Sequencer
participant Res as Resolver
participant TLog as Log Server
Client->>CP: commit(read/write sets)
CP->>Seq: allocate commit version
Seq-->>CP: commit version + prev
CP->>Res: conflict check
alt conflict
Res-->>CP: abort
CP-->>Client: not_committed
else no conflict
Res-->>CP: admit
CP->>TLog: persist mutations
TLog-->>CP: durable
CP->>Seq: report committed version
CP-->>Client: success
end
3.1 系统键与路由缓存
\xff 开头的系统元数据变更会经 Resolver
分发给各 Commit Proxy(官方 Architecture)。其中包含
key range → Storage Server 映射。客户端向
Commit Proxy 按需拉取并缓存;若打到错误的 Storage
Server,清空缓存再问 Proxy。Put 路径上 Proxy 也用内存 shard
map 决定 mutation 打到哪些 Log 标签(论文 Figure 2,细节第
7、10 篇)。
3.2 空提交与批处理
官方 Background Work:Commit Proxy 会周期性向 Transaction Log 发送空提交,以在客户端安静时仍推进 commit version。生产实现里,对 Sequencer 的版本请求与对 Resolver / TLog 的调用都会批处理——论文明确指出先定 commit version 再冲突检测,便于批量分配版本与批量检测(§2.4.2)。
工程含义:Proxy 是延迟与吞吐的汇聚点。多 Proxy 分摊客户端连接;但每一笔仍依赖 Sequencer 的版本权威(第 6 篇)和 Resolver 的窗口状态(第 8 篇)。
四、Ratekeeper:踩刹车踩在发版本上
Ratekeeper 由 Cluster Controller 招募(第 3 篇),从 Grv / Commit Proxy、Log、Storage 收集负载,计算目标事务速率(官方 Architecture;论文 §2.3.1)。
过载时主要杠杆是:降低 Grv Proxy 发放 read version 的速率。没有新的 read version,新事务进不了读/写循环,比在存储层随机拒绝更可控。这是系统级反压,不是应用层 HTTP 429 的简单翻译。
flowchart TB
rk["Ratekeeper"]
grv["Grv Proxies"]
cp["Commit Proxies"]
tlog["Log Servers"]
ss["Storage Servers"]
grv -->|"stats"| rk
cp -->|"stats"| rk
tlog -->|"stats"| rk
ss -->|"stats"| rk
rk -->|"throttle read version rate"| grv
常见误解
- 「Proxy 就是 TiDB / SQL 层」。Proxy 不懂表与 SQL;Record Layer 等在客户端进程里,经同一 KV 事务 API 访问集群(第 15 篇)。
- 「读也必须过 Commit Proxy」。普通读过 Storage;Commit Proxy 参与提交与元数据/路由,不承担全量读带宽。
- 「Grv Proxy 返回的版本保证 Storage 上立刻有该版本的全部数据」。版本保证的是序列与可见性契约;Storage 异步 apply 可能有短暂滞后,客户端可读其他副本或重试(论文 §2.4.3)。机制见第 10–11 篇。
工程间隙
论文微基准提到单线程 Resolver 的冲突检测能力(§2.4.2);那是 Resolver 侧数字,且依赖特定假事务形态。Proxy 侧真实延迟取决于批大小、日志 fsync、跨 Resolver 分片与恢复频率。本站无实测则不给出「Proxy P99」类结论;排障时把 Proxy 与 TLog、Storage 落后分开看(第 17 篇)。
五、批处理:为什么先定版本再检测冲突
论文 §2.4.2 对比一类「先冲突检测再分配时间戳」的写法(如某些 write-snapshot isolation 变体):FDB 先向 Sequencer 要 commit version,再做 Resolver 检测。好处是版本分配与冲突检测都可以按批切割;代价是已分配版本的事务若最终 abort,仍可能在部分 Resolver 上留下写历史,造成短窗口内的假阳性冲突(论文亦承认,并指出生产低冲突与 5 秒过期会限制影响)。
对 Proxy 实现者/排障者:
- 批太大:单批尾延迟上升,客户端 P99 变差。
- 批太小:Sequencer / Resolver RPC 次数上升,CPU 与连接开销变差。
- 跨多个 Resolver 的事务:任一否决即整笔失败;假阳性放大在「写集跨越多个 Resolver 分片」时更明显。
flowchart LR
batch["Commit Proxy batch"]
ver["Allocate commit versions<br/>from Sequencer"]
occ["Parallel Resolver checks"]
log["Persist winners to TLog"]
batch --> ver --> occ --> log
这不是让你在业务里「调 Proxy 批大小」——多数部署用默认;而是理解:提交路径的延迟结构是批处理队列,而不是逐笔锁等待。
5.1 客户端该打哪个 Proxy
客户端从 Cluster Controller 拿到 Proxy 列表后,通常在列表上做选择/负载分担(具体策略随 binding 版本演进,以官方客户端行为为准)。要点是:
- Grv 与 Commit 列表分离后,读版本与提交可以打到不同进程。
- 列表陈旧时,请求失败或经恢复错误回到
on_error重试,并刷新拓扑。 - 不要在应用层缓存「某一个 Proxy IP 永久不变」——世代切换会换人。
5.2 一篇里能记住的延迟分解
把一笔写提交的客户端可见延迟粗拆为(不含 Storage apply):
| 段 | 组件 | 主导成本 |
|---|---|---|
| A | Grv Proxy ↔︎ Sequencer(若本笔需要新读版本) | 批处理排队、Ratekeeper |
| B | Commit Proxy ↔︎ Sequencer | 版本批分配 |
| C | Commit Proxy ↔︎ Resolver(s) | OCC CPU、跨 Resolver 协调 |
| D | Commit Proxy ↔︎ Log Server | 复制 + fsync |
业务「commit 慢」要先分清卡在 B/C/D 哪一段:C 高往往是冲突或 Resolver 过载;D 高是日志盘或复制;B 高才更像 Sequencer/批处理问题。第 7、10、17 篇会回到这张表。
flowchart LR
A["A: read version"] --> B["B: commit version"]
B --> C["C: resolve"]
C --> D["D: log durable"]
D --> E["Client success"]
六、小结
三句话小结
- 7.x 将 Proxy 拆成 Grv(发 read version + 日志存活检查 + Ratekeeper 节流)与 Commit(要 commit version、跑 Resolver、写 TLog)。
- read version 必须赶上已知提交,且发放者须仍属于当前日志世代,否则只读可能陈旧。
- Commit Proxy 是写管线编排器而非存储引擎;批处理与空提交服务于版本推进与吞吐,细节流水线见第 7 篇。
下一篇把角色收成一张读写总图:角色拓扑与数据流。
参考资料
核心论文
- Zhou et al., SIGMOD 2021, §2.3.2–2.3.3、§2.4.1–2.4.2(Proxies、读写分离扩展、提交三步、批处理动机)。
规范 / 文档
- FoundationDB Architecture — GRV Proxies、Commit Proxies、Determine Read Version、Transaction Commit、Ratekeeper(7.x)。
站内
下一篇:角色拓扑与数据流
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【FoundationDB 内核】Sequencer:read/commit version、逻辑单点与恢复
说明 Sequencer 如何分配 read version 与 commit version、为何是逻辑单点而非无高可用,以及世代恢复如何与版本推进衔接。
【FoundationDB 内核】写事务流水线:Client → Commit Proxy → Sequencer → Resolver → TLog
钉住 FoundationDB 一次写事务的提交路径:Client 经 Commit Proxy 取 commit version、Resolver 做 OCC、TLog 多数持久化后才向客户端返回成功;并划清「提交成功」与「Storage Server 可读」之间的边界。
【FoundationDB 内核】运维与容量:角色资源画像
按 Unbundled 角色给出可观测的资源画像、分片热点成因与备份/DR 边界;只引用官方 Administration / Backup 文档与 SIGMOD 2021 机制,不写无来源容量公式。
【FoundationDB 内核】Unbundled 全景:事务、日志与存储为何拆开
以 SIGMOD 2021 为锚点,厘清 FoundationDB 控制面与数据面角色分工,说明一次事务穿过 Proxy、Sequencer、Resolver、TLog、Storage 的路径,并与站内 distributed/39、tikv-htap 划清边界。