3. 限流算法(上):固定窗口计数器算法原理、滑动窗口计数器算法原理、算法对比与适用场景

限流算法是模型服务保障稳定性的第一道防线。本章聚焦两种最基础的计数器算法:固定窗口计数器与滑动窗口计数器。理解它们的原理、缺陷与适用场景,是后续学习令牌桶、漏桶等高级算法的基础。

3.1 固定窗口计数器算法原理

固定窗口计数器是最直观的限流实现。它将时间划分为固定的窗口(如 1 秒、1 分钟),每个窗口内维护一个计数器,请求到达时计数器加 1;当计数器超过预设阈值时,拒绝后续请求;窗口结束时计数器归零。

核心步骤:
  1. 定义时间窗口大小 W(如 1000ms)和阈值 limit(如 10)。
  2. 每个请求到达时,检查当前窗口计数器 count。
  3. 若 count < limit,允许请求,count++。
  4. 若 count >= limit,拒绝请求,返回 429 或降级。
  5. 窗口到期(如当前时间 >= 窗口起始时间 + W),重置 count = 0,开启新窗口。
固定窗口计数器算法流程 窗口 1 (0~1000ms) 计数器: 0 → 1 → 2 → ... → limit 窗口 2 (1000~2000ms) 计数器重置为 0,重新计数 请求 拒绝 阈值 limit=10 ⚠ 边界突刺问题 窗口切换瞬间,计数器重置,可能导致窗口末+窗口初的请求量 达到 2×limit,形成流量突刺,压垮后端模型服务。
边界突刺(临界问题):固定窗口计数器最致命的缺陷。例如窗口大小为 1s、阈值为 10,若第 0.9s 有 10 个请求,第 1.0s(新窗口开始)又有 10 个请求,则 0.1s 内通过了 20 个请求,远超阈值。模型服务可能因此被瞬时击穿。

3.2 滑动窗口计数器算法原理

滑动窗口计数器通过将时间窗口细分为多个小格子(子窗口),并随着时间推移动态滑动窗口,解决了固定窗口的边界突刺问题。每个小格子独立计数,窗口总计数为所有子窗口计数之和。

核心步骤:
  1. 将时间窗口 W 划分为 N 个等长子窗口(如 1s 窗口划分为 10 个 100ms 子窗口)。
  2. 每个子窗口维护独立的计数器。
  3. 请求到达时,计算当前时间所属的子窗口,该子窗口计数器加 1。
  4. 计算当前时间向前 W 时间内所有子窗口的计数总和。
  5. 若总和 < 阈值 limit,允许请求;否则拒绝。
  6. 过期的子窗口(超出当前时间 - W)自动失效,不计入总和。
滑动窗口计数器算法示意(1s 窗口,10 个子窗口) 0~100ms cnt:3 100~200ms cnt:2 200~300ms cnt:1 300~400ms cnt:0 400~500ms cnt:2 500~600ms cnt:1 600~700ms cnt:0 700~800ms cnt:3 800~900ms cnt:1 900~1000ms cnt:2 当前滑动窗口(1s 跨度,覆盖 10 个子窗口) 当前时间 已过期 ✅ 滑动窗口优势 子窗口粒度越细,限流精度越高。例如 1s 窗口分 10 份,可检测到 100ms 级别的流量突刺。 但子窗口越多,内存占用和计算开销越大,需在精度与性能间权衡。

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 个),否则内存和计算开销线性增长。
  • 可使用“缓存总和”技巧:每次滑动时只更新受影响的子窗口,避免全量求和。

3.5 小结