生产环境必须用etcd lease选主+cron触发调度请求,而非直接使用robfig/cron/v3等单机定时器,因其无法解决多实例下任务重复执行的分布式协调问题。

直接用 cron 或 time.Ticker 在多实例环境下必然重复执行任务,这不是配置问题,而是架构缺陷——缺少全局执行权协商机制。真正在生产环境跑得稳的方案,必须把「谁来跑」和「什么时候跑」拆开,用 etcd lease 选主 + cron 触发调度请求。
为什么 robfig/cron/v3 不能直接用于分布式场景
robfig/cron/v3 是单机定时器,AddFunc 注册的函数会在每个实例本地无条件执行。K8s 部署 3 个副本,0 0 * * * 就会每天凌晨触发 3 次相同任务,哪怕加了数据库标记也拦不住:节点宕机时锁未释放、事务未提交、网络分区导致状态不一致,都会造成漏跑或双跑。
- 它不感知其他节点存在,也不提供抢占、续租、watch 等分布式原语
- 所有时间解析(如
cron.ParseStandard)都只解决「何时触发」,但没回答「该由谁触发」 - 强行在 AddFunc 里加 Redis SETNX 或 MySQL UPDATE … WHERE status=‘pending’,会因超时设置不合理、未捕获异常、未处理 lease 失效而变成“伪分布式”
etcd lease + watch 实现轻量级选主的核心写法
关键不是存任务,而是抢到 /scheduler/jobs/send_daily_report 这个路径的写入权。只有拿到 lease 的节点才能写入 key,其他节点 watch 它,失效即抢。
-
clientv3.Grant创建 lease,TTL 设为任务预期耗时的 2–3 倍(例如任务通常 15s 完成,TTL 设 45s) - 抢占必须用
clientv3.OpPut配合clientv3.WithLease(leaseID)和clientv3.OpGet的 compare-and-swap(CAS)逻辑,不能只靠 Put 成功与否判断 - 续租要起独立 goroutine,监听
lease.KeepAlive返回的 channel;channel 关闭说明 lease 被 revoke,需立刻退出执行并让出控制权 - watch 必须带
clientv3.WithPrevKV(),否则拿不到旧值,无法区分是自己释放的还是别人抢走的
如何让 cron 只做“触发器”,不干“执行者”
把 cron 当成本地闹钟,只负责每分钟 tick 一次,调用 tryAcquireAndRun("send_daily_report")。这个函数内部完成 etcd 抢锁、执行业务、清理锁全流程。
- 不要在
cron.AddFunc里写doSendDailyReport(),那是最常见且最危险的错误 -
tryAcquireAndRun应返回 bool 表示是否真正执行了任务,便于日志和监控区分“调度发生”和“实际运行” - 任务执行失败时,不要自动重试——重试策略应由调度层统一管理(比如写入 DB 标记 retry_count,由另一个 cron job 扫描重试队列)
- 如果任务本身有幂等性(如发邮件前查 DB 是否已发),仍需配合分布式锁,因为幂等只防重复结果,不防重复资源消耗(如并发调三次 SMTP)
监控必须暴露可抓取的指标,而非仅靠日志
只打 log.Printf("task send_daily_report executed") 是无效监控。Prometheus 需要结构化指标:任务成功/失败次数、延迟直方图、当前持有 lease 的节点 ID。
- 用
prometheus.NewCounterVec统计各任务的执行结果,标签含job_name、status(success/fail)、node_id - 用
prometheus.NewGauge暴露当前 lease TTL 剩余秒数,低于阈值(如 10s)就告警——说明节点可能卡住或网络异常 - HTTP handler 必须用
otelhttp.NewHandler包裹,否则 trace context 断链,无法关联任务调度、etcd 请求、业务执行的完整链路 - worker 节点上报健康状态时,别只 ping 数据库——要验证 etcd 连通性、lease 续租是否正常、任务队列是否积压,这些才是分布式调度的真实瓶颈
最容易被忽略的是 lease 续租失败后的清理动作:goroutine 退出前必须显式 clientv3.Revoke,否则残留 key 会阻塞下一轮抢占;还有就是 watch channel 关闭后没重连,导致节点彻底失联却无感知。这些细节不处理,系统看起来能跑,但一遇网络抖动或节点重启就丢任务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











