Skip to content

Lec 13 乐观并发控制:FaRM

阅读材料

  • 论文:Dragojević et al., No compromises: distributed transactions with consistency, availability, and performance (FaRM, SOSP 2015)
  • 课程讲义:6.5840 l-farm.txtfarm-faq.txt

一句话总览(TL;DR):FaRM(Fast Remote Memory)追求单数据中心内每秒数百万笔分布式事务、单笔延迟仅几十微秒。它的做法是同时榨干三个瓶颈——用 NVRAM 消灭磁盘写、用 单边 RDMA + 内核旁路 消灭网络/CPU 开销、用 乐观并发控制(Optimistic Concurrency Control, OCC 让读操作无需服务器 CPU 参与。OCC 与单边 RDMA 是天作之合:读不加锁,提交时再"验证"是否有冲突。

NOTE

FaRM 目前仍是微软研究院的研究原型,未投生产,但其理念可能影响未来架构。顺带回答你笔记里的问题——公司为何公开前沿论文而非保密:① 核心团队多有学术背景(博士),视传播创新为使命、希望获业界认可;② 论文能展示"这里在做有趣的智力工作",从而吸引顶尖人才;③ 借技术影响力建立行业标准。当然,公司只会公开自己软件中极小的一部分。


1. 背景与目标

我们读 FaRM,是为了看"事务 + 复制 + 分片"的又一种取法——这仍是开放的研究领域。它的两个独特之处:乐观并发控制,以及对 RDMA 网卡巨大性能潜力的压榨

整体设定:全部在单数据中心内;用 ZooKeeper 实现的配置管理器(Configuration Manager, CM 选定各分片的主/备;数据按分片做主备复制(P1/B1P2/B2…),只要每个分片至少存活一个副本即可恢复(即 f+1 副本容 f 故障)。事务客户端跑在服务器上,其事务代码充当 两阶段提交(2PC事务协调者(Transaction Coordinator, TC

IMPORTANT

目标是百万级分布式事务/秒,于是单笔时间预算只有几十微秒——这个量级下,任何一次磁盘写(SSD ~100µs)、一次传统 RPC(~10µs)、一次系统调用/中断/上下文切换都贵得离谱。FaRM 的全部设计都是在和"微秒"赛跑。


2. 三大性能武器

高性能来自分片到很多台机器(评测用 90 台),外加下面三件事同时做对。

2.1 数据全装进 RAM(消灭磁盘读)

数据集必须能装进所有机器 RAM 的总和,因此读永远不碰磁盘。

2.2 NVRAM:非易失内存(消灭磁盘写)

📘 定义:NVRAM(Non-Volatile RAM)

写只落到 RAM(~200ns)而非磁盘(HDD ~10ms / SSD ~100µs),消灭持久化写这个巨大瓶颈。但 RAM 断电即失,靠下述机制变"非易失":

为什么不能简单地"写到 f+1 台机器的 RAM"就算持久?因为断电故障并不独立——它可能同时击中 100% 的机器。FaRM 的做法:

text
每个机架配电池(UPS),可撑机器运行几分钟
  → 电源硬件在主电源失效时通知软件
  → 软件停止所有事务处理
  → 软件把 FaRM 的 RAM 内容写入 SSD(可能花几分钟)
  → 机器关机
重启后:FaRM 从 SSD 读回内存镜像 —— 这就是"非易失 RAM"

💁🏼‍♀️提示

若崩溃不是断电(FaRM/内核 bug、CPU/内存硬件错误)导致软件来不及写 SSD 怎么办?——这类单机崩溃靠复制兜底。前提假设:除断电外的崩溃必须相互独立。所以 DRAM 严格说并非完全非易失,正因如此才要把每个 region 复制到多台机器并配快速恢复协议。(用 SSD 而非机械盘是为了快:HDD 会让断电时写盘和恢复时读回都慢 ~10×,需要更大电池和更多耐心。)

NVRAM 消灭了持久化写瓶颈,只剩网络CPU 两个瓶颈。

2.3 单边 RDMA + 内核旁路(消灭网络/CPU 开销)

为什么网络常是瓶颈?单 DC 内光速延迟很低,但网络数据处理的 CPU 成本很高。传统 RPC over TCP over LAN 要经过:系统调用 → 拷贝消息 → 中断 → 上下文切换……每包 CPU 成本让小消息 RPC 很难超过每秒几十万次,而线速(如 10 Gbps)通常并非瓶颈。FaRM 用两招破局:

📘 定义:内核旁路(Kernel Bypass)与单边 RDMA(One-sided RDMA)

内核旁路:应用直接和网卡(NIC)交互,无系统调用、无内核;NIC 直接 DMA 进出用户态 RAM;FaRM 软件轮询(polling) DMA 区域收发消息(专门钉住 CPU 核做轮询,省掉中断/系统调用/用户-内核拷贝/上下文切换)。

单边 RDMA:一台机器的应用借 RDMA 网卡直接读写另一台机器的内存,完全不惊动对方 CPU;按缓存行原子读取。一台服务器吞吐 10M+ ops/s,延迟 ~5µs;单次单边读/写最快可低至 1/18 µs。

NOTE

FaRM 也会用 RDMA 来实现一种"类 RPC":发送方用 RDMA 把请求写入接收方正在轮询的内存区,接收方同样方式回复——这比传统 RPC 快得多(无中断、无用户/内核切换)。FaRM 的 LOCK 阶段就是这样用 RDMA 的;而 VALIDATE 阶段用的是纯单边读


3. 为什么是乐观并发控制(OCC)?

并发控制两大流派:

悲观(两阶段锁 2PL)乐观(OCC)
首次使用即加锁,持有到提交/中止不加锁直接读
就地修改提交前不安装,本地缓写
冲突造成等待/延迟提交时验证:无冲突则提交,有冲突则中止重试
✅ 关键结论:FaRM 选 OCC 的根本原因

OCC 的"读不加锁"让 FaRM 可以用单边 RDMA 读取数据——服务器 CPU 完全不必参与读。而此前我们见过的协议(查/置锁、查租约、确认已持久化)都要求服务器主动参与,与单边 RDMA 不兼容。"乐观"赌的是冲突很少:若冲突频繁,OCC 会产生大量中止,性能反而崩。


4. 数据模型与内存布局

简化事务 API

text
txCreate()
o = txRead(oid)     // 单边 RDMA 读
o.f += 1
txWrite(oid, o)     // 纯本地操作(缓写)
ok = txCommit()     // 走 Figure 4 提交协议

oid = <region#, address>region# 索引到一张 [primary, backup1, ...] 映射;address 被目标 RDMA 网卡直接用于读 RAM。

服务器内存布局:若干 region,每个是对象数组;对象头含版本号(version#),锁标志位占版本号的最高位;并为每个其他服务器维护"接入日志(incoming log)"与"接入消息队列"(发送方用 RDMA 写入、本地 FaRM 轮询读取)。这一切都在 NVRAM 里。


5. Figure 4:事务执行与提交协议

聚焦并发控制(暂略容错细节)。下面是简化的消息流(以 f=1、写 1 个主为例,共 4 条消息):

text
        Execute(无锁读)                   Commit
TC   ──RDMA读──► [Primary]        ──LOCK──► ──VALIDATE──► ──COMMIT-BACKUP──► ──COMMIT-PRIMARY──►
     (记录version#,本地缓写)        到主日志    单边重读只读对象   到备日志(等硬件ack)   到主日志(等硬件ack后返回"yes")
                                   ↓回yes/no
Primary 处理 COMMIT-PRIMARY: 写入新值 → version#++ → 清锁标志

Execute 阶段:TC 用单边 RDMA 读它需要的对象(包括将要写的),不加锁——这正是"乐观"所在;TC 记下各对象的 version#,并把写缓存在本地

提交有两大目标:原子分布式提交(要么全写要么全不写)与可串行化(serializability)(看起来像整体先于或后于其他每个事务)。

LOCK(提交协议第一条消息):TC 用 RDMA 向每个被写对象的主追加一条 LOCK 日志记录(含 oid、事务读到的 version#、新值)。它既是预写日志(WAL)条目,又是发给主的 RPC 请求,落在主的 NVRAM 里、可挺过断电。主处理 LOCK 时:

✅ 关键结论:LOCK 的原子检查

主轮询到 LOCK 后:若对象已被锁、或 version# ≠ 事务读到的值,回 "no";否则置锁标志并回 "yes",但暂不改数据。检查锁 + 检查版本 + 置锁这三步用原子 compare-and-swap(CAS) 完成(锁标志是 version# 的最高位),以防别的 CPU 也在处理 LOCK、或有客户端正用 RDMA 在读。

TC 等齐所有 LOCK 回复:任一为 "no" 即中止(向各主追加 ABORT 以释放锁,txCommit() 返回 "no")。

VALIDATE(只读对象的优化):对事务只读未写的对象,TC 发一次单边 RDMA 读重新取其 version#/lock;若已被锁或版本变了则中止。它不置锁,因而比 "LOCK+COMMIT" 快。

COMMIT-BACKUP / COMMIT-PRIMARY:TC 向各追加 COMMIT-BACKUP(等硬件 ack),再向各追加 COMMIT-PRIMARY(只等 RDMA 硬件 ack——表示已安全落在主的 NVRAM,不等主处理日志),随后 txCommit() 返回 "yes"。主之后处理 COMMIT-PRIMARY 时才:拷新值入对象内存、version# 自增、清锁。

📘 定义:提交点(Commit Point)

写下第一个 COMMIT-PRIMARY 的时刻即提交点——从那一刻起事务结果就可能被揭示(主写值并解锁)。纯只读事务只用单边 RDMA 读,无写、无日志记录、无锁——极快


6. 为什么 OCC 能保证可串行化(直觉 + 例子)

直觉:验证就是在问"这次执行是否等同于一次只跑一个"。若期间没有冲突事务,版本号不会变;若有冲突,必有一方在 LOCK 或 VALIDATE 时看到锁或变化的版本号,从而中止。

🔬 例子 1:T1、T2 都做 x = x + 1

可串行化允许的结果(等价于逐个跑):x=2(两方都"成功")、x=1(一方成功一方中止)、x=0(两方都中止)。记号 Rx0=读到 x=0、Lx=锁 x、Cx=提交 x:

text
完全同步:  T1: Rx0 Lx Cx        交错:  T1:    Rx0 Lx Cx        错开:  T1: Rx0 Lx Cx
           T2: Rx0 Lx Cx               T2: Rx0          Lx Cx          T2:           Rx0 Lx Cx

同步/交错时,后到的 LOCK 会发现 version# 已变(或被锁)而中止;错开时两者都能提交得 x=2。无论哪种交错,结果都落在可串行化允许的集合里。

🔬 例子 2:经典 VALIDATE 测试(x、y 初值为 0)

T1: if x==0 then y=1T2: if y==0 then x=1。可串行化的合法结果:T1,T2 → y=1,x=0T2,T1 → x=1,y=0;中止可留 x=0,y=0。但禁止 x=1,y=1

text
同步:  T1: Rx Ly Vx Cy          错开:  T1: Rx Ly Vx     Cy
       T2: Ry Lx Vy Cx                 T2: Ry       Lx Vy Cx

同步时两个 LOCK 都成功、但两个 VALIDATE 都因看到对方锁位而失败 → 两者都中止(合法)。错开时 T1 提交、T2 的 Vy 看到 T1 的锁或更高版本而中止。关键:不可能两个 V 都排在对方的 L 之前,所以永远产生不出被禁止的 x=1,y=1。VALIDATE 既正确又快(一次单边读,而非 LOCK+COMMIT 两次写)。


7. 容错与恢复

✅ 关键结论:恢复的核心准则 若一个事务被故障打断,**而客户端可能已被告知它提交、或其提交值可能已被别的事务读到**,那么恢复时**必须保留并完成**该事务。

由 Figure 4:已提交的写可能在第一个 COMMIT-PRIMARY 发出时就被揭示,所以到那时,该事务的所有写必须已在所有相关分片的全部 f+1 个副本上。LOCK + COMMIT-BACKUP 正好做到这点:LOCK 把新值告诉所有主,COMMIT-BACKUP 把新值告诉所有备;TC 在收齐所有 LOCK 与 COMMIT-BACKUP 之前不发 COMMIT-PRIMARY(备可能还没处理 COMMIT-BACKUP,但已在其 NVRAM 日志里)。

为什么必须写 COMMIT-PRIMARY?(否则的危险情形)

IMPORTANT

若 TC 在 COMMIT-BACKUP 之后、COMMIT-PRIMARY 之前就对应用回 "yes",随后 TC 与所有备都失败——此时仅剩的证据是 LOCK 记录,而即便一整套 LOCK 记录也无法判断 TC 到底提交了还是因 VALIDATE 失败而中止。写下 COMMIT-PRIMARY 化解了风险:因为总有一个分片同时拥有完整的 COMMIT-BACKUP 与 COMMIT-PRIMARY,其中任何一个都是"主决定提交"的证据,从而 TC 的决定能挺过任一分片的 f 次失败。

💁🏼‍♀️提示

日志截断(truncation):TC 见到所有副本都已有 COMMIT-PRIMARY/COMMIT-BACKUP 后,通知它们删除该事务的日志条目;为让恢复仍知"此事务已完成",主在截断后仍记住已完成事务 ID。每个主/备对每个 TC 有独立日志,故截断点之前的条目可一并删除。


8. 局限性

  • OCC 怕冲突:多事务争同一对象时,LOCK 阶段会撞已持锁、VALIDATE 阶段会撞变化版本 → 大量中止重试,性能下滑。FaRM 评测好,多半因其负载冲突少。
  • 数据须全装 RAM
  • 仅单数据中心内复制,无地理分布 → 容错能力有限。
  • 数据模型底层,需自建如 SQL 的上层库;NSDI 2014 的 API 用回调返回、较难用,应用代码须紧密交织"执行事务"与"轮询 RDMA 队列/日志",且执行中可能看到(最终会被中止的)不一致——应用须容忍而不崩溃。
  • 依赖不常见的 RDMA + NVRAM 硬件;线程被钉在核上、占满所有核,应用难自由用线程。
  • FaRM 只保证已提交事务的可串行化;看到不一致的事务会被中止。

NOTE

FAQ 的清醒提醒:论文中"终结一致性/可用性/性能的妥协"一节更像广告而非科学——历史表明,没有哪种性能高到无人想要更多,而那些人往往愿意在别处妥协来换性能。


9. FaRM vs Spanner

两者都分片、复制、用 2PC 做事务,但优化方向相反:

✅ 关键对比
SpannerFaRM
主要应对地理复制的网络延迟CPU 成本
关键技术Paxos 容忍延迟 + TrueTime 读本地副本RDMA + 直连 NIC + NVRAM 避免磁盘写 + OCC
部署跨地理单数据中心
性能读写事务 10–100ms简单事务 58µs(约快 100×)

NOTE

FaRM 用 ZooKeeper + CM 做外部配置管理、用标准主备协议处理事务写,论文将这种"外部主导重配置、Paxos 组重配时仍可服务"的风格松散地称为 vertical Paxos


10. 小结与工程视角

✅ 一图流记忆 NVRAM(电池+SSD)消灭磁盘写 + 单边 RDMA/内核旁路消灭网络-CPU 开销 + OCC 让读无需服务器参与
⇒ 微秒级分布式事务;
LOCK(WAL+RPC,CAS 原子置锁)→ VALIDATE(只读对象单边重读)→ COMMIT-BACKUP → COMMIT-PRIMARY(提交点)
⇒ 原子提交 + 可串行化 + 可恢复。
  • 超高速分布式事务:硬件异构(NVRAM、RDMA)但未来可能普及;性能不仅来自硬件,更来自同时优化网络、持久化、CPU 三者(前人多只优化其一),以及大量用单边 RDMA 替代完整 RPC。
  • 对你(分布式 SaaS)的可借鉴点:① 若自配电池/UPS,可改造你的 Raft / k-v 服务利用电池做快速持久化;② OCC 适合读多写少、冲突稀疏的负载(很多 SaaS 读路径符合);③ "用版本号 + 原子 CAS 做无锁校验"是高并发计数/库存场景的通用模式;④ FaRM 只在"需要每秒百万级事务"时才划算——几千 TPS 直接用成熟的 MySQL 即可。