滑动窗口该用time.AfterFunc而非time.Ticker,因其基于绝对时间点调度可自动对齐窗口边界,避免累积漂移;每次触发后重新计算下一轮绝对时间点并调用time.AfterFunc,且回调中不可阻塞。

滑动窗口该用 time.Ticker 还是 time.AfterFunc?
直接用 time.Ticker 做窗口推进容易累积时间漂移,尤其在指标采集频率高(如 100ms 窗口)或 GC 暂停明显时。更稳妥的方式是每次窗口结束时用 time.AfterFunc 启动下一轮——它基于绝对时间点调度,能自动对齐预期窗口边界。
实操建议:
- 初始化窗口起始时间用
start := time.Now().Truncate(window),避免首次偏移 - 每次触发后重新计算下一个触发点:
next := start.Add(window),再调time.AfterFunc(next.Sub(time.Now()), ...) - 别在
AfterFunc回调里做阻塞操作(如写磁盘、HTTP 请求),否则会拖慢下一次触发
窗口数据结构选 slice 还是 container/list?
绝大多数场景用 []struct{ts time.Time; val float64} 就够了。Go 的 slice append + 切片截断比 container/list 内存开销小、缓存局部性好,且滑动窗口天然按时间顺序追加+淘汰,不需要双向链表的任意插入能力。
关键细节:
- 预分配容量:
buf := make([]item, 0, int(window/duration)+1),避免频繁扩容 - 淘汰旧数据用切片重切:
buf = buf[i:],不是遍历删除;用二分查找定位第一个 >=now.Add(-window)的索引 - 如果需支持乱序写入(如日志延迟到达),才考虑改用 map + 时间桶,但会显著增加内存和 GC 压力
prometheus.Counter 能直接用于滑动窗口求均值吗?
不能。Counter 是单调递增的累计值,没有时间维度信息。想算“过去 60 秒请求量均值”,你得自己维护一个带时间戳的样本队列,再对其中 ts > now.Add(-60*time.Second) 的样本求和并除以数量。
常见错误:
- 误用
prometheus.NewGaugeVec存每个窗口的快照——这会导致指标数爆炸(每窗口 1 个 metric),Prometheus server 无法有效 scrape - 把 Counter 的
Inc()和窗口更新混在一起调用,造成采样时机错位 - 正确做法:用本地内存 buffer 统计,只在窗口结束时暴露一个
Gauge或通过prometheus.NewHistogramVec记录分布
如何避免高并发写入时的锁竞争?
窗口统计最常卡在写入路径上。别一上来就用 sync.Mutex 包整个 buffer 更新逻辑——细粒度拆分更有效:
- 用
sync/atomic维护一个 uint64 类型的计数器(如总请求数),读写都无锁 - 对需要时间序列分析的字段(如响应时间分布),用分片数组:比如 8 个
[]float64,写入时shardID := uint64(ts.UnixNano()) % 8,最后合并时再加锁汇总 - 如果使用
sync.RWMutex,确保读多写少;写操作仅在窗口切换瞬间发生,其余时间全是读,所以读锁开销可接受
真正难处理的是窗口切换与写入同时发生的竞态——必须在切换前先停止新写入(比如用 atomic.CompareAndSwapUint32(&writing, 1, 0) 打断写入流),再原子替换 buffer 引用,最后恢复写入。

















