根本原因是cron默认不防重叠、不控执行时长、不校准时区;耗时任务堆积导致调度偏移,时区错位(如容器中fallback到utc)引发最大漂移,需用delayifstillrunning()和显式withlocation()解决。

为什么 cron 任务会越跑越晚
根本原因不是 cron 库“不准”,而是它默认不防重叠、不控执行时长、不校准时区。一个耗时 8 秒的任务配 "*/5 * * * *"(每 5 分钟一次),若某次执行卡住,下一次调度仍按原计划触发——但此时前序任务还没结束,新 goroutine 又拉起,时间戳不断累积偏移,几分钟后就差出一两分钟。
用 cron.WithChain(cron.DelayIfStillRunning()) 防堆积
这是最直接有效的漂移控制手段。它让 cron 在发现上一个 job 还没结束时,跳过本次调度,而不是并发拉起新实例。
-
cron.DelayIfStillRunning()不是“等它结束再执行”,而是“跳过本次”,避免雪崩 - 必须配合
cron.Recover()一起用,否则 panic 会导致整个链失效 - 慎用
cron.SkipIfStillRunning():它会丢弃任务,适合通知类;而DelayIfStillRunning更适合状态同步类(如 DB 刷数) - 参数可选:
cron.DefaultDelay是默认延迟阈值(10ms),也可传自定义 duration
时区错位才是最大漂移源,别信 time.Local
本地开发看到“每天 9:00 执行”没问题,一上容器就变成 UTC 时间——因为多数镜像没装 tzdata,time.LoadLocation("Asia/Shanghai") 会静默 fallback 到 UTC,而 cron.New() 默认用 time.Local,等于把错误时区当真了。
- 生产环境必须显式传入 location:
cron.New(cron.WithLocation(loc)),其中loc, _ := time.LoadLocation("Asia/Shanghai") - 绝对不要用
time.Now().Location()动态取——Docker 内它大概率返回UTC,且不报错 - 配置文件里存 cron 表达式时,必须连带存
"location": "Asia/Shanghai",不能只存"spec": "0 9 * * *" - 验证方式:启动后调用
c.Entries(),检查每个 entry 的Next字段是否符合预期时间点(用fmt.Printf("%v", e.Next.In(loc)))
用 time.Ticker 做底层驱动反而更准?
robfig/cron/v3 底层其实也是靠 time.AfterFunc 和排序数组维护下次触发时间,高频率任务(比如秒级)+ 大量 job 时,排序开销明显,pprof 显示 sort.Sort 占 CPU 高峰 30%+。这时候不如自己用 time.Ticker 对齐整点,再手动计算 next run time。
- 适用场景:固定周期(如每 30 秒)、任务数
- 关键写法:
t := time.NewTicker(time.Second * 30),然后在for range t.C里调用time.Now().Truncate(30 * time.Second).Add(30 * time.Second)算下一个整点 - 好处:无排序、无反射、无 goroutine 泄漏风险;坏处:不支持
@daily这类语义表达式,得自己 parse - 注意:
time.Ticker本身不防漂移——如果某次处理耗时 > 30s,下一次 tick 会立刻触发,所以仍要加select { case 控制单次执行上限
真正容易被忽略的,是“漂移”往往不是时间不准,而是你根本没意识到某个任务已经连续三次被 DelayIfStillRunning 跳过——日志里只有一行“skipped”,没带 entry ID 和持续时长,查问题时只能翻监控曲线猜。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











