租约、锁与 Fencing Token
租约到期改变协调者状态,不会清除旧工作者的记忆,也不会撤回它的迟到请求。
恢复运行的旧工作者
让租约过期,再决定谁先到达下游。
正在加载互动… 交互需要 JavaScript;正文仍可直接阅读。
学习目标
区分锁所有权与下游写权限,构造单调递增的 fencing token,找到必须原子执行的检查边界。
前置:未知结果与原子更新、租约和进程暂停。
过期持有者仍是一个活进程
A 获得租约锁,读入一些数据,然后暂停。协调者上的租约过期,B 获锁并写入新结果。A 恢复后仍持有旧的本地判断,继续写下游。若下游不检查 token,就可能在 B 的新写之后接受 A 的旧写;锁服务本身并没有必要出错。
A: 获得 token 12 ── 暂停 ───────────────── 恢复 → 旧写 etcd: 锁存在 ─────── 到期 ── 新锁 token 14 B: 获锁 ── 写新值 下游: 记住 14 ── 拒绝 12
Fencing token 为所有权代次排序。接收资源持久保存已接受的最高代次,并把 token 检查与受保护更新原子执行。小于最高代次的请求被拒绝。它不取消暂停的进程,只限制资源接受哪些迟到请求。
具体实现
基线用 etcd Txn 比较 VERSION(lock)=0,创建一个绑定两秒租约的锁键。Token 取键的 create_revision,不是 lease ID。在同一持续运行的集群谱系中,后续成功创建的 revision 更大。下游用 SQLite BEGIN IMMEDIATE,在同一事务中读取最高代次、比较 token、同时更新值与代次。
同一所有权代次内,基线允许相等 token,因此 fencing 不会去重当前持有者自己的重复写,也不为它的并发命令排序。只有见过更新的 token,才会拒绝旧代次。如果 A 在租约过期后、B 尚未向下游展示新 token 前到达,只检查最高代次的下游仍可能接受 A。更强的“当前仍持有锁”语义需要额外协议或交接边界。
从快照恢复或更换集群,可能破坏直接使用 revision 的假设。恢复时必须在复用 token 空间前确保更高的 epoch,或严谨管理 revision 策略。下游最高代次也必须持久保留,所有相关写路径都要执行检查。客户端先检查、再单独写入,不构成原子的 fence。
真实实验 C:迟到的写
A 是独立的 Python 子进程,实际被 SIGSTOP 暂停,不发送续租。运行器轮询到锁键确实不存在,而非假定过了两秒就一定到期。B 随后获得更高 token,向同一 HTTP/SQLite 下游的两种模式写入。运行器恢复 A,让它携带旧 token 向两种模式提交。
无 fencing 模式最终应变成 A-late;带 fencing 模式应拒绝 A 并保留 B-current。检查实际 token、观测到的过期等待与两个响应。协调者和下游请求都是真实执行;上图 token 数字只是示意。
动手:定位 lab/services.py 中的比较逻辑,解释为何把它移出 SQLite 事务会产生竞争。扩展时让 A 在 B 尚未联系下游前写入,说明最高代次为何可能接受它,以及这不违反本实验展示的较窄约定。
自测
Lease ID 可以直接当 fencing token 吗?
不能。Lease ID 标识租约,不是承诺按所有权顺序增长的序列。基线使用特定集群谱系内,原子创建锁键的 revision。
有了 fencing 是否就不需要幂等?
仍需要。相等 token 的重试可能重复副作用;代次新鲜度和操作去重保护不同不变量。
原始资料
- Burrows:The Chubby lock service,§2.4 的 sequencer 与迟到请求。
- etcd API 参考:租约 TTL 与 create_revision。
- etcd 恢复文档:恢复后的 revision 变化。
对照 MIT 6.5840 学习
Lecture 9: Zookeeper · Lab 2: Key/Value Server, lock section
真实租约过期后比较旧代次写;以 Chubby §2.4 补充课程。
python3 scripts/run_all.py --repeats 1 --out build/my-fence-study本 Lab 可运行练习为独立原创,不是所链接 MIT 作业的解答。安装方法见复现页;不代表注册、评分或认证。
检索已核验课程地图 →