分布式任务调度需解决单机cron无法保证仅执行一次、宕机丢任务,以及裸http调用缺乏重试、状态跟踪与并发控制等问题;核心要求是任务可发现、节点可伸缩、失败自动漂移,且不依赖外部中间件。

为什么不用 cron + HTTP 调用就直接上分布式调度?
因为单机 cron 无法保证任务只执行一次,节点宕机后任务就丢了;而裸写 HTTP 调用又没失败重试、状态跟踪、并发控制。真正需要的是:任务注册可发现、执行节点可伸缩、失败能自动漂移、且不依赖外部数据库或消息队列——所以得自己收拢核心逻辑,把 ZooKeeper/Etcd 替换成基于内存+轻量心跳的节点管理,把 Quartz 那套削薄到只剩 Job、Scheduler、Executor 三层。
如何用 Go 做去中心化节点发现与选主?
不引入 Etcd,就靠 UDP 心跳 + 简单选举协议。每个节点启动时广播 JOIN 包,监听其他节点的 HEARTBEAT;所有节点维护一份 nodeList,按 IP+端口哈希算出「当前 leader」——不是强一致选主,而是「多数节点看到同一个 leader 就接受它」。关键点:
-
leader只负责分发任务(从本地队列 pop 出Job,按一致性哈希选一个在线worker发送) - 非 leader 节点定时向 leader 提交自己的负载(CPU/正在运行 job 数),leader 用这个做加权轮询
- 如果连续 3 次收不到某节点心跳,就把它从
nodeList移除,并把分配给它的未完成任务标记为FAILED - 避免使用
time.Now().UnixNano()做超时判断——不同机器时钟偏差会导致误判,改用收到心跳包时附带的单调递增序列号
Job 执行必须支持上下文取消和幂等重入
网络抖动会让 leader 重复下发同一个 jobID,worker 必须能识别并拒绝二次执行。实现上:
- 每个
Job带唯一jobID和execID(由 leader 生成,形如"job-abc123-exec-456") - worker 执行前先查本地内存缓存
execMap(map[string]bool),命中则直接返回ALREADY_EXECUTED - 执行中必须接收
context.Context,并在 HTTP handler 或 goroutine 内定期检查ctx.Err() != nil,及时退出 - 禁止在
Job.Run()里起长期 goroutine 而不绑定ctx——否则节点重启时任务还在后台跑,变成幽灵进程
怎么让任务定义既灵活又不侵入业务代码?
别要求用户实现某个 interface 或继承 base struct。直接约定:任何函数只要签名是 func(context.Context, map[string]interface{}) error,就能注册为任务。注册时用反射提取函数名作 jobType,参数结构体字段 tag 标注 json:"param_name" 即可被调度器解析。示例:
func SendEmail(ctx context.Context, p map[string]interface{}) error {
to := p["to"].(string)
subject := p["subject"].(string)
return smtp.Send(to, subject)
}
// 注册:scheduler.Register("send_email", SendEmail)
这样业务方改任务逻辑只需改函数体,不用动调度层代码;新增任务也不用重启服务,支持热注册(通过 HTTP POST /v1/job/register 提交函数名和元数据)。
真正难的不是分发,是当 worker 进程 panic 后怎么把 panic 日志连同 jobID、输入参数一起上报——这部分容易漏掉 recover 包裹,导致失败无声无息。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











