3. 限流算法(上):固定窗口计数器算法原理、滑动窗口计数器算法原理、算法对比与适用场景
限流算法是模型服务保障稳定性的第一道防线。本章聚焦两种最基础的计数器算法:固定窗口计数器与滑动窗口计数器。理解它们的原理、缺陷与适用场景,是后续学习令牌桶、漏桶等高级算法的基础。
3.1 固定窗口计数器算法原理
固定窗口计数器是最直观的限流实现。它将时间划分为固定的窗口(如 1 秒、1 分钟),每个窗口内维护一个计数器,请求到达时计数器加 1;当计数器超过预设阈值时,拒绝后续请求;窗口结束时计数器归零。
核心步骤:
- 定义时间窗口大小
W(如 1000ms)和阈值limit(如 10)。 - 每个请求到达时,检查当前窗口计数器
count。 - 若
count < limit,允许请求,count++。 - 若
count >= limit,拒绝请求,返回 429 或降级。 - 窗口到期(如当前时间 >= 窗口起始时间 + W),重置
count = 0,开启新窗口。
边界突刺(临界问题):固定窗口计数器最致命的缺陷。例如窗口大小为 1s、阈值为 10,若第 0.9s 有 10 个请求,第 1.0s(新窗口开始)又有 10 个请求,则 0.1s 内通过了 20 个请求,远超阈值。模型服务可能因此被瞬时击穿。
3.2 滑动窗口计数器算法原理
滑动窗口计数器通过将时间窗口细分为多个小格子(子窗口),并随着时间推移动态滑动窗口,解决了固定窗口的边界突刺问题。每个小格子独立计数,窗口总计数为所有子窗口计数之和。
核心步骤:
- 将时间窗口
W划分为N个等长子窗口(如 1s 窗口划分为 10 个 100ms 子窗口)。 - 每个子窗口维护独立的计数器。
- 请求到达时,计算当前时间所属的子窗口,该子窗口计数器加 1。
- 计算当前时间向前
W时间内所有子窗口的计数总和。 - 若总和 < 阈值
limit,允许请求;否则拒绝。 - 过期的子窗口(超出当前时间 - W)自动失效,不计入总和。
3.3 算法对比与适用场景
两种算法各有优劣,选择时需结合模型服务的流量特征、精度要求和资源限制。下表从多个维度进行对比:
| 对比维度 | 固定窗口计数器 | 滑动窗口计数器 |
|---|---|---|
| 实现复杂度 | 极低,仅需一个计数器 + 时间戳 | 中等,需维护 N 个子窗口计数器 + 滑动逻辑 |
| 内存开销 | O(1) | O(N),N 为子窗口数量 |
| 边界突刺 | 严重,窗口切换时可突破 2×limit | 无,平滑滑动,精度由子窗口粒度决定 |
| 限流精度 | 粗粒度,仅保证窗口内总量 | 细粒度,可感知窗口内任意时刻的流量分布 |
| 计算开销 | 极低,每次请求 O(1) | 较低,每次请求需求和 N 个子窗口,可优化为 O(1)(缓存总和) |
| 适用场景 | 流量平稳、对突刺不敏感、资源受限的边缘设备 | 流量波动大、对突刺敏感、需要精确限流的在线模型服务 |
模型服务选型建议:
- 离线批量推理(如定时特征工程、数据预处理):流量可预测,固定窗口计数器足够,且节省资源。
- 在线实时推理(如 LLM 对话、推荐系统):流量突发频繁,强烈建议使用滑动窗口计数器,子窗口粒度建议设为 100ms~200ms,平衡精度与开销。
- 混合部署场景:可在网关层使用滑动窗口做粗粒度限流,在模型实例层使用固定窗口做快速拒绝,形成两级防护。
3.4 代码示例:滑动窗口计数器核心逻辑
以下为滑动窗口计数器的简化实现,展示子窗口维护与滑动求和的核心逻辑:
class SlidingWindowCounter:
def __init__(self, window_size_ms=1000, sub_window_count=10, limit=100):
self.window_size = window_size_ms
self.sub_window_count = sub_window_count
self.sub_window_size = window_size_ms // sub_window_count
self.limit = limit
# 使用循环数组存储子窗口计数
self.counters = [0] * sub_window_count
self.current_index = 0
self.total = 0
self.last_time_ms = 0
def allow_request(self, current_time_ms):
# 1. 计算当前时间所属子窗口索引
elapsed = current_time_ms - self.last_time_ms
if elapsed >= self.window_size:
# 超过一个窗口,全部重置
self.counters = [0] * self.sub_window_count
self.total = 0
self.current_index = 0
else:
# 滑动:将过期的子窗口计数从 total 中减去
steps = elapsed // self.sub_window_size
for _ in range(int(steps)):
self.total -= self.counters[self.current_index]
self.counters[self.current_index] = 0
self.current_index = (self.current_index + 1) % self.sub_window_count
# 2. 更新当前子窗口计数
self.counters[self.current_index] += 1
self.total += 1
self.last_time_ms = current_time_ms
# 3. 判断是否超过阈值
return self.total <= self.limit
实现注意事项:
- 上述代码为教学简化版,生产环境需处理时间回拨、并发安全(加锁或原子操作)。
- 子窗口数量不宜过多(建议 5~20 个),否则内存和计算开销线性增长。
- 可使用“缓存总和”技巧:每次滑动时只更新受影响的子窗口,避免全量求和。