2. 限流算法(一):固定窗口计数器算法原理、Go实现、边界问题(突刺现象)
2.1 算法原理
固定窗口计数器(Fixed Window Counter)是最直观的限流算法。它将时间划分为固定长度的窗口(例如 1 秒),每个窗口维护一个计数器,记录该窗口内通过的请求数。当请求到达时,若当前窗口计数器未达到阈值则放行并计数加 1;若已达到阈值则直接拒绝。
核心思想: 以时间窗口为粒度,限制窗口内的最大请求总量。窗口切换时计数器重置。
算法三要素
| 要素 | 说明 | 典型值 |
|---|---|---|
| 窗口大小 | 固定时间段的长度 | 1s / 1min / 1h |
| 窗口阈值 | 每个窗口允许的最大请求数 | 100 / 1000 / 10000 |
| 当前计数 | 窗口内已通过的请求累计值 | 0 → 阈值 |
2.2 固定窗口计数器的工作流程
下图展示了固定窗口计数器在连续两个窗口内的请求处理逻辑:
2.3 Go 语言实现
以下是一个线程安全的固定窗口计数器实现,使用 sync.Mutex 保护共享状态:
package limiter
import (
"sync"
"time"
)
// FixedWindowLimiter 固定窗口计数器限流器
type FixedWindowLimiter struct {
mu sync.Mutex
limit int // 窗口阈值
window time.Duration // 窗口大小
count int // 当前计数
windowStart time.Time // 当前窗口起始时间
}
// NewFixedWindowLimiter 创建固定窗口限流器
func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowLimiter {
return &FixedWindowLimiter{
limit: limit,
window: window,
windowStart: time.Now(),
}
}
// Allow 判断是否允许请求通过
func (l *FixedWindowLimiter) Allow() bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
// 如果已超出当前窗口,重置计数器
if now.Sub(l.windowStart) >= l.window {
l.count = 0
l.windowStart = now
}
// 判断是否超过阈值
if l.count < l.limit {
l.count++
return true
}
return false
}
实现要点:
- 使用
time.Now()判断窗口是否过期,避免依赖外部定时器。 - 窗口重置时同时更新
windowStart和count,保证原子性。 Allow()方法适合单次判断场景;若需批量判断,可扩展AllowN(n int)。
2.4 边界问题:突刺现象
固定窗口计数器存在一个严重的边界缺陷——突刺现象(Traffic Spike at Window Boundary)。由于窗口切换时计数器归零,攻击者或突发流量可以在窗口边界处集中请求,导致实际 QPS 达到阈值的 2 倍。
突刺现象示例
假设阈值 = 5,窗口 = 1s:
- 在 0.9s ~ 1.0s 内发送 5 个请求(窗口 1 允许)。
- 在 1.0s ~ 1.1s 内发送 5 个请求(窗口 2 允许)。
- 实际 0.2s 内通过了 10 个请求,瞬时 QPS = 50,远超阈值 5。
突刺现象的本质: 固定窗口只限制窗口内的总量,不限制窗口边界处的请求密度。窗口切换的瞬间,计数器重置为 0,导致边界两侧的请求可以“接力”通过。
2.5 突刺现象的视觉化分析
下图展示了突刺现象如何让瞬时流量突破阈值:
2.6 突刺现象的量化分析
| 指标 | 正常情况 | 突刺情况 | 恶化倍数 |
|---|---|---|---|
| 窗口阈值 | 5 req/s | 5 req/s | — |
| 实际通过量 | 5 req/s | 10 req / 0.2s | 10× |
| 瞬时 QPS | 5 | 50 | 10× |
| 后端压力 | 可控 | 可能击穿 | — |
2.7 突刺现象的应对思路
突刺现象是固定窗口计数器的固有缺陷,无法通过参数调优彻底消除。常见的应对策略包括:
- 缩小窗口粒度: 将 1s 窗口拆分为 100ms 窗口,降低单窗口容量,但无法根除边界问题。
- 引入滑动窗口: 将多个子窗口的计数叠加,平滑边界效应(下一章详细讲解)。
- 配合漏桶/令牌桶: 在固定窗口基础上增加速率限制,控制请求的到达间隔。
- 增加前置缓冲: 在限流器前加一层请求队列,削峰填谷。
工程建议: 固定窗口计数器适合对精度要求不高的场景(如小时级/天级配额限制)。对于秒级限流,尤其是需要防御突发流量的场景,建议直接使用滑动窗口或令牌桶算法。
2.8 本章小结
- 固定窗口计数器以固定时间窗口为单位,限制窗口内的请求总量,实现简单、性能高。
- Go 实现使用
sync.Mutex保护计数器和窗口起始时间,窗口过期时重置。 - 突刺现象是固定窗口的核心缺陷:窗口边界处计数器重置,导致瞬时流量可达阈值的 2 倍甚至更高。
- 突刺现象无法通过参数调优消除,需要引入滑动窗口或令牌桶等更平滑的算法。