第 7 篇 讲过 Put 与 quota;第 10 篇 说明 KeepAlive 不经 Raft。协调层里 CAS、选举、锁 几乎都写成 Txn。误区是把「用了 Txn」等同于「可串行化协调原语」——Jepsen 对 etcd 的证伪边界要分开看:多 key KV Txn 与 compare/mod 路径 vs Lease 会话锁。
本篇钉 TxnRequest 在 v3.5.33
的实现:applyCompares、嵌套
Txn、isTxnReadonly 的 ReadIndex 短路,以及
Jepsen etcd 3.4.3 与 distributed/53
的分工。不写未跑并发测试数字。
本篇在系列中的位置
篇目 核心内容 第 10 篇 · Lease 与 KeepAlive TTL、checkpoint、Leader 切换 第 11 篇 · Txn 与并发语义 compare/mod、Jepsen 边界 第 12 篇 · Compaction 与容量 auto-compaction、quota、alarm 系列目录 五轴、阅读路径
版本锚定:etcd v3.5.33(tag
v3.5.33)。对齐server/etcdserver/apply.go(Txn、applyCompare)、server/etcdserver/v3_server.go(Txn入口)@ etcd-io/etcd v3.5.33。Jepsen 结论引用 etcd 3.4.3 报告(B 级边界,非 3.5.33 逐条复现)。无 Jepsen 重跑则不粘贴操作历史或 checker 输出。
一、Txn 在 API 层的两条路径
客户端 Txn 含
Compare(谓词)与
Success/Failure(RequestOp
列表,可嵌套 Txn)。
EtcdServer.Txn(v3_server.go)分叉:
| 类型 | 判定 | 路径 |
|---|---|---|
| 只读 Txn | Success/Failure 仅含 RequestRange |
默认 linearizableReadNotify →
doSerialize → applyV3Base.Txn
不经 propose |
| Serializable 只读 | 所有 Range 带 Serializable: true |
跳过 ReadIndex |
| 写 Txn | 含 Put/DeleteRange/嵌套写 | raftRequest(Txn) → 单条
Raft entry → apply |
写 Txn 与 Put 共享 单 Raft 组单写者
槽位(第 2、6 篇);compare 失败时仍占一条 log entry(mod
为空),revision 可能 不变 或仅 bump
header——以 applyTxn 是否产生
changes 为准。
二、compare:MOD、CREATE、VERSION 与空 key
compareToPath 先对 rt.Compare
调用 applyCompares;全真走 Success 分支,否则
Failure。嵌套 Txn 递归展开 txnPath []bool。
applyCompare(apply.go)对每个
Compare:
Range(key, rangeEnd)读 当前 ReadView revision 的 KV;- 无 KV 时:
Compare_VALUE恒 false;其他 target 与 零值 KV 比较; - 有 KV 时:对 range 内 每个 KV 调用
compareKV,全满足才 true。
compareKV 支持的
Target:
| Target | 比较字段 | 典型用途 |
|---|---|---|
| VALUE | bytes.Compare(value) |
值 CAS |
| CREATE | CreateRevision |
key 是否存在 / 创建世代 |
| MOD | ModRevision |
乐观锁、fencing(ModRevision 作 token) |
| VERSION | Version |
逻辑写次数 |
| LEASE | Lease |
绑定租约 ID |
Result 支持 =,
!=, >,
<(Compare_EQUAL
等)。clientv3 的 If(Compare(...))
是同一 protobuf 的封装。
fencing 惯用法(distributed/51):锁
Txn 成功后取 Header.Revision 或 key 的
ModRevision 作 token;写资源时
Compare(ModRevision("<resource>"), "<", token),拒绝
stale holder 的写入——这是 应用层协议,不是
etcd 自动提供 lock 语义。
三、applyTxn 与 MVCC 可见性
applierV3backend.Txn 流程:
- 只读阶段:
Read(ConcurrentReadTxMode)或实验性SharedBufReadTxMode上执行 compare + checkRequests(预检查 range 权限与 revision); - 若写 Txn:
txn.End()后KV().Write()开启真实写事务——注释强调写 serialized on raft loop,ReadView revision 即本次写 txn revision; applyTxn执行 Success/Failure 分支中的 Put/Delete/Range;- 写失败
panic(
unexpected error during txn with writes),只读失败仅 log——体现 写 Txn 不可部分成功 的不变量。
Txn 内 Range 读到的是 apply 时刻 MVCC 快照;与 Percolator 两阶段不同,etcd 无跨 key 锁表,靠 Raft 日志全序 + 单 store 写者保证 原子 apply。
嵌套 Txn 的 compare 按路径逐段求值;深度过大时成本线性于 compare 次数 × range 宽度(源码 TODO 提到 range 分块优化)。
四、Jepsen 边界:什么被证伪、什么没有
distributed/53 · Jepsen 方法论 第 5.5 节与 Raft 深度重写 均引用 Jepsen etcd 3.4.3(2019–2020 报告链):
| 子系统 | Jepsen 结论(3.4.3) | 工程含义 |
|---|---|---|
| KV 读/写、多 key Txn | Knossos 等检查器下 strict-serializable(默认线性一致读路径) | compare/mod 协调 在 KV 模型内 与 Raft 一致 |
--serializable 读 |
允许 stale | 与文档一致;不可用于锁验证 |
| Lease 锁 / 会话 | 不安全 子集(clock、partition 组合) | concurrency.Mutex 需 fencing +
业务校验 |
| corrupt-check × Watch | 特定配置竞态(3.4.3) | 3.5.x 修复链;生产慎开实验 flag |
版本边界:Jepsen 钉 3.4.3,本篇机制钉 3.5.33——不能把 3.4.3 的 bug 列表原样当作 3.5.33 未修,也不能假设 3.5.33 已独立重跑 Jepsen。合理写法是:KV Txn 路径的设计目标仍是 linearizable + Raft 全序;Lease 轴另列。
与 TiKV Percolator(tikv-htap)对照:etcd Txn 是 单 Raft 组上的轻量 CAS 批,无分布式锁服务、无 GC 事务 record——选型见第 16 篇。
五、并发语义速查表
| 场景 | 推荐机制 | 避免 |
|---|---|---|
| 选主 / 抢锁 | Txn + CREATE=0 或 MOD 比较 + Lease | 无 Lease 的永久 key |
| 保护共享资源写 | MOD revision fencing | 只 Compare VALUE |
| 读最新配置 | 线性一致 Get 或写 Txn 只读 |
Serializable 默认误用 |
| 批量条件更新 | 单 Txn 多 Put | 客户端多次 Put 无 compare |
| Leader 租约 | Lease + KeepAlive | 把 KeepAlive 当 Txn |
开放争论:strict-serializable 来自 黑盒历史检验,不覆盖 所有 client 库用法(例如本地缓存 revision、错误重试序);DST 路径见 distributed/54,与 Jepsen 互补(53 篇表格)。
六、谱系与开放问题
Lamport:线性一致(1979)→ Herlihy & Wing 形式化(1990)
→ Jepsen:历史检验 strict-serializable(Kingsbury)
→ etcd:Raft 全序 + MVCC apply → KV Txn 通过 3.4.3 检验
→ Lease 锁:会话 + 时钟 → 不在同一检验子集
→ 开放:K8s 全链路(apiserver + watch cache)无统一 Jepsen 模型
本篇不写什么:client/v3
concurrency 包逐函数走读;Jepsen Clojure
源码;未重跑的 3.5.33 Jepsen 数字。
参考资料
规范 / 官方文档 / 源码(A)
- etcd-io/etcd
v3.5.33:server/etcdserver/apply.go(Txn、applyCompare、compareKV);server/etcdserver/v3_server.go;api/v3/etcdserverpb/rpc.proto(Compare、TxnRequest) - etcd v3.5 · Transaction
论文 / 测试报告(A/B)
- Herlihy & Wing, Linearizability: A Correctness Condition for Concurrent Objects, TOPLAS 1990
- Kingsbury, K., Jepsen: etcd 3.4.3(B:版本 3.4.3;KV strict-serializable;锁不安全)
站内对照
- distributed/53 · Jepsen 方法论
- distributed/51 · 分布式锁
- distributed/13 · Raft 深度 · Jepsen 小节
- PLAN.md §九篇 11
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【etcd】生产全景:缺口、五轴坐标系与 16 篇路线
相对 distributed/50、39、13 钉清 etcd 生产内核缺口;定义 Raft/WAL/MVCC/Watch/Lease 五轴排障坐标系,给出 16 篇阅读路线与 K8s 控制面耦合指针。版本锚定 v3.5.33。
【etcd】MVCC 数据模型:Revision、keyIndex 与 generation
拆解 etcd v3.5.33 的 Revision (main, sub) 全序、keyIndex/generation 生命周期与 treeIndex 分工;简要对照 v2 平面键空间,交代 MVCC 与 Watch/compaction 的语义基础。
【etcd】Watch 机制:watchableStore、synced/unsynced 与 ErrCompacted
钉 etcd v3.5.33 watchableStore 的 synced/unsynced/victims 三分法、Apply 后 notify 与历史追赶 syncWatchersLoop,以及 CompactRevision 触发 ErrCompacted 时的客户端重同步边界。
【etcd】Compaction、Defrag 与容量:quota、alarm 与 SLO
钉 etcd v3.5.33 MVCC compaction 与 bbolt defrag 的分工、auto-compaction 保留窗口、quota-backend-bytes 与 NOSPACE alarm 写入门,以及 ErrCompacted 如何写进 Watch SLO。