第6章:安全运行时重配 —— epoch handoff 与 I1 / I2 / I3 不变式
AURA v27 怎么保证在线搬锁不破坏快照隔离:freeze-drain-handoff-publish 四阶段时序、W14.5 dual-authority 守卫、I1(at-most-one authority)+ I2(read set 与 commit ts 一致)+ I3(epoch handoff 排空 in-flight txn)三个不变式的证明草图与反例破坏
第 4-5 章告诉你 v27 用 A1/A2/A3 真的会在线搬 cohort owner、改 cohort 形状。但 OLTP 系统不能用”反正后台慢慢搬”的方式应付一致性——搬运过程中只要有一刻出现”两个 CN 同时认为自己是 owner”,整个 SI/OCC 一致性就完蛋。第 6 章就是回答这个问题的:AURA v27 用 epoch-based handoff + W14.5 dual-authority guard + I1/I2/I3 三不变式 把”safe reconfiguration”做成可证明的协议。本章把 freeze-drain-handoff-publish 四阶段时序图、W14.5 invariant 守卫、I1/I2/I3 证明草图和最重要的反例破坏(如果哪个阶段省掉会怎么坏)讲清楚——这是 paper §5 Correctness 一节的可教学版。
📑 目录
- 1. 为什么”在线搬”不能简单地”复制 + 切换”
- 2. Epoch handoff 四阶段:freeze-drain-handoff-publish
- 3. W14.5 dual-authority 守卫
- 4. I1 不变式:at-most-one active authority
- 5. I2 不变式:read set 与 commit timestamp 一致
- 6. I3 不变式:epoch handoff 排空 in-flight txn
- 7. 反例破坏:每一步省掉会发生什么
1. 为什么”在线搬”不能简单地”复制 + 切换”
朴素方案如下:
CN A (旧 owner) Router CN B (新 owner)
┌────────────────────┐ ┌─────────────┐
│ cohort C 的 lock │ │ │
│ table │ │ │
└──────────┬─────────┘ └──────┬──────┘
│ │
│ ① 复制 lock table state ────────────────► │
│ │
│ ② 通知 router 切换 ownership │
├──────────► router.OwnerMap[C] = B │
│ │
│ ③ 后续 txn 到 CN B │
这个方案在 OLTP 上至少三处会坏:
问题 1:CN A 的 in-flight txn 还没 commit,复制时机不对
CN A 正在处理 txn T1(已经拿到 cohort C 的锁,正在做 OCC validation)。复制时刻 T1 还没 commit——复制过去的 state 是 “T1 持有锁但未释放” 这个中间态。CN B 接管后 T1 commit/abort 走 CN A 的路径,CN B 的 state 跟实际 lock holder 不一致。
问题 2:router 切换那一刻,旧请求还可能到 CN A、新请求已经到 CN B —— 双权威
router 把 OwnerMap[C] 改成 B 这一动作是 TCP push 给所有 client。push 完成前,部分 client 还在用 A,部分 client 已经在用 B。如果一笔 txn 在 A acquire 同时另一笔 txn 在 B acquire,两笔 txn 都成功 —— OCC validation 时各自看到的 lock holder 不一样。
问题 3:copy 不是原子的,复制中途 CN A 可能新写入 cohort 状态
复制 1 MB 的 lock table 需要时间。复制开始到结束之间 CN A 可能:
- 释放某把锁(state 在 A 是”已释放”,但复制过去的 state 是”持有”)
- 接受新 acquire(state 在 A 是”新锁”,复制过去的 state 是”无锁”)
结论:必须设计显式协议,不能依赖”快速复制 + 切换”的乐观假设。
🍎 直觉比喻:搬家不能”两边都开门、客户随便走”——必须先关 A 门、清空 A 屋、把行李运到 B、再开 B 门。
🧠 关键洞察:disaggregated OLTP 的 in-line migration 在所有 prior work 里都有一些版本(DrTM+H / Calvin / Spanner),但 AURA 是最频繁的(100 ms tick)。频率高使得”协议正确性 + drain 速度”成为关键 trade-off——这就是 §3-§6 要讲的。
2. Epoch handoff 四阶段:freeze-drain-handoff-publish
v27 的迁移协议有 4 个显式阶段。任意一阶段的次序错乱都会导致 §1 中列出的三个问题之一。
2.1 完整时序图
t CN A (旧 owner) CN B (新 owner) Router
│ │
├── ①freeze──> stop accepting new req on C │
│ (新到达的 txn 被 reject,client retry) │
│ │
├── ②drain──> wait until all in-flight on C commit/abort │
│ (CN A 上 cohort C 的 in-flight count → 0) │
│ │
├── ③handoff ─copy frozen state─────────────────────► install │
│ │
│ state on CN B │
│ │
├── ④publish ────────────────────────────────────────────────────►│
│ router 切 OwnerMap[C] = B
│ │
▼ ⑤新请求路由到 CN B
2.2 4 阶段的必要性
| 阶段 | 防什么 | 跳过会怎样 |
|---|---|---|
| ①freeze | 防止新 txn 在迁移期间加塞 | drain 永远不结束 |
| ②drain | 防止 in-flight txn commit 时找不到 authority | partial commit / SI 破坏 |
| ③handoff | 防止 CN B 接管时 state 缺失 | CN B 持有的锁是空的 |
| ④publish | 防止 client 继续路由到已 freeze 的 A | cohort 不可用 |
2.3 关键时序约束
四阶段必须严格顺序:
freeze ──before──> drain ──before──> handoff ──before──> publish
任意两个阶段的次序颠倒都会触发问题。§7 给出每种颠倒的反例。
2.4 实现位置
代码骨架:
// src/transaction/aura/MigrationCoordinator.cc
void Coordinator::Migrate(CohortId c, CnId from, CnId to) {
Freeze(c, from); // ① freeze
DrainInflight(c, from); // ② drain
auto state = CopyFrozenState(c, from); // ③ handoff: copy
InstallState(c, to, state); // ③ handoff: install
PublishOwnerMap(c, to); // ④ publish
}
⭐ 教学要点:四阶段每一步都是 blocking——下一步必须等上一步完成。AURA 每 100 ms 触发 N 个 cohort migration,所以 drain 速度是性能关键(见 §6 trade-off)。
3. W14.5 dual-authority 守卫
四阶段协议看起来 sound,但存在一个 race window:
第 ④ publish 阶段,router 把 OwnerMap[C] 改成 B 这一动作不是 atomic broadcast。 部分 client 已经看到新 map(路由到 B),部分 client 还看到旧 map(路由到 A)。 如果旧 client 在这一窗口里发请求到 CN A —— CN A 已经 freeze 了,怎么办?
答案:W14.5 dual-authority guard。
3.1 守卫位置
CN 在接收任何 lock 请求时,先检查 epoch:
// src/transaction/aura/OwnerLockTable.cc - W14.5 guard
LockResult OwnerLockTable::Acquire(LockReq req) {
if (req.epoch < current_epoch_) {
return REJECT_STALE_EPOCH; // ← W14.5 guard
}
if (cohort_state[req.cohort] == FROZEN) {
return REJECT_FROZEN;
}
// ...
}
- 每次 OwnerMap 更新时 epoch 单调递增
- client 路由请求时把当前 epoch 带上
- CN 收到请求时拒绝 stale epoch
3.2 client 端处理
收到 REJECT_STALE_EPOCH → client 拉新 manifest(从 memcached)→ retry。
retry 路径非常快(manifest 拉取 ~ms 级别),用户感知不到。
3.3 W14.5 vs W14
W14 是 v25 时期的原始 invariant:
W14:cohort C 的 active authority 在任意 wall-clock 时刻 ≤ 1。
W14.5 是 v27 对 W14 的细化:
W14.5:W14 + 在 OwnerMap 切换的 race window 内,旧 CN 通过 epoch check 拒绝 stale 请求,新 CN 通过 mode check 拒绝未安装完 state 的请求。
W14 是目标,W14.5 是实现方式。
🍎 直觉比喻:W14 是法律:“任何时候只能一个 owner”。W14.5 是执法:“出示 epoch 凭证,旧 epoch 一律拒绝”。
3.4 实现细节
epoch 在 router 进程维护:
// src/transaction/aura/EpochManager.cc
class EpochManager {
std::atomic<Epoch> current_epoch_;
Epoch BumpEpoch() {
return current_epoch_.fetch_add(1, std::memory_order_acq_rel);
}
};
每次 PublishOwnerMap 之前先 BumpEpoch。
🧠 关键洞察:W14.5 guard 加在 CN 端而不是 router 端 —— 因为 router 端无法保证 atomic broadcast。把守卫推到 CN 端就避免了”router broadcast 时间窗口”这个不可消除的物理事实。
4. I1 不变式:at-most-one active authority
4.1 形式化陈述
对任意 cohort c、任意 wall-clock 时刻 t:
| { cn : cn ∈ CNs ∧ Active(cn, c, t) } | ≤ 1
其中 Active(cn, c, t) = cn 在 t 时刻持有 c 的 active authority。
4.2 证明草图
按四阶段时序逐阶段证明:
阶段 ①freeze(after):CN A 仍然持有 active authority,但不接受 new req。Active = {A},size = 1。
阶段 ②drain(after):CN A 仍持有 authority,但 in-flight count = 0。Active = {A},size = 1。
阶段 ③handoff(after):CN A 仍持有名义 authority(OwnerMap 没改),CN B 安装了 state 但还没”声明 active”。Active = {A},size = 1。
阶段 ④publish(瞬间切换):router BumpEpoch + 切 OwnerMap。在这一瞬间:
- 任何到 CN A 的请求带的是新 epoch(因为 client 已看到新 map)→ W14.5 guard 跟 CN A 的
current_epoch_比较 → 注意:CN A 没收到 BumpEpoch 通知前,自己的 epoch 仍是旧值 - 关键细化:router publish 前先 broadcast epoch bump 给所有 CN(包括 A),等所有 CN 确认收到后再切 OwnerMap
这一细化保证:publish 瞬间 CN A 已经知道 current_epoch_ 是新值,新请求带新 epoch 到 A 会被拒绝(cohort state 是 FROZEN)。
阶段 ④publish(after):CN B 收到第一个新 epoch 请求 → Acquire → state 是 INSTALLED → Active = {B},size = 1。
→ 全程任意 t 都有 |Active| ≤ 1。
4.3 关键引理
引理证明依赖两个事实:
- Epoch 单调递增:BumpEpoch 是 atomic fetch_add,全局有序
- Epoch broadcast 在 OwnerMap 切换之前:router 协议保证
少任一条 I1 都不成立。
⭐ 教学重点:I1 是”双权威”的负面定义——证明”不出现双权威”等价于证明”任意时刻 active authority 集合 ≤ 1”。
5. I2 不变式:read set 与 commit timestamp 一致
5.1 形式化陈述
对每个 commit 的 txn T:
∀ k ∈ T.read_set : version(k, T.commit_ts) = T.read_value(k)
即 T 读到的每个 key 的值,在 T 的 commit timestamp 处仍然有效(OCC validation pass)。
5.2 证明草图
OCC validation 的标准证明依赖:
- validation 阶段需要 lock authority(防止其他 txn 同时改 read set)
- lock authority 必须 “active”
- active authority 必须唯一(即 I1)
→ I2 依赖 I1。
只要 I1 成立,OCC validation 的原子性就有保证——validation 在唯一 active authority 内进行,没有并发干扰。
5.3 cohort 边界处理
如果 T 的 read set 跨多个 cohort,每个 cohort 都有自己的 active authority。需要:
- 每个 cohort 的 lock authority 提供”local validation”
- 跨 cohort 的 read set consistency 由 timestamp 序保证(OCC 全局 commit_ts 单调)
这是标准 OCC 协议保证的,AURA 不改这部分。
🌟 结论:I2 不是 AURA 的新发明——是 OCC 协议的基本保证。AURA 的贡献是让 I2 在 active authority 迁移中仍成立(因为 I1)。
6. I3 不变式:epoch handoff 排空 in-flight txn
6.1 形式化陈述
对每个 epoch e → e+1 切换:
∀ txn T : T 在 epoch e 启动 ⇒ T 在 epoch e 内 commit 或 abort
即 epoch 内启动的 txn 必须在 epoch 内结束。
6.2 实现:drain 阶段
阶段 ②drain 的定义就是”等所有 in-flight 完成”:
// src/transaction/aura/MigrationCoordinator.cc - drain
void DrainInflight(CohortId c, CnId from) {
while (true) {
auto count = inflight_count_.load(std::memory_order_acquire);
if (count == 0) break;
std::this_thread::yield();
}
}
CN 端维护 cohort-level in-flight counter,drain 等到 counter == 0。
6.3 风险:长事务导致 drain 卡住
OLTP 大部分 txn 是短事务(< 1 ms),drain 通常 < 10 ms 完成。但偶尔有长事务(如 TPC-C delivery),drain 等几十 ms 才结束。
→ migration 卡住,planner 后面排队的 migration 全部 stall。
6.4 v27 mitigation
drain 超时(默认 100 ms)→ 强制 abort 残余 txn → migration 继续。
trade-off:
- 不超时:safety 优先(所有 txn 正常 commit),但 migration 可能 stall
- 短超时:liveness 优先(migration 不 stall),但部分长 txn 被无辜 abort
v27 选 100 ms 是个经验值——TPC-C 99% txn < 5 ms,超时影响 < 1%。
🧠 关键洞察:I3 是”liveness vs safety”的 trade——drain 超时是 safety 优先的体现。两者不可兼得,必须显式选择。 AURA 选 safety 主导,liveness 用超时兜底。
6.5 跟 Calvin / Spanner 的对照
Calvin 的 epoch 是 batch boundary(deterministic execution),drain 是天然的(每 batch 跑完)。 Spanner 的 epoch 是 commit timestamp 边界,drain 通过 TrueTime wait 实现。 AURA 的 epoch 是 OwnerMap 切换边界,drain 通过 cohort-level in-flight counter 实现。
三者都是 epoch 范式,但 actuation 不同。AURA 是最细粒度的(per-cohort)。
7. 反例破坏:每一步省掉会发生什么
教学反例——把每种”省掉”的具体后果讲清楚。
7.1 省掉 ①freeze
症状:CN A 继续接 new req on cohort C。
后果:
t=0: drain 开始,inflight_count = 5
t=1ms: 3 个 in-flight commit,count = 2
t=1.1ms: 新 req 到 CN A(freeze 没生效)→ count = 3
t=2ms: 2 个 commit + 新 1 个 acquire → count = 2
...
inflight_count 永远到不了 0 —— drain 永远不结束,migration 永远卡。
7.2 省掉 ②drain
症状:CN A 上还有 in-flight txn T1(持有 cohort C 的锁),handoff 直接开始。
后果:
- T1 在 CN A 上做 OCC validation,需要查 cohort C 的 lock holders
- handoff 同时把 lock table state 复制到 CN B
- T1 commit 时 CN A 的 state 已被复制,T1 释放锁的动作没同步到 CN B
- → CN B 上 cohort C 的 lock table 显示 “T1 仍持有锁”,但实际 T1 已 commit
- → 后续 txn 在 CN B acquire 锁时被 phantom T1 阻塞
SI 破坏:partial commit + phantom lock。
7.3 省掉 ③handoff(含 state copy)
症状:CN A freeze + drain 完成,router 直接切 OwnerMap[C] = B。
后果:
- CN B 上 cohort C 的 lock table 是空的
- 第一个新 txn 在 CN B acquire cohort C 的 lock:lock table 显示无 holder → grant
- 但 CN A 在 freeze 前持有的写者状态(如 record k 被 T0 写过、写时间戳 ts0)丢了
- → CN B 给新 txn 的 view 不包含 T0 的写
- → SI snapshot read 错误
SI 破坏:丢失旧写。
7.4 省掉 ④publish
症状:所有阶段都执行完,但 router OwnerMap 没翻新。
后果:
- client 继续路由到 CN A
- CN A 已 freeze,所有请求被 REJECT_FROZEN
- client retry → 仍 reject → 用户超时
- → 整个 cohort C 不可用,直到 router 终于 publish
Liveness 破坏:cohort 永久不可用。
7.5 省掉 W14.5 guard
症状:四阶段都正常,但 CN B 不检查 epoch。
后果:
- publish 阶段 router broadcast OwnerMap[C] = B,部分 client 已看到,部分还没
- 旧 client 发请求到 CN A(带旧 epoch)→ CN A 已 freeze → REJECT
- 新 client 发请求到 CN B(带新 epoch)→ CN B accept
- 但是:万一某个新 client 因为 partial broadcast 看到了”OwnerMap 的新值但 epoch 是旧的”(这是 broadcast 不 atomic 的副作用)
- → 该 client 路由到 CN B 但带旧 epoch
- → 没有 W14.5 guard,CN B 接受这笔请求
- → 这笔请求的 read set 是基于”旧 OwnerMap 假设的 cohort 分布”,跟 CN B 的实际 state 不一致
- → 读到过期数据
SI 破坏:dirty read。
7.6 反例破坏的 5 条都被处理
v27 协议设计已经 cover 这 5 个反例:
| 反例 | v27 防御 |
|---|---|
| 省 freeze | freeze 是 ① 阶段,必须在 drain 前 |
| 省 drain | drain 等 inflight_count = 0 |
| 省 handoff | handoff 是 ③ 阶段,必须在 publish 前 |
| 省 publish | publish 是 ④ 阶段,由 MigrationCoordinator 保证 |
| 省 W14.5 | W14.5 是 OwnerLockTable.Acquire 第一行 check |
⭐ 教学重点:把”为什么这一步不能省”讲透,比把”协议是什么”讲透更重要——评委或读者真正想知道的是”如果偷懒会怎么坏”。
🌟 结论:第 6 章不是抽象证明,而是 5 个具体反例的”破坏剧本”。读完你能讲出”如果第 N 步不做,会在第 M 时刻具体怎么坏”,而不是只复述协议名字。
✅ 自我检验清单
- 三大朴素问题:能列出 “复制 + 切换”朴素方案的 3 个失败模式(in-flight 未完 / 双权威 / copy 不原子)
- 四阶段顺序:能默写 freeze → drain → handoff → publish 并解释每阶段的必要性
- W14.5 守卫:能解释 W14.5 守卫在什么位置、防什么 race(broadcast 不 atomic 的 race window)
- I1 证明:能给出 I1 的形式化陈述 + 四阶段证明草图 + 两个关键引理(epoch 单调 + epoch broadcast 在 publish 前)
- I2 依赖 I1:能解释 I2 怎么依赖 I1 + OCC 原子性 + cohort 边界处理
- I3 trade:能解释 I3 drain 超时是 safety vs liveness 的取舍,并举出 100 ms 经验值的理由
- 反例破坏:能针对四阶段中任意一阶段 + W14.5 共 5 条,举出”如果省掉会发生什么”的具体例子
- v25 → v27 差异:能解释 W14.5 比 W14 多防了哪个 race window(broadcast 不 atomic)
📚 参考资料
概念入门
- v27 paper outline §3.8 + §5 —— epoch handoff + I1/I2/I3 source of truth
- Snapshot Isolation (Berenson et al., SIGMOD’95) —— SI 定义
- OCC (Kung & Robinson, TODS’81) —— OCC 三阶段定义
关键论文
- Calvin (SIGMOD’12) —— deterministic 执行的 epoch 范式启发;AURA 用 epoch 边界做 OwnerMap 切换
- Spanner (OSDI’12) —— global epoch + TrueTime;AURA 不需要 TrueTime(用 cohort-level inflight counter)
- DrTM+H (USENIX ATC’18) —— RDMA 时代的事务 epoch handoff 范式
行业讨论
- 模块二十三《AURA 论文精讲》第7章-迁移协议与I1-I4一致性证明 —— v25 时期有 I1-I4 四不变式
- 模块十五《分离式事务的动态锁所有权》第12章-W15W16W18跨CN一致性的两条独立路径 —— W 系列 invariant 的工程踩坑视角
框架文档(代码 anchor)
src/transaction/aura/EpochManager.cc—— epoch 状态机src/transaction/aura/MigrationCoordinator.cc—— 四阶段编排src/transaction/aura/OwnerLockTable.cc::Acquire—— W14.5 dual-authority guardsrc/transaction/aura/OwnerLockTable.cc::ReleaseAfterCommit—— I2 lock release timing
📎 v25 对照视角:模块二十三-AURA 论文精讲 第7章-迁移协议与I1-I4一致性证明 —— v25 用 4 个不变式(I1-I4),v27 合并 I3+I4 为单条 I3;增加 W14.5 处理 broadcast race window