直接用 cron + http 无法保障任务唯一执行、容错恢复与状态一致性,因单机 cron 易丢任务且无节点存活感知;裸 http 缺乏重试、并发控制与状态跟踪。需依托 etcd 的 txn 原子操作、lease 心跳与 watch 机制实现任务去重、自动漂移与闭环调度。

为什么不能直接用 cron + HTTP 调用分发任务
因为单机 cron 无法保证任务只执行一次,节点宕机后 pending 任务就丢了;裸写 HTTP 调用又没失败重试、状态跟踪、并发控制,更无法感知 worker 是否存活。微服务场景下,你真正需要的是:任务可发现、节点可伸缩、失败能自动漂移,且不依赖外部数据库或消息队列——所以得把协调逻辑收拢到调度器内部,用轻量心跳 + 状态同步替代强一致中间件。
etcd 的 /tasks/pending 和 /tasks/running 必须用 Txn 写入
常见错误是直接 client.Put 提交任务,导致竞态:两个 scheduler 同时读到同一条 pending 任务,都把它挪进 /tasks/running/{id},结果任务被重复执行。正确做法是用 Txn 做原子判断+写入:
- 先检查
/tasks/pending/{id}是否 still exists(用Compare检查 Revision) - 再
Put到/tasks/running/{id}并Delete原 pending key - 整个操作必须在一个
Txn中完成,否则无法闭环
漏掉 Revision 检查,等于放弃幂等性底线。
worker 注册时 Lease 续租必须用 clientv3.NewKeepAliveChannel
手动调 Lease.KeepAlive 容易出错:goroutine 泄漏、续租失败未重试、context 取消后没 cleanup。用 clientv3.NewKeepAliveChannel 能自动处理断连重连和心跳保活,但要注意:
- 注册路径必须带 Lease ID,例如
/workers/{id},且该 key 的 TTL 设为 15s - worker 启动后立即 watch
/tasks/assigned/{worker_id},别等第一次心跳成功才开始监听 - watch 返回的
ctx.Done()事件必须触发本地 task 清理,防止幽灵执行
gRPC ReportResult 接口必须支持 context.WithTimeout
worker 执行完任务后调 ReportResult 上报,但 scheduler 可能正卡在 DB 写入或 etcd Txn 中。若不设超时,worker 会 hang 死,后续任务全堵住。实操要点:
- worker 端调用前必须套
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - scheduler 端实现里,所有 etcd 写操作(如更新
/tasks/history/{id})都要加ctx传递,一旦超时直接 return 错误 - 上报失败不能静默丢弃——要 fallback 到本地 retry 队列,最多重试 3 次,否则任务状态永远卡在 running
最常被忽略的是:scheduler 故障恢复后,必须扫描 /tasks/running/ 下所有无更新时间戳(比如 >5 分钟)的 key,并主动触发重调度——这个兜底逻辑不在 gRPC 流程里,得单独起协程做。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











