第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 没降的真因
第 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 控制平面留下的两个问题
- 2. 设计映射:AURA-router-centric 在 OLTP 谱系里的位置
- 3. 8-phase 重构地图:R0 → R7 的步骤拆解
- 4. CN → router 的反向数据流:ACCESS_SUMMARY 上报与 kRouterCnId 哨兵
- 5. 9216 cohort 全表展开:从 wh-kg 到 per-bucket × 9 表
- 6. InstallCohort 幂等性陷阱 —— LOCAL 命中 17% 但 atomic 没降的真因
- 7. 端到端实测:v7/v8/v9b/v10 链条 + 路由分布数据
- 8. 下一个 lever:W9 REMOTE_OWNER takeover 与 client-side cohort 路由
- 自我检验清单
- 参考资料
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 / VoltDB | site coordinator | per-partition single-thread executor | command pipe | partition 严格静态,DM 拷不动数据 |
| Calvin | sequencer(deterministic ordering) | per-shard executor | 全序日志 | 全序需求过强,不利于 OCC |
| TiDB / TiKV | PD (Placement Driver) | TiKV / KV node | gRPC + Raft | 分片元数据可漂,与 cohort owner 漂移直接对应 |
| F1 / Spanner | spanserver master | data shard worker | Paxos group + TrueTime | 强一致 + 全球时钟,DM 用不上 |
| AURA-router-centric(本章主题) | BenchClient(内嵌 OwnerRpcServer + RouterControlLoop) | CN(OwnerMapSubscriber + OwnerLockTable) | memcached pull + OwnerRpc reverse channel | cohort 动态、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 阶段拆解
| 阶段 | 目标 | 关键改动 |
|---|---|---|
| R0 | baseline 数据 + 重构计划 | docs/aura/PHASE_R_PLAN.md 制定 8 阶段方案 |
| R1 | CN 端反向通道哨兵 | AuraTypes.h 加 kRouterCnId = INVALID_CN - 1 = 0xFFFE |
| R2 | CN 上报 access summary | AuraControlLoop heartbeat 末尾 unicast SendAccessSummary(kRouterCnId, items) |
| R3 | router 端接收 | OwnerRpcServer::SetAccessSummarySink(cb) + GetSinkSnapshot() |
| R4 | router 端聚合 + 发布 | AggregatedAccessGraph(Ingest/ArgmaxOwner)+ RouterControlLoop 100 ms tick 写 memcached |
| R5 | CN 拉 manifest | OwnerMapSubscriber(libmemcached pull,100 ms refresh) |
| R6 | CN 退出自发布 | AuraControlLoop::PublishManifest() 在 IsRouterCentric() 时 early-return |
| R7 | M6 runner + 端到端 smoke | bench/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 b 归 wh_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::Serialize 把 wh_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 路径):
| Counter | n=50000 | n=100000 |
|---|---|---|
| local | 4475 (9%) | 16521 (17%) |
| remote | 13871 (28%) | 51825 (52%) |
| unknown | 31655 (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, ..., 280 调 InstallCohort(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 怎么办?
- CN 仍是该 bucket 的 owner:本次 manifest tick 中
wh_to_cn[b % n_wh]没换人,owner 没变;EvictCohort 只清掉旧 epoch state,立刻用新 epoch 重装 - TryLock 失败回退原本就走 MN CAS:即便有微秒级 race,未完事务回退到 atomic 路径不丢一致性(这是第 4 章不变式 I4)
- 真要换 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 进度表
| smoke | commit | 验证点 | 结果 |
|---|---|---|---|
| v6 | 5567afc | R5 pipeline 启动 | CN0 acquire 命中 0%(wh-kg only) |
| v7 | b851801 | 8 组件 LOG_INFO 全有 | OwnerMapSubscriber applied epoch=N 持续增长 |
| v8 | 7ea956b | per-bucket × 9 表 install | LOG total_cohorts=9216 local=2304(每 CN 拿 1/4) |
| v9b | 9f9ee41 | 路由分布 counter | LOCAL=17% / REMOTE=52% / UNKNOWN=32%(n=100 k) |
| v10 | 7dbfb61 | EvictCohort+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_txn | 13.51 | 12.91 | −4.4% |
| LOCAL hit | 17% | 35.8% | +110% |
| UNKNOWN_KG | 32% | 0% | ✅ |
| commit_rate | 99.9% | 99.91% | ✅ |
| commit KOPS | 134 | 130 | −3% |
| cohorts published per tick | 0 (v1 wh-only) | 169 (sticky cache) | new |
| overlay_kgs per CN | 0 | 305 | new |
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_id 让 bucket = 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:
- client-side cohort-aware dispatch(§8.2 B,~100 LOC)——BenchClient 查 OwnerMap snapshot 把 txn 路由给 cohort owner。
- record_key 编码 reshape(结构性改动)——
MakeStockKey让bucket高位 =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 final | v25 + Lever B | Δ |
|---|---|---|---|
| atomic_per_txn | 12.91 | 13.00 | +0.7% ❌ |
| LOCAL hit | 35.8% | 25% | −30% ❌ |
| KOPS | 130 | 130 | same |
| dispatch | 19940/19929/20043/20088(均匀) | 16524/25979/18982/20510(skewed) | cohort 路由生效 |
| overlay_kgs | 305 | 21(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 cache | RouterControlLoop.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 分支单独 mirror | BenchRunner.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=clientflag 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 运维教训的硬件背景