一项任务原本由执行者 A 负责。A 暂停超过租约期限,协调者把任务交给 B;A 随后恢复,继续提交自己算出的旧结果。此时,协调者已经换了负责人,但写入目标是否知道这次接管?
心跳用于判断执行者是否可能失联,租约用于约定它持有任务的期限。两者都不能撤销 A 已拿到的连接、文件句柄或在途请求。要阻止接管后的旧写入,还需要写入目标检查一个递增的持有者编号,也就是 fencing token,并拒绝旧编号。
本文围绕任务接管解释这些机制,用 Pekko Typed 1.1.3 的单进程模型验证协议反例。模型没有实现真实的多机任务调度器,验证范围与生产系统所需条件会分别说明。
接管前,先让写入目标拒绝旧编号
协调者使用自己的单调时钟判断租约是否到期,不相信 Worker 自报时间。新持有者获取一个更大的 token;授予它之前,必须先在写入目标推进 fence。写入目标在同一个原子操作中检查 token 与写入数据。
若只在应用层先读取 fence 再写数据,两步之间可能发生接管,旧写入仍能穿过检查。目标端必须支持条件更新、事务或等价的原子检查机制。如果目标不支持这种能力,协调者的心跳协议不能凭空提供它。
A 持有 token=1,暂停
协调者判断 A 租约到期
目标端 fence 推进到 2
B 收到 token=2
A 恢复并提交 token=1 → 目标拒绝
租约到期不等于旧写入立即失效
仅 TTL 到期、还没有新的持有者时,目标端的 fence 仍为 1。因此旧 token=1 的写入仍可能被接受。fencing 防止旧持有者覆盖已经接管的新持有者,不自动提供“墙钟到某一刻所有旧写入立刻失效”的保证。若业务需要绝对的租约有效期检查,写入目标还必须参与期限协议,并处理时钟与提交时刻的边界。
实验刻意检查这一反例,再接管到 token=2 后检查旧写入被拒绝,避免把两种保证混为一谈。
检查编号与写入必须是同一个原子操作
final class FencedSink {
private var fence = 0L
private var results = Map.empty[String, String]
def advance(): Long = synchronized { fence += 1; fence }
def write(token: Long, resultId: String, payload: String): Boolean = synchronized {
if (token != fence) false
else results.get(resultId) match {
case Some(old) => old == payload
case None => results += resultId -> payload; true
}
}
def snapshot: Map[String, String] = synchronized { results }
}
这段 synchronized 只表达一个原子边界:比较 token、检查 resultId、写入结果必须一起发生。相同 resultId 和相同 payload 视为重放;相同 ID 却携带不同 payload 拒绝。幂等键应包含业务任务身份,不能直接用随机请求 ID 代替。
实验目标只有一个资源,因此 fence 是单值。多资源场景需要每个资源独立的代次与所有权,不能让另一个无关任务的接管使当前任务全部失效。token 要由可信协调机制分配,不接受 Worker 随意提供“更大的数字”。
编号持久化与申请重试属于真实协议的一部分
完整模型中的 Acquire 在有效租约内拒绝其他申请;Renew 必须同时匹配 owner incarnation 与 token,且租约仍有效。过期 Renew 无法复活租约。
Registry 重启实验保留同一个 FencedSink 对象,所以 token 继续递增,旧写入被拒绝。这个结果只验证“状态放在哪里”的差别,不证明进程重启后的持久性。真实服务必须持久化目标端 fence,或者在有一致性保证的协调存储里分配代次;Coordinator 自己的内存计数器重启归零是不安全的。
远程请求还要处理身份认证、授予回执丢失后的重试、协调者双主、重复提交和网络分区。本文的 Acquire 不实现网络级幂等重试;真实协议应记录申请 ID 与授予结果,避免回执丢失后重复分配。这里也没有把同步 sink 调用包装成真实远程存储客户端。
用可控时钟验证接管前后的写入行为
LeaseFencingDemo.scala 通过可控时钟验证:TTL 临界点续租被拒;仅过期而未接管时旧写入仍可达;接管后旧写入被拒;同一结果重放成功、冲突载荷失败;Registry 重启后目标端代次不重置。
源码使用 Java 17、Scala 2.13.16、Pekko Typed 1.1.3,运行方式见 源码包。这些断言验证单进程模型中的接管和写入规则;分布式部署还需要持久存储、网络协议和故障测试。单个执行者内部的结果乱序问题,见 异步任务的结果顺序与并发上限。