Go ticker 长时间运行后任务越来越漂移怎么办
来源:17golang原创
时间:2026-09-12 13:19:36 247浏览 收藏
定时任务跑几个小时后越来越晚,先别急着把 time.Ticker 的周期改小。Go 的 ticker 本身按固定周期产生 tick;真正造成“漂移”的,通常是任务执行时间、接收 tick 的延迟,或代码把“任务完成时间”当成了下一轮的起点。更重要的一点是:慢接收方不会得到无限积压的 tick,官方实现会调整间隔或丢掉来不及接收的 tick。
如果任务必须按墙上时钟对齐,就用“固定截止时间 + 当前时间判断”调度;如果任务只要求两次执行之间间隔一段时间,才适合“执行完成后再等待”。先决定错过的周期是跳过还是补偿,再决定是否使用 ticker。
tickAt是调度事件时间,任务真正开始的时间可能更晚,二者不要混为一谈。- 任务耗时超过周期时,
time.Ticker会丢 tick,不会替你创建无限补偿队列。 - 固定周期任务应按
next = next.Add(period)推进截止点,并显式处理过期情况。
漂移先看哪两个时间点
排查时同时记录 tick 携带的时间和任务实际开始时间。下面的示例只展示记录与判断方式,任务函数应替换成自己的业务逻辑。
ticker := time.NewTicker(period)
defer ticker.Stop() // 任务退出时停止发送,避免遗留调度资源
for {
select {
case tickAt :=
如果 lag 逐渐变大,说明任务开始得越来越晚,或者当前 goroutine 没有及时回到 select。如果 lag 不大但两次任务开始时间间隔不稳定,应继续看任务耗时、日志时间戳和是否存在其他阻塞。

为什么慢任务会让人感觉 ticker 在漂移
time.Ticker 的 channel 只负责交付时间事件,任务代码通常在同一个循环里同步执行。任务执行期间循环无法接收新的 tick;当任务超过一个周期,未及时交付的 tick 可能被丢弃。这样做避免了 channel 中堆满过期任务,却也意味着“每个周期都必须执行一次”这个要求不能交给 ticker 保证。
还有一种常见写法是任务结束后调用 time.Sleep(period),或者用 time.After(period) 重新等待。它表达的是“本轮结束后间隔 period”,任务耗时会被加进周期,长时间运行时自然越来越偏离整点。如果需求是两次任务之间留出冷却时间,这种行为是正确的;如果需求是每 5 分钟对齐一次,就不是正确的模型。
| 需求 | 合适模型 | 过期时的决定 |
|---|---|---|
| 尽量每隔一段空闲时间执行 | 任务完成后重新等待 | 不追赶,周期包含任务耗时 |
| 按固定墙钟周期触发 | 固定截止时间 | 通常跳过已过期轮次 |
| 账务或批处理必须补齐 | 截止时间加任务队列 | 限量补偿,避免无限追赶 |
用固定截止时间而不是任务结束时间推进
需要稳定周期时,可以用 time.Timer 等待下一个截止点。每轮都从上一个截止点加周期,任务变慢只会让当前轮变晚,不会把后续计划整体向后平移。
func runOnSchedule(ctx context.Context, period time.Duration, run func() error) error {
next := time.Now().Add(period)
timer := time.NewTimer(time.Until(next))
defer timer.Stop() // 无论正常退出还是取消,都停止当前计时器
for {
select {
case
这里的“跳过”不是丢失业务数据,而是承认一次周期性快照已经没有执行价值。若每个截止点都必须处理,应把截止点转成队列消息,并设置最大补偿次数、队列容量和告警阈值,不要在单个 goroutine 里无限追赶。

上线前把三种延迟分开监控
至少记录计划时间、实际开始时间和任务结束时间:schedule_lag = startedAt - plannedAt,run_duration = finishedAt - startedAt,再统计跳过次数或补偿次数。不要只看“上一轮结束到下一轮开始”的间隔,因为它无法告诉你是任务变慢,还是调度点已经被整体推迟。
退出时用 ctx.Done() 打断等待;使用 NewTicker 时保留 Stop 仍然是清楚的生命周期表达。周期小于等于零不要传给 NewTicker 或 Timer.Reset。如果任务允许并发执行,还要额外限制 worker 数量,否则修复了时间漂移却制造了并发重入。
常见问题
time.Ticker 会把错过的 tick 补回来吗?
不会保证逐个补回。官方文档明确说明,慢接收方会通过调整间隔或丢 tick 来追上节奏;需要补偿时应自行建立有上限的队列。
为什么不用每轮重新创建 ticker?
若目标是固定周期,重新从 time.Now() 创建会把任务耗时加入下一轮,反而形成完成时间驱动的漂移。固定截止时间或单个 timer 更容易表达策略。
跳过过期轮次会不会漏数据?
会不会漏取决于任务语义。刷新缓存、采集当前状态通常可以跳过;账务、订单和不可重复消费的批次应把周期转换为可追踪的队列项,采用限量补偿并监控积压。
核对时可以直接查看 Go 官方 time 包中 NewTicker、Stop 和 Reset 的说明:https://pkg.go.dev/time。最终要修的不是 ticker 的“漂移参数”,而是任务完成时间、计划截止时间和过期处理策略之间的混用。
-
375 收藏
-
298 收藏
-
248 收藏
-
261 收藏
-
483 收藏
-
112 收藏
-
Golang · Go问答 | 2小时前 | Go问答 · 连接复用 · HTTP超时 · Context取消 · 服务端取消 · Go context Transport http.Client WithTimeout WithContext180 收藏
-
Golang · Go问答 | 2小时前 | HTTP · go · 接口排查 · cookiejar · Cookie会话 · Go Cookiejar expires MaxAge http.CookieJar 过期Cookie181 收藏
-
Golang · Go问答 | 2小时前 | 重定向 · 排查 · Cookie · net/http · Go问答 · 重定向 Go Cookiejar http.Client CheckRedirect http.Cookie464 收藏
-
Golang · Go问答 | 2小时前 | 代理 · 环境变量 · 故障排查 · HTTP客户端 · Go问答 · Go http.Transport http.Client ProxyFromEnvironment HTTP_PROXY HTTPS_PROXY NO_PROXY282 收藏
-
Golang · Go问答 | 2小时前 | go语言 · 接口设计 · Go问答 · 兼容性 · JSON解析 · encoding/json 接口兼容 DisallowUnknownFields RawMessage Go JSON 未知字段482 收藏
-
260 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习