816 words
4 minutes
RL-Scheduler 中的 Master-Worker 机制可用性分析
Master-Worker 可用性评估(修正版)
一、3 个 Redis Key 的作用
任务分派时,Master 通过 tryDispatchQueuedTaskToWorker() 依次设置:
┌───────────────────────────────┬────────────────────────────┬──────┬─────────────────┐ │ Key │ 设置时机 │ TTL │ 作用 │ ├───────────────────────────────┼────────────────────────────┼──────┼─────────────────┤ │ worker:{id}:task=taskI │ tryPreemptWorker() Lua │ 120s │ Worker │ │ │ 脚本原子写入 │ │ 正在处理哪个任务 │ ├───────────────────────────────┼────────────────────────────┼──────┼──────────────────┤ │task:{taskId}:workerId=workerId│ registerTaskOwner() │ 120s │ 任务被哪个 Worker │ │ │ │ │ 认领 │ ├───────────────────────────────┼────────────────────────────┼──────┼──────────────────┤ │ worker:{id}:hb │ Worker 心跳时续期 │ 30s │ Worker 是否存活 │ └───────────────────────────────┴────────────────────────────┴──────┴──────────────────┘二、分派时序(正常流程)
1. tryPreemptWorker() → 原子写入 worker:{id}:task = taskId (TTL 120s) 2. registerTaskOwner() → 写入 task:{taskId}:workerId = workerId (TTL 120s) 3. dispatchTask() → 通过 Netty 发给 Worker 4. DB status → PENDING 更新为 RUNNING三、Worker 宕机场景
Worker 崩溃 → 不再发送心跳 → worker:{id}:hb 在 30s 后过期 → RunningTaskRecovery (每5s) 发现: - task:{taskId}:workerId 存在 ✓ (任务曾被分派) - worker:{id}:hb 不存在 → Worker 已死 - worker:{id}:task 存在 → 任务孤立 → 标记 PENDING + releaseTaskOwner() → PendingTaskReconciler (每2s) 发现 idle worker → 重新分发
恢复延迟:30s (心跳TTL) + 5s (扫描间隔) ≈ 35s四、Master 宕机场景(核心)
场景 A:Master 在步骤 1-2 之间宕机(未设置 taskOwnerKey)
宕机前只设置了 worker:{id}:task DB 状态 = RUNNING(已在 dispatchTask 前更新) 重启后 reconstructWorkerTasksFromRedis(): - 发现 worker:{id}:task = taskId - 发现 task:{taskId}:workerId 不存在或不匹配 → 标记 PENDING + 入队场景 B:Master 在步骤 2-3 之间宕机(已设置 taskOwnerKey 但未收到 Worker 响应)
宕机前设置了 task:{taskId}:workerId + worker:{id}:task 但 DB status = RUNNING 未变(dispatchTask 发送失败或未处理) 重启后: - worker:{id}:task 存在 - task:{taskId}:workerId 存在且匹配 - RunningTaskRecovery: hb 存在则跳过(Worker 还在跑或刚完成) - PendingTaskReconciler: worker:{id}:task 存在则不视为 idle 最终由 Worker 心跳或 checkAndFixStaleRunningTasks() 清理场景 C:Master 在设置 taskOwnerKey 之前宕机(只设置了 Lua 脚本的 worker:{id} )
宕机前只设置了 worker:{id}:task task:{taskId}:workerId 不存在 DB status = RUNNING(dispatchTask 前已更新) 重启后 reconstructWorkerTasksFromRedis(): - worker:{id}:task 存在 - task:{taskId}:workerId 不存在或不匹配 → 标记 PENDING + 入队五、关键机制总结
┌─────────────────────────────────┬──────────────────────────────────────────┬────────────────────┐ │ 组件 │ 职责 │ 扫描频率 │ ├─────────────────────────────────┼──────────────────────────────────────────┼────────────────────┤ │ RunningTaskRecovery │ 检测 Worker 宕机导致的任务孤立 │ 每 5s │ ├─────────────────────────────────┼──────────────────────────────────────────┼────────────────────┤ │ reconstructWorkerTasksFromRedis │ Master 重启时恢复被中断的分派 │ @PostConstruct │ │ │ │ 一次 │ ├─────────────────────────────────┼──────────────────────────────────────────┼────────────────────┤ │ PendingTaskReconciler │ 将 PENDING 任务重新入队给 idle worker │ 每 2s │ ├─────────────────────────────────┼──────────────────────────────────────────┼────────────────────┤ │ checkAndFixStaleRunningTasks │ 心跳时检测 通知丢失导致的任务卡住 │ 每次心跳 │ ├─────────────────────────────────┼──────────────────────────────────────────┼────────────────────┤ │ 心跳续期 │ renewTaskOwnerTtl() 保持 taskOwnerKey │ 每次心跳 │ │ │ 不过期 │ │ └─────────────────────────────────┴──────────────────────────────────────────┴────────────────────┘六、3 Key 的最终一致性保证
无论 Worker 宕机还是 Master 宕机:
Worker 宕机: worker:{id}:hb 过期 → RunningTaskRecovery 检测 → PENDING + 重新入队
Master 宕机(未收到完成通知): 1. 重启时 reconstructWorkerTasksFromRedis() 修正不一致状态 2. 或者 Worker 心跳时 checkAndFixStaleRunningTasks() 发现 taskKey 没了但 ownerKey 还在 → 标记 COMPLETED七、当前设计的限制
- 故障检测延迟:Worker 宕机后约 35s 才能发现(30s TTL + 5s 扫描)
- Master 重启恢复:依赖 @PostConstruct 一次性重建,不保证 Master 运行时实时修复所有不一致
- channelInactive() 有 TODO 未完成:MasterHandler.java:59-63 未触发恢复逻辑,但依赖心跳 TTL 兜底
- 无真正零宕机切换:Master 宕机期间任务状态更新请求会丢失,依赖 Worker 侧重试或最终一致性
RL-Scheduler 中的 Master-Worker 机制可用性分析
https://sgjki547.top/posts/master-worker-avaliable/