Appearance
多个 Worker 抢同一个 Run 怎么办:Claim、Lease、Heartbeat 与 Fencing
前置:Durable State;Queue / Worker Delivery Semantics
核心问题:Worker A 的 lease 已过期、Worker B 已接管后,为什么仅有 lease 还不能阻止 A 的晚到写入,Fencing 要验证什么?
先看一个会重复执行的场景
Run 的 Node deploy-prod 已经进入 READY。Worker A 从队列拿到任务,刚把部署请求发出去就发生长时间 GC;这时 Worker B 也拿到了同一个 Node。
如果系统只有一个 status = RUNNING 字段,却没有“谁现在有权提交这个 Node”的概念,A 和 B 都可能继续执行。最坏结果不是日志重复,而是部署、支付、发邮件等真实副作用发生两次。
所以多 Worker Runtime 需要在 Node 状态之外再建立一层 execution ownership。
从 Claim 到 Lease
Claim:先取得执行资格,再真正执行
Scheduler 找到 READY Node 后,不应直接调用 Tool,而是先做一次原子 claim:
text
READY
↓ atomic claim
RUNNING(owner=worker-A, attempt=3)
↓
executeClaim 至少要同时检查旧状态/版本并写入 owner、attempt、lease expiry。两个 Worker 同时竞争时,只能有一个更新成功。
Lease:Ownership 不能永久存在
如果 Worker A claim 后直接宕机,永久锁会让 Node 永远卡在 RUNNING。Lease 给 ownership 一个到期时间:Worker 需要定期 heartbeat 延长 lease;长时间没有 heartbeat,系统才允许重新领取。
text
owner = worker-A
lease_until = 10:30:20
heartbeat → 10:30:30
heartbeat → 10:30:40Lease 解决的是 owner 消失后如何重新获得进展,它不是 exactly-once 保证。旧 Worker 可能只是网络隔离,并没有真的死。
为什么还需要 Fencing
假设 A 的 lease 已过期,B 成功取得新 lease;几秒后 A 从卡顿中恢复。如果 A 仍拿着旧凭证写 StateStore 或调用受保护资源,就出现“僵尸 Worker”。
因此每次 ownership 变更都应产生单调递增的 fencing token / execution epoch:
text
A claim → epoch 41
A expires
B claim → epoch 42
A late commit(epoch=41) → reject
B commit(epoch=42) → acceptFencing 的关键不是 token 长什么样,而是所有会接受执行提交的关键边界都验证最新 epoch/version。只在内存里比较没有意义。
Heartbeat 记录“还活着”,不是“已经成功”
Heartbeat 只能证明 Worker 最近还在推进,不能替代 Tool execution record。例如一个 20 分钟数据处理 Tool 可以定期上报:
text
attempt=3
progress=62%
last_heartbeat_at=...但最终成功仍必须由确定性的 Tool/Node completion record 提交。不要因为 heartbeat 正常就把 Node 推进为 SUCCEEDED。
Shadow Harness 中的数据边界
最小 ownership record 可以是:
python
@dataclass
class NodeLease:
run_id: str
node_id: str
attempt: int
owner_id: str
epoch: int
lease_until: datetime典型流程是:
text
Scheduler sees READY
→ StateStore.try_claim(node, expected_version)
→ Worker receives lease(epoch)
→ heartbeat while long work runs
→ Executor commits result with same epoch
→ StateStore verifies epoch + transition
→ release/terminalQueue message 本身不要成为 ownership authority。At-least-once Queue 可以重复投递;真正谁有执行权由 StateStore/lease record 决定。
Failure Modes
只用 Redis/Queue 的“拿到消息”当作 ownership
消息被重新投递、consumer 重启或 visibility timeout 到期后,第二个 Worker 可能再次拿到任务。
处理: 消息只触发 claim;Node 的 durable state + lease/epoch 才决定是否可以执行和提交。
Lease 过期后旧 Worker 仍能提交
只有 TTL 没有 fencing 时,过期 Worker 恢复后仍可能写入晚到结果。
处理: 每次 claim 生成新的 epoch/version,所有 completion/update 都携带并校验它。
如何验证
| 场景 | 必须成立 |
|---|---|
| 两个 Worker 同时 claim 同一 READY Node | 只有一个 claim 成功 |
| owner crash、lease 到期 | 新 Worker 可以重新 claim |
| 旧 Worker 在新 lease 后恢复并提交 | stale epoch 被拒绝 |
| Queue 重复投递同一 Node | 不产生两个有效 execution owner |
自测
- Worker A 的 lease 过期后 B 接管;A 网络恢复后提交晚到写入。为什么只有 lease 还不够,fencing token 要挡住什么?
- Heartbeat 延迟与 Worker 真正死亡如何区分?claim/lease 设计必须接受哪种不可避免的不确定性?