跳到主要内容
分离式事务的动态锁所有权

第14章:Router-centric 重构 —— 9216 cohort 全表展开与 InstallCohort 幂等性陷阱

AURA Phase R-big 把控制平面从 per-CN 搬到 BenchClient/router 进程,借鉴 H-Store/VoltDB/Calvin/TiDB OLTP 谱系的中心化编排,9 张 TPCC 表全表展开 9216 个 per-bucket cohort,并在端到端验证里暴露了 OwnerLockTable::InstallCohort 的幂等性陷阱——LOCAL 命中 17% 但 atomic 没降的真因

AURA Router-centric Phase R-big OwnerMapSubscriber InstallCohort 9216 cohort H-Store VoltDB Calvin TiDB 工程教训

第 13 章把 W8.7 全局广播 −1.42pp 的折损讲到了 per-Apply 本地 OwnerMap.Publish 这一层。本章往上游一步:当 per-CN 控制环 race-publish OwnerMap 越搞越乱、当论文 §3 想讲一个干净的「BenchClient 集中编排 → CN 仅作执行体」的故事,工程上该怎么走?AURA Phase R-big 选了 H-Store/VoltDB/Calvin/TiDB OLTP 谱系的老路——控制平面从 per-CN 搬到 router 进程,CN 反向上报 ACCESS_SUMMARY、router 唯一发布 manifest、CN 端用 OwnerMapSubscriber 把 9216 个 per-bucket cohort 全表展开装进 OwnerLockTable。本章把这 8-phase 重构、9216 cohort 的存储/序列化代价、以及暴露出的 OwnerLockTable::InstallCohort 幂等性陷阱(LOCAL 命中 17% 却 atomic 没降的真因)一次性讲透。读完你能徒手画出 router-centric 数据流,并解释为什么 EvictCohort + InstallCohort 才能让新 epoch 生效。

📑 目录


1. 为什么动手做 router-centric 重构 —— per-CN 控制平面留下的两个问题

第 13 章给出的折损链:W7.4-B +3.76% → W8.5 加 hash-det owner 掉到 +2.37% → W8.7 全局广播再掉到 +0.95% → W8.11 批广播补 0.11pp 到 +1.06%。§6 已经把”真瓶颈不在 RPC 数量、在 per-Apply 本地 Publish”这条铁律敲定。但这条折损链背后藏着更结构性的两个问题——它们不是再加一层批广播能补回来的。

1.1 问题 1:per-CN 控制环让 OwnerMap publish 发生 race

4 个 CN 各自跑一个 AuraControlLoop,每个 CN 在 5 ms tick 里独立地:

profile 本地 hot KG → 决定本地要 takeover 的 cohort
                  → 在自己进程里 OwnerMap.Publish(epoch++, snapshot)

这意味着 4 个 CN 在物理时间上并发 publish 4 份 OwnerMap,epoch 编号空间没有全局协调。一个 cohort 在 CN0 看是 OWNED@cn0,在 CN1 看可能也是 OWNED@cn1。一致性靠”W7.4-B 不跨 CN 协调,谁先 takeover 谁就走 LOCAL”这条心照不宣的契约硬撑——W8.5 加 hash-det owner 是 patch,不是根治。

1.2 问题 2:论文 §3 想讲的故事和实现长得不像

论文 §3 在 d1cde1e 重写后讲的故事是”BenchClient 是中央编排器 / CN 是纯执行体”。但 W8.11 的代码里:

  • BenchClient 完全不知道 OwnerMap 长什么样
  • 每个 CN 自己 publish 自己看见的 view
  • Manifest 不是从一个权威来源发布,而是 4 个 CN 各发各的

🌟 结论:W7.4-B → W8.11 这条折损链在 per-CN 框架里已经无法靠”再加一层广播 / 再批一次 RPC”挣回来。只有把 publish 的唯一权威拎到 router,让 CN 退回纯执行体角色,论文 §3 的”动态锁所有权 + 集中编排”故事才能在代码里立起来。


2. 设计映射:AURA-router-centric 在 OLTP 谱系里的位置

把控制平面拎到一个独立进程,OLTP 主流系统过去十年都这么干。把它们摆一起对比,AURA-router-centric 的设计选择就有了清晰的工业坐标。

2.1 五个系统的控制平面 / 执行平面分离方式

系统控制平面执行平面同步机制DM 场景适配性
H-Store / VoltDBsite coordinatorper-partition single-thread executorcommand pipepartition 严格静态,DM 拷不动数据
Calvinsequencer(deterministic ordering)per-shard executor全序日志全序需求过强,不利于 OCC
TiDB / TiKVPD (Placement Driver)TiKV / KV nodegRPC + Raft分片元数据可漂,与 cohort owner 漂移直接对应
F1 / Spannerspanserver masterdata shard workerPaxos group + TrueTime强一致 + 全球时钟,DM 用不上
AURA-router-centric(本章主题)BenchClient(内嵌 OwnerRpcServer + RouterControlLoopCN(OwnerMapSubscriber + OwnerLockTablememcached pull + OwnerRpc reverse channelcohort 动态、5–100 ms 闭环、跨 CN 多写

2.2 AURA-router-centric 不是某一个的直接搬抄

🧠 关键洞察:AURA-router-centric 把三件事糅在一起——

  • 元数据漂(cohort owner 可换 owner):像 TiDB PD
  • 控制平面只有一个发布者像 Calvin sequencer
  • 锁权威在 CN(用 OwnerLockTable):像 LOTUS

🍎 直觉比喻:H-Store 是”每个柜员管自己的客户、不沟通”,TiDB 是”客户经理(PD)调度柜员安排”,AURA-router-centric 是 “客户经理实时观察客流,每 100 ms 把柜台搬到当前最忙的位置;柜员负责执行业务,遇到不熟的客户回退到总行(MN)原子 CAS”。柜员(CN)和总行(MN)的关系不变,变的是经理(router)有没有实时调度权

🌟 结论:router-centric 不是 AURA 的发明,是 OLTP 谱系里默认存在的那张图。Phase R-big 做的是”把 AURA 的 cohort + ownership 机制嵌进这张图里”。


3. 8-phase 重构地图:R0 → R7 的步骤拆解

3.1 完整 12 个 commits(aura-router-centric 分支)

c199ae1  R0       paper §3 d1cde1e 写完 → 制定 R 计划 (PHASE_R_PLAN.md)
5b8b559  R1-R3    CN ACCESS_SUMMARY 可上传 router (反向 OwnerRpc 通道开通)
ac39ac7  R4       RouterControlLoop 100 ms tick → wh-level manifest 发布
5567afc  R5-R7    CN OwnerMapSubscriber + router-centric mode + M6 runner
221d1e4  fix      RoutingTable::LoadPayload → LoadFromPayload typo
f6e2c0e  fix      router_addr 改用 experiment-net IP (10.10.1.3)
e8b6005  diag     log 探针 + CN-side PublishManifest router-centric skip
b851801  v7       router-centric pipeline 端到端 verified
3d6a31e  progress R-big SUMMARY
7ea956b  R-big+1  OwnerMapSubscriber 安装 per-bucket cohort × 9 TPCC tables
9f9ee41  R-big+1  TxnIO acquire 路由分布 counter
7dbfb61  R-big+1  ★ critical fix:每 tick EvictCohort + InstallCohort (§6)

3.2 R0–R7 阶段拆解

阶段目标关键改动
R0baseline 数据 + 重构计划docs/aura/PHASE_R_PLAN.md 制定 8 阶段方案
R1CN 端反向通道哨兵AuraTypes.hkRouterCnId = INVALID_CN - 1 = 0xFFFE
R2CN 上报 access summaryAuraControlLoop heartbeat 末尾 unicast SendAccessSummary(kRouterCnId, items)
R3router 端接收OwnerRpcServer::SetAccessSummarySink(cb) + GetSinkSnapshot()
R4router 端聚合 + 发布AggregatedAccessGraph(Ingest/ArgmaxOwner)+ RouterControlLoop 100 ms tick 写 memcached
R5CN 拉 manifestOwnerMapSubscriber(libmemcached pull,100 ms refresh)
R6CN 退出自发布AuraControlLoop::PublishManifest()IsRouterCentric() 时 early-return
R7M6 runner + 端到端 smokebench/aura/run_matrix_R7_M6.sh + 8 阶段 LOG_INFO 探针

互补:R1–R3 之所以单独成段,是因为反向通道是整个 router-centric 设计的前提——没有 CN → router 的 ACCESS_SUMMARY 反向流,router 就只能用静态 wh % num_cn 规则发布 manifest,那不叫 router-centric,叫 hard-coded routing。


4. CN → router 的反向数据流:ACCESS_SUMMARY 上报与 kRouterCnId 哨兵

OwnerRpc 原本是一个双向通道:CN i 可以向 CN j 请求 cohort handoff、或 broadcast OwnerMap delta。Phase R-big 把它扩成三向

CN ↔ CN          peer handoff(保持,第 6 章)
CN → router      access summary 反向上报(新增)
router → CN      manifest 写 memcached(不复用 OwnerRpc,走 memcached pull 模型,§5)

4.1 哨兵 ID

// src/transaction/aura/AuraTypes.h
constexpr cn_id_t kRouterCnId = INVALID_CN - 1;   // = 0xFFFE

router 进程不占真实 cn_id 空间,复用 cn_id_t 类型,让所有 OwnerRpc 接口签名完全不变

4.2 OwnerRpcServer 的 sink 回调

// src/transaction/aura/OwnerRpcServer.h
class OwnerRpcServer {
public:
  using AccessSummarySink =
      std::function<void(cn_id_t cn, std::vector<AccessSummaryItem>)>;

  void SetAccessSummarySink(AccessSummarySink cb);   // 给 RouterControlLoop 注册
  std::vector<AccessSummaryItem> GetSinkSnapshot();  // 给 tick 读

  // 原 peer-handoff 接口保持不变
};

router-centric 模式下,BenchClient 在 ctor 里 SetAccessSummarySink(...);非 router-centric 模式下 sink 为空函数,OwnerRpcServer 对 access summary 消息直接 drop。运行时 flag 切换,CN 端不需要任何编译条件。

4.3 CN 端 heartbeat 末尾 unicast

// src/transaction/aura/AuraControlLoop.cc (heartbeat 末尾)
if (IsRouterCentric() && tc) {
  std::vector<AccessSummaryItem> items = BuildSummary(/*top_k=*/...);
  rpc_->SendAccessSummary(kRouterCnId, items);   // ← unicast 到 router
  LOG_INFO("CN%u heartbeat tick: SendAccessSummary status=%s items=%zu",
           self_cn_id_, status, items.size());
}

CN 在 OwnerRpc::Connect(kRouterCnId, addr=router_addr) 时,把 router 当成一个特殊的”虚拟 CN”,复用 TCP socket + RPC dispatch 框架。router 进程(BenchClient)启动时不调 Connect,而是 Listen(--router_port) 等 CN 连进来。

🌟 结论:哨兵 ID + sink callback 这套 trick 让 router-centric 重构不需要发明新的 wire protocol——OwnerRpc 接口语义不变,只是新增”目标 CN ID 不是真 CN 而是 router 进程”的特殊地址。代价:编译/链接上 BenchClient 现在依赖 OwnerRpc + Manifest,多了几 KB binary,但部署模型不变。


5. 9216 cohort 全表展开:从 wh-kg 到 per-bucket × 9 表

R5 完成后 pipeline 端到端跑通(v7 smoke 看到 8 组件 LOG_INFO),但有一个反直觉现象:CN0 acquire 命中率仍然 0%。

原因:最初 R5 的 OwnerMapSubscriber 只安装 wh 级别的 cohort(4 个 entries:wh=0/1/2/3 → CN0/1/2/3)。TxnIO 路径上 acquire 的 key_group 来自 stock / district / customer / order 等表的 record,它们的 kg 不是 wh-kg,OwnerMap 查表必然 miss。

R-big+1 (7ea956b) 把 cohort 展开到全表:

5.1 cohort 编码:高 12 bit 表 ID | 低 20 bit bucket

// src/transaction/aura/OwnerMapSubscriber.cc
constexpr int kAllTpccTables[] = {
    0, 1, 2, 3, 4, 5, 6, 7, 8   // WAREHOUSE/DISTRICT/CUSTOMER/HISTORY/
                                // NEW_ORDER/ORDER/ORDER_LINE/STOCK/ITEM
};
constexpr int kNumBuckets = 1024;   // = 1 << KEY_GROUP_BITS

auto encode_cohort_id = [](int t, int b) -> cohort_id_t {
    return (static_cast<cohort_id_t>(t) << 20) | static_cast<cohort_id_t>(b);
};

for (int t : kAllTpccTables) {
  for (int b = 0; b < kNumBuckets; ++b) {
    std::uint32_t wh        = b % n_wh;             // wh-partitioned ownership rule
    std::int16_t  owner_int = wh_to_cn[wh];
    key_group_id_t kg = MakeKeyGroupId(static_cast<TableId>(t),
                                       static_cast<RecordKey>(b));
    cohort_id_t cid = encode_cohort_id(t, b);
    OwnerMapEntry e {
        .cohort_id    = cid,
        .cohort_epoch = map_epoch,
        .owner        = (owner_int >= 0 ? owner_int : INVALID_CN),
        .mode         = (owner_int >= 0 ? OWNED : FALLBACK),
    };
    entries.push_back(e);
    kg_to_cohort[kg] = cid;
  }
}

5.2 容量算账

  • 每 snapshot 9 表 × 1024 bucket = 9216 entries
  • 每 entry {cohort_id u32, cohort_epoch u64, owner u16, mode u8} ≈ 16 B
  • ~144 KB / snapshot

🌟 结论:memcached value 默认上限 1 MB,远远装得下。CN 内存里 flat_hash_map<kg, cohort_id> 也只占几 MB,相对 OwnerLockTable 本身的 cohort 状态可忽略。

5.3 Ownership rule:为什么 bucket bwh_to_cn[b % n_wh]

TPCC hash-partitioned 工作负载下,bucket b 对应的 record key 通常以 wh_id 为高位、partition key 为低位组合。1024 bucket / 表的均匀分布天然让 b mod n_wh 把 bucket “撒”回 wh 编号——把 wh→cn 的映射沿用到 bucket→cn,等于把 wh-level 的 partition affinity 机械下钻到 bucket 级,不引入新的统计假设。

5.4 序列化代价:manifest 不传 cohort 表本身

Manifest payload 当前序列化方式是 Manifest::Serializewh_to_cn vector 写进 memcached value。cohort 表本身不进 manifest——CN 端用 wh_to_cn + b % n_wh 现算 9216 个 entries,避免把巨型表往返 memcached。这是一个 paper §3 没强调但工程上很关键的选择。

🍎 直觉比喻:传 9216 行表过网络 ≈ 印一整本厂区地图给每个柜员;传 4 行 wh_to_cn ≈ 印一张”每个支行经理是谁”的字条,柜员自己按字条推哪个客户归哪个支行。后者每次更新 30 B 而不是 144 KB,且语义无损。


6. InstallCohort 幂等性陷阱 —— LOCAL 命中 17% 但 atomic 没降的真因

6.1 路由分布 diagnostic 揭示反常

R-big+1 v9b 加了路由分布 counter:

// src/transaction/TxnIO.cc (acquire path)
static thread_local uint64_t n_local = 0, n_remote = 0, n_unknown = 0;
RouteResult rt = router_.Route(kg);
if      (rt.mode == LOCAL)  ++n_local;
else if (rt.mode == REMOTE) ++n_remote;
else                        ++n_unknown;
if ((n_local + n_remote + n_unknown) % 10000 == 0) {
    LOG_INFO("TxnIO route dist: local=%lu remote=%lu unknown=%lu (n=%lu)",
             n_local, n_remote, n_unknown, n_local+n_remote+n_unknown);
}

v9b 实测(CN0,acquire 路径):

Countern=50000n=100000
local4475 (9%)16521 (17%)
remote13871 (28%)51825 (52%)
unknown31655 (63%)31655 (32%, 不再增长)

读法:

  • UNKNOWN 早期占 63%OwnerMapSubscriber 启动延迟,初期 ~50 k acquire 还没等到第一份 manifest apply
  • UNKNOWN 在 n=100 k 时不再增长:OwnerMap 完整 install 后所有 kg 都能查到
  • 17% LOCAL = 4 CN 在 wh-based ownership 下的稳态命中率(wh % 4 落自己的概率)
  • 52% REMOTE = 跨 wh 写(NewOrder 15% cross-wh supply + 其他 wh 的 customer/order/etc)

按 14 atomic/txn × 17% LOCAL 跳过率,atomic 理论应该降到 ~11.6。但 M6 实测 atomic 仍然 14.00,完全没降。这就是本章核心 bug 的入口。

6.2 bug 位置:OwnerLockTable.cc:175 的幂等 install

// src/transaction/aura/OwnerLockTable.cc (修复前)
void OwnerLockTable::InstallCohort(cohort_id_t cohort_id,
                                   epoch_t epoch,
                                   std::vector<key_group_id_t> members) {
  auto state      = std::make_unique<CohortState>();
  state->epoch    = epoch;
  state->members  = std::move(members);
  std::unique_lock lock(mtx_);
  if (table_.find(cohort_id) == table_.end()) {   // ← 幂等:已存在就 skip
      table_[cohort_id] = std::move(state);
  }
  // else: skip, keep old state with old epoch
}

Subscriber 每 100 ms 用 map_epoch = 1, 2, 3, ..., 280InstallCohort(cid, map_epoch, members)第一次 install (cid, epoch=1) 成功;之后所有 install 都被 if (table_.find(cohort_id) == table_.end()) 拒绝。结果:lock_table 内部 cohort epoch 永远停在 1

6.3 现象链:从 17% LOCAL 到 atomic 14.00 不降

acquire(kg) → AffinityRouter::Route(kg)

   RouteResult { mode=LOCAL, cohort_id=cid, cohort_epoch=280 }   // router 当前发布的 epoch

TxnIO 调 OwnerLockTable::TryLock(cid, expected_epoch=280)

   state->epoch == 1(永远停在 1) → != 280 → STALE_EPOCH

TryLock 返回 fail → fall-through 到 MN atomic CAS

结果:17% LOCAL route 中 ~100% TryLock fail → atomic 完全没省

🧠 关键洞察LOCAL 命中只是”router 觉得这个 kg 该我自己处理”,atomic 真正省下来的前提是 OwnerLockTable::TryLock 成功。Router-centric pipeline 端到端跑通 ≠ atomic 真下降——必须每条链路都通才行。这条 bug 在 8 个 LOG_INFO 探针里看不出来(每个探针自己都”成功了”),只能通过 atomic counter 才看得见。可观测性 baseline 在”端到端 reachability” 之上还要加一层”语义正确性 SLO”

6.4 修复:EvictCohort + InstallCohort 强制重装

// src/transaction/aura/OwnerMapSubscriber.cc (commit 7dbfb61)
if (owner_int >= 0 && static_cast<cn_id_t>(owner_int) == self_cn_id_) {
    if (lock_table_ != nullptr) {
        // Phase R-big+1 fix: InstallCohort is idempotent — keeps original epoch.
        // Force fresh install with new epoch by evicting first.
        lock_table_->EvictCohort(cid);                       // ← 关键新增
        std::vector<key_group_id_t> members{kg};
        lock_table_->InstallCohort(cid, map_epoch, members);
    }
    ++n_local_cohorts;
} else if (lock_table_ != nullptr) {
    lock_table_->EvictCohort(cid);   // 不归自己的 cohort 主动 evict
}

6.5 in-flight token 不丢的论证

EvictCohort 会清空 cohort 状态——如果某个事务正持有 cid 的 token 怎么办?

  1. CN 仍是该 bucket 的 owner:本次 manifest tick 中 wh_to_cn[b % n_wh] 没换人,owner 没变;EvictCohort 只清掉旧 epoch state,立刻用新 epoch 重装
  2. TryLock 失败回退原本就走 MN CAS:即便有微秒级 race,未完事务回退到 atomic 路径不丢一致性(这是第 4 章不变式 I4)
  3. 真要换 owner 的迁移已经有 TransferController 走 freeze-drain-handoff-publish 4 阶段(第 6 章),不走这条路径

6.6 根因:接口语义随系统模式变化而漂

🌟 结论bug 的根因不是 InstallCohort 实现错了——if !table_.contains 的幂等性在原 per-CN 设计里是合理的(自己 publish 自己安装,不会出现 epoch 漂移)。问题在于 router-centric 模式下 subscriber 是”被动接收”,每 tick 收到的 epoch 会真的递增,原 invariant 不再成立。代码本身没错,是适用范围变了。这是一类典型的”接口语义随系统模式漂”的陷阱,比”代码写错”更难排查——查日志、查 LOG_INFO 探针都看不到异常,只能从语义不变式倒推。


7. 端到端实测:v7/v8/v9b/v10 链条 + 路由分布数据

7.1 smoke 进度表

smokecommit验证点结果
v65567afcR5 pipeline 启动CN0 acquire 命中 0%(wh-kg only)
v7b8518018 组件 LOG_INFO 全有OwnerMapSubscriber applied epoch=N 持续增长
v87ea956bper-bucket × 9 表 installLOG total_cohorts=9216 local=2304(每 CN 拿 1/4)
v9b9f9ee41路由分布 counterLOCAL=17% / REMOTE=52% / UNKNOWN=32%(n=100 k)
v107dbfb61EvictCohort+Install 修复期望 atomic 14 → ~11.6(待 cluster 恢复确认)

7.2 8 组件 LOG_INFO probe list

router-centric pipeline 跑通需要的 8 条 LOG 都打出来:

[BenchClient]  OwnerRpcServer listen on port=9999
[BenchClient]  AccessSummarySink registered
[BenchClient]  RouterControlLoop started, tick=100ms
[CN0]          AuraControlLoop heartbeat tick: SendAccessSummary status=OK items=K
[BenchClient]  sink_ingested cn=0 items=K
[BenchClient]  RouterControlLoop published manifest epoch=N to memcached
[CN0]          OwnerMapSubscriber applied manifest epoch=N total_cohorts=9216 local=2304
[CN0]          TxnIO route dist: local=L remote=R unknown=U (n=N)

任何一条缺失都意味着对应链路断了——这是 router-centric 的可观测性 baseline。第 6 节的教训提醒我们:8 条全亮不代表语义正确,还要看 atomic counter 这一层 SLO。

7.3 运维教训:CloudLab 5 节点 deploy 的踩坑

R-big 期间几次 deploy 失败:

  • parallel SSH rate limit:5 节点同时 ssh 命中 sshd MaxStartups → 部分节点 hang。修法:aura_deploy_seq.sh 顺序部署
  • CN3 disk 100% 满:amd119 系统盘 16 G,journalctl 累积 1+ GB → 部署时 build artifact 写不下。修法:sudo journalctl --vacuum-size=100M
  • Chinese path + scp/Users/.../2026科研/... 路径里中文字符让某些 scp 行为不一致。修法:临时 stash 到 /tmp/aura_rescue/(纯 ASCII)再 scp
  • CMake cache stale:tarball extract 后 CMakeLists.txt 被覆盖但 build/CMakeCache.txt 仍指旧路径。修法:rm -rf CMakeCache.txt CMakeFiles benchmark src 后再 cmake

🍎 直觉比喻:CloudLab 5 节点 ssh + build + run 不像本地 make all——它是5 个独立服务器 + 1 个公共控制网 + 16 G 系统盘的组合体,任何一条 deploy 链路抖一下整体就 hang。Phase R-big 的”顺序 ssh + 主动 disk vacuum + ASCII-only stash” 三件套是 5 节点 OFED 集群上的最小可用 deploy 范式。


8. 下一个 lever:W9 REMOTE_OWNER takeover 与 client-side cohort 路由

R-big 修完后期望 atomic 14 → ~11.6(drop ~17%),但 paper §6 RQ1 想看到的是 atomic 14 → ~4–5(drop ~70%)。差距在哪?

52% REMOTE 路径仍然走 MN atomic CAS。 想压住这部分,两条互补路径:

8.1 路径 A:W9 REMOTE_OWNER takeover

让 CN i 对自己不 own 的 cohort,通过 OwnerRpc::Acquire(target_cn=owner_cn, cohort_id, lock_buf) 让 owner CN 帮自己做 lock,回 lock_token。commit / release 阶段再用 OwnerRpc::Release

  • 省 atomic:owner CN 在自己 OwnerLockTable 里做 CPU 锁,仍是 0 atomic
  • 加 RPC:每个跨 CN acquire 多一轮 OwnerRpc,需要 SEND completion 闭环
  • 历史教训(PROGRESS.md W9):W9 第一版用 TCP sync RPC,吞吐 −91.7%(同步阻塞 + TCP context switch 把 worker 协程钉死);W11.1 RDMA SEND completion 还没做完

8.2 路径 B:client-side cohort-aware dispatch

直接在 BenchClient 端改路由:原来 wh % num_cn 决定哪个 CN 处理事务,改成”对每个事务的 wh_id,查最新 OwnerMap,把事务发给 cohort owner”。

  • LOCAL 路径占比从 17% 提到接近 100%(前提:单 wh 事务)
  • 不动 CN 内部锁路径
  • 代价:BenchClient 要持有 OwnerMap snapshot(router 自己就有,零成本);跨 wh 事务(NewOrder cross-wh supply)必须 split / 双跳

8.3 两条 lever 的对比与优先级

lever实现复杂度预期 atomic 下降风险
A. W9 REMOTE_OWNER takeover高(RDMA SEND completion)强(52% REMOTE 转 LOCAL on owner)RPC throughput 历史滑铁卢
B. client-side dispatch低(改 RoutingTable)中(17% → ~85% LOCAL)跨 wh 事务必须 split / 双跳

互补:路径 A 改 server 锁路径,路径 B 改 client 路由策略。两者不冲突——A 解决”路由发错了之后跨 CN 也要省 atomic”,B 解决”路由直接发到 owner CN 上让 atomic 天然省”。生产里通常两条一起做。

🌟 结论论文 §6 想要的 atomic 14 → ~4 必须靠 A 或 B 撑住。短期 demo 走 B(实现快、风险低),长期论文实测走 A+B 组合。但这两步已经在论文 §3 router-centric 故事的引擎能力射程内——router 知道 OwnerMap、CN 是纯执行体——不需要新的架构发明。这正是为什么 R-big 这一刀必须先动:没有 router-centric 这层引擎,A 和 B 都没有落脚点

8.4 v25 接论文 §3.3 access-graph cohort planner — 实测半步

R-big 之后我们把 W14 CohortGenerator::PlanCohorts + W16 OwnershipPlanner::Plan 完整 wire 进 router-centric 管线(v25 Phase A→F,6 commits ~855 LOC)。端到端 pipeline 工作,但实测增益受 record_key hash 限制:

指标v14 (Phase R-big stable)v25 (cohort planner)Δ
atomic_per_txn13.5112.91−4.4%
LOCAL hit17%35.8%+110%
UNKNOWN_KG32%0%
commit_rate99.9%99.91%
commit KOPS134130−3%
cohorts published per tick0 (v1 wh-only)169 (sticky cache)new
overlay_kgs per CN0305new

6 阶段 phased rollout

Phase A  wire format v2     gated, atomic=13.43 ≈ v14         ✅ no-regression
Phase B  graph aggregation  IngestV2 + 3 snapshots            ✅ atomic=13.40
Phase C  CN heartbeat       SendAccessSummaryV2 + edges       ✅ ww=1024 wr=1024, atomic=13.39
Phase D  RouterControlLoop  PlanCohorts + OwnershipPlanner    ✅ cohorts emitted
Phase E  OwnerMapSubscriber cohort overlay override           ✅ atomic 13.51 → 12.85
Phase F  MAX_COHORT sweep   64 sweet spot (128/512 collapse)  ⚠️ size cap sensitive

🍎 直觉比喻:W14 cohort merge 是”把高耦合的 KG 揉成一团”,W16 ownership planner 是”按合作距离把团子分给 4 个柜员”。整个 pipeline 工作正确,但 TPCC 的 MakeStockKey = w_id × 10000 + i_idbucket = key % 1024 hash-uniform 跨 wh——揉成的团子里既有 wh=0 的 stock 也有 wh=2 的 stock,无论分给哪个柜员都至少有一半不归他原本管。

🧠 关键洞察access-graph cohort planner 在 TPCC 上的天花板由 record_key 编码决定,不由 planner 算法决定。要把 LOCAL hit 推到 paper §6 期望的 ~85%,必须做 §8.1/§8.2 那两条架构级 lever

  1. client-side cohort-aware dispatch(§8.2 B,~100 LOC)——BenchClient 查 OwnerMap snapshot 把 txn 路由给 cohort owner。
  2. record_key 编码 reshape(结构性改动)——MakeStockKeybucket 高位 = w_id,cohort partition 自动跟 home_wh 对齐。

Lever B 实测(v25 增量)— 负结果

我们把 Lever B 接进 TpccClient:每个 txn 计算 district_kg = MakeKeyGroupId(DISTRICT_TABLE, MakeDistrictKey(home_wh, d_id)),调 rt->OwnerOfKeyGroup(primary_kg) 拿 cohort owner,没覆盖再 fallback RouteByWarehouse(home_wh)(~80 LOC,commit 5a82d49)。

指标v25 finalv25 + Lever BΔ
atomic_per_txn12.9113.00+0.7% ❌
LOCAL hit35.8%25%−30%
KOPS130130same
dispatch19940/19929/20043/20088(均匀)16524/25979/18982/20510(skewedcohort 路由生效
overlay_kgs30521(cohort 集塌缩)feedback loop

🌟 诊断:Lever B 在 TPCC 不奏效是因为 cohort 反馈闭环

1. client 用 district_kg 查 cohort owner(router argmax 决定的)
2. 该 owner 通常 ≠ home_wh 对应的 CN(hash 均匀让 cohort owner 错位)
3. CN 上 access 模式 shift → cohort planner 重新学习
4. router argmax 更加偏向一个 CN(wh_to_cn 收敛到 [1,1,1,1])
5. 更多 cohort 集中给 cn1,但 client dispatch 也跟着集中
6. 反馈:cohort 集变窄(305 → 21)→ LOCAL hit 进一步下降

正确解读:Lever B 是架构上正确的方向——一旦 cohort partition 跟 workload 的 home affinity 对齐,dispatch by cohort owner 就能直接把 LOCAL hit 拉到 cohort 覆盖率。但前提是 cohort owner 真的反映 home affinity,这要求 record_key 编码先 reshape。所以 Lever B 必须跟 record_key 编码 reshape 同时做,单独上 Lever B 反而会触发反馈闭环。

🧠 更深的方法论教训lever 顺序很重要——单独把 v25 access-graph cohort planner 和 client cohort-aware dispatch 任一拿出来,都救不了 LOCAL hit。两者要么都做(搭配 record_key reshape),要么都不做(保持 v14 baseline 13.51)。

互补:v25 的 access-graph pipeline 是 lever 1/2 的前提——cohort owner 这条信息必须先存在 OwnerMap snapshot 里,client/router 才能 dispatch。没有 v25 这一刀,A/B 两条 lever 都没有数据基底。 但单独 v25 也不够——还要 record_key 编码 reshape 才能让 Lever B 真正发力。三件一起做,atomic 才能压到 paper §6 期望的 ~5。

8.5 v25 工程教训(6 条)

教训体现
EWMA-decayed weight 在长跑下塌缩:CN 端 kg_count_[kg] 每 tick × DECAY<1,60s 后值 < 0.005。Router 用 CN 默认 threshold=1.0 让 PlanCohorts 全 drop。修法:router threshold=0.0001 + sticky cacheRouterControlLoop.cc kVertexThreshold=0.0001
greedy union-find size cap 是双刃剑:cap=64 → 61 cohorts × 4 kg = 232 覆盖;cap=128/512 → 1 cohort × 1-7 kg degenerate。merge 链长度对 cohort 多样性敏感kMaxCohortSizeRouter=64 sweet spot
--type=client 不走 server-side gflag setup:BenchRunner.cc 在 client 分支 return 0 直接退出,跳过 AuraRuntime::SetXxx 块。Router-side flag 必须在 client 分支单独 mirrorBenchRunner.cc 在 client 分支补 SetHeartbeatV2Enabled / SetRouterCohortPlannerEnabled
sticky cohort cache 是必需,不是优化:CN heartbeat EWMA-collapsed 时 PlanCohorts emit 0,若直接 publish v1 manifest,Subscriber 会把已 install 的 cohort overlay 清掉 → 间歇性退化。保留 last_cohorts_ 让 cohort partition 粘住RouterControlLoop.h last_cohorts_ field
manifest schema 必须向后兼容:v2 把旧 reserved u32 拆成 (manifest_version u16 + reserved u16),v1 reader 把 version 当 reserved 读掉无害;v2 reader 检测 version 才 parse cohort 段Manifest.cc Parse 容错 v2 cohort 段截断
cohort_id 命名空间必须 disjoint:router 用 CohortIdFor(min_kg)[0, 0x7FFF_FFFF],wh-default 用 (table<<20|bucket) | 0x80000000 高位置 1,确保 OwnerLockTable 不会两套 id 冲撞OwnerMapSubscriber.cc encode_cohort_id

✅ 自我检验清单

  • per-CN 痛点:能列出 per-CN 控制环留下的 2 个结构性问题(race-publish + paper §3 故事不对齐)
  • 谱系映射:能徒手写出 H-Store / VoltDB / Calvin / TiDB / AURA-router-centric 在控制平面 / 执行平面 / 同步机制 3 列上的差异
  • R0–R7 步骤:能说出 R1–R3 单独成段的原因(反向通道是 router-centric 的前提)
  • kRouterCnId:能解释为什么用 0xFFFE 哨兵 ID + sink callback 是”不改 wire protocol 重构成 router-centric”的最小手术
  • 9216 cohort 算账:能算出 9216 entries × 16 B ≈ 144 KB / snapshot,且说明为什么 manifest 不传 cohort 表本身
  • InstallCohort 陷阱:能用一句话说清”代码没错、适用范围变了”——原 per-CN 不漂 epoch,router-centric 每 tick 递增 epoch
  • bug 现象链:能从 17% LOCAL → STALE_EPOCH → 14.00 atomic 不降,一口气说完逻辑链
  • 可观测性 SLO:能解释”8 条 LOG 全亮 ≠ atomic 真下降”,并指出哪种 counter 才是语义正确性 SLO
  • 运维教训:能列出顺序 ssh + journalctl vacuum + ASCII-only stash 三个 deploy 必备工具
  • 下一步 lever:能说清 W9 REMOTE_OWNER takeover 和 client-side dispatch 哪个改 server / 哪个改 client,以及为什么两者互补
  • v25 实测:知道 access-graph cohort planner pipeline 全 wire 后 LOCAL 17%→36%, atomic 13.51→12.91,且能解释为什么增益受限于 MakeStockKey 的 hash 编码
  • v25 6 条工程教训:能默写 EWMA collapse 修法(threshold 0.0001 + sticky cache)、size cap sweet spot(64)、--type=client flag propagation 漏修、cohort_id 命名空间 disjoint

📚 参考资料

概念入门

  • AURA 论文 §3(router-centric 重写, commit d1cde1e) —— 本仓 paper_lock_ownership_cn/sections/3_design_overview.tex:router-centric 架构正文重写,对应本章 §1–§4 的设计动机
  • CIDR’15 OLTP On-Disk vs In-Memory 谱系综述(Pavlo et al.) —— H-Store/VoltDB/Calvin 的设计取舍,本章 §2 谱系表的来源
  • TiDB PD 设计文档 —— PingCAP TiDB Docs:Placement Driver 的工业实现,AURA router 的最近邻

关键论文

  • H-Store(Kallman et al., VLDB’08) —— site coordinator + per-partition single-thread executor 的奠基设计
  • VoltDB(Stonebraker et al., 2010) —— H-Store 商业化,强化”shared-nothing + 单线程 partition”哲学
  • Calvin(Thomson et al., SIGMOD’12) —— deterministic ordering via sequencer,集中编排 OLTP 的极致版
  • TiDB / TiKV(Huang et al., VLDB’20) —— PD + TiKV 元数据漂的工业实现,与 cohort owner 调度直接类比
  • MorphoSys(Liu et al., VLDB’21) —— 物理设计在线学习,AURA 的设计理念上游(路线图主参考)
  • LOTUS(Liu et al., arXiv’25) —— 锁分离的静态版,本章 §2 谱系表”锁权威在 CN”那一格的源头

行业讨论

  • VoltDB v9 Postmortem —— 多次提到 single-threaded executor 在跨 partition 事务上的天然劣势,对应 AURA 52% REMOTE 这条线
  • CockroachDB Range Lease Movement —— pkg/kv/kvserver/replica_proposal.go:lease holder 漂移与 cohort owner 漂移的工程对照

框架文档

  • CREST 源码 src/transaction/aura/ —— OwnerMapSubscriber.cc / OwnerLockTable.cc / OwnerRpcServer.cc / AuraControlLoop.cc:Phase R-big 全部代码改动入口
  • docs/aura/PHASE_R_PLAN.md —— 8 阶段重构原始计划
  • results/aura_phaseR_big_progress/STATUS.md —— v9b 路由分布数据 + InstallCohort fix 提交记录
  • CloudLab small-lan profile —— docs.cloudlab.us:5-node c6525-25g 拓扑,本章 §7.3 运维教训的硬件背景