租约到期改变协调者状态,不会清除旧工作者的记忆,也不会撤回它的迟到请求。
区分锁所有权与下游写权限,构造单调递增的 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。
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 标识租约,不是承诺按所有权顺序增长的序列。基线使用特定集群谱系内,原子创建锁键的 revision。
仍需要。相等 token 的重试可能重复副作用;代次新鲜度和操作去重保护不同不变量。