Skip to content
预计 1 分钟右侧目录可快速定位

多个 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)
  ↓
execute

Claim 至少要同时检查旧状态/版本并写入 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:40

Lease 解决的是 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)      → accept

Fencing 的关键不是 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/terminal

Queue 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

自测 ​

  1. Worker A 的 lease 过期后 B 接管;A 网络恢复后提交晚到写入。为什么只有 lease 还不够,fencing token 要挡住什么?
  2. Heartbeat 延迟与 Worker 真正死亡如何区分?claim/lease 设计必须接受哪种不可避免的不确定性?

Sources ​

AI Engineering · Agent · Harness · Backend Systems