3. 限流基础理论:令牌桶、漏桶、固定窗口与滑动窗口算法原理与对比

在分布式服务边界实施限流,本质上是控制请求到达后端服务的速率。不同的限流算法决定了流量整形效果的平滑度、突发容忍能力以及资源消耗。本节深入剖析四种核心算法的原理、数学模型与适用场景,为后续 gRPC 限流配置提供理论基石。

3.1 令牌桶算法(Token Bucket)

令牌桶是业界最广泛使用的限流算法,gRPC 内置的 gRPC-LB 及许多服务网格(如 Istio)均基于此实现。其核心思想是:以固定速率向桶中投放令牌,请求必须获取令牌才能被处理。

  • 关键参数:
    • r(rate):令牌生成速率,即平均允许的请求速率(如 100 req/s)。
    • b(burst):桶的最大容量,即允许的突发请求数量(如 200)。
  • 工作流程:
    1. 系统以恒定速率 r 向桶中添加令牌,桶满则丢弃多余令牌。
    2. 每个请求到达时,尝试从桶中取出 1 个(或多个)令牌。
    3. 若桶中有足够令牌,请求被放行,令牌数减少;否则请求被拒绝或排队。
  • 核心特性:允许一定程度的突发流量(由 b 控制),同时保证长期平均速率不超过 r。
💡 工程启示: 令牌桶的突发能力对 gRPC 流式调用尤为重要。当客户端发起一个包含 100 条消息的 Server Streaming RPC,若桶容量足够大,前 100 个消息可瞬间通过,后续消息则被平滑至 r 速率,避免因逐条限流导致连接中断。

3.2 漏桶算法(Leaky Bucket)

漏桶算法与令牌桶相反,它将请求视为水滴,以固定速率从桶底漏出。桶本身是一个缓冲队列,用于暂存超出处理能力的请求。

  • 关键参数:
    • c(capacity):桶的容量,即最大排队请求数。
    • o(outflow rate):漏出速率,即请求被处理的最大速率。
  • 工作流程:
    1. 请求到达后,若桶未满则放入桶中排队;若桶已满则直接丢弃(或返回限流错误)。
    2. 系统以恒定速率 o 从桶底取出请求进行处理。
  • 核心特性:强制输出速率绝对平滑,不允许任何突发。适用于必须严格整形流量的场景(如数据库连接池、硬件设备接口)。
⚠️ 适用性注意: 漏桶算法在 gRPC 服务端限流中较少直接使用,因为它会强制所有请求排队等待,增加尾部延迟(Tail Latency)。但在客户端侧(如 gRPC 连接池)或对延迟不敏感的批处理系统中,漏桶能有效防止后端过载。

3.3 固定窗口算法(Fixed Window)

固定窗口算法将时间划分为固定长度的时间窗口(如 1 秒),在每个窗口内统计请求总数,超过阈值则拒绝后续请求。窗口结束后计数器重置。

  • 关键参数:
    • w(window size):窗口大小(如 1s、10s)。
    • limit:每个窗口允许的最大请求数。
  • 工作流程:
    1. 每个请求到达时,检查当前窗口计数器。
    2. 若计数器 < limit,请求放行,计数器 +1。
    3. 若计数器 ≥ limit,请求被拒绝。
    4. 窗口切换时,计数器归零。
  • 核心缺陷:存在“临界突变”问题。在窗口边界处,流量可能瞬间翻倍(如窗口最后 1ms 和下一窗口第 1ms 各允许 limit 个请求,实际瞬时速率达到 2×limit)。

3.4 滑动窗口算法(Sliding Window)

滑动窗口算法是对固定窗口的改进,它将时间窗口细分为多个小格子(如 1 秒窗口分为 10 个 100ms 格子),并记录每个格子的请求数。当前窗口的请求总数 = 当前格子 + 前 N-1 个格子的总和。

  • 关键参数:
    • w:总窗口大小。
    • s(slot):子窗口大小(精度)。
    • limit:滑动窗口内允许的最大请求数。
  • 工作流程:
    1. 请求到达时,计算其所属的当前子窗口。
    2. 累加当前子窗口及之前 w/s - 1 个子窗口的请求数。
    3. 若总和 < limit,放行并更新当前子窗口计数器;否则拒绝。
  • 核心特性:解决了固定窗口的临界突变问题,限流边界平滑,精度由子窗口大小决定。是分布式限流组件(如 Sentinel、Envoy 的本地限流)的常用实现。

3.5 四种算法对比总览

维度 令牌桶 漏桶 固定窗口 滑动窗口
突发容忍 ✅ 允许(由桶容量控制) ❌ 不允许 ⚠️ 边界处存在突发 ✅ 允许(窗口内平滑)
输出平滑度 中等(突发后平滑) 极高(绝对平滑) 低(窗口内无控制) 较高(取决于子窗口粒度)
内存/计算开销 低(仅维护桶状态) 低(仅维护队列) 极低(仅一个计数器) 中(需维护多个子窗口计数器)
临界突变问题 无 无 ⚠️ 严重 无
gRPC 适用场景 服务端限流(首选) 客户端连接池限流 简单计数器(不推荐) 分布式限流(精确控制)
典型实现 gRPC 内置、Istio Nginx limit_req Redis INCR + TTL Sentinel、Envoy 本地限流

3.6 算法选择决策流程

在服务边界限流配置中,可参考以下决策树选择算法:

  1. 是否需要容忍突发流量?
    • 是 → 进入步骤 2。
    • 否 → 选择漏桶算法(如数据库连接池限流)。
  2. 是否需要精确的窗口边界控制?
    • 是 → 选择滑动窗口算法(如多租户配额管理)。
    • 否 → 选择令牌桶算法(gRPC 服务端默认推荐)。
  3. 资源极度受限(如嵌入式设备)?
    • 是 → 可退化为固定窗口算法,但需接受临界突变风险。
🎯 最佳实践: 在 gRPC 服务端限流中,令牌桶算法是默认首选。它既能通过 b 参数允许合理的业务突发(如秒杀、批量查询),又能通过 r 参数保护后端资源。若需要更精细的分布式限流(如按用户 ID 分桶),可结合滑动窗口算法实现。

3.7 算法核心流程对比图

下图从请求处理视角,对比令牌桶与漏桶两种经典算法的核心流程差异:

令牌桶 vs 漏桶:请求处理流程对比 令牌桶算法 令牌生成器(速率 r) 令牌桶(容量 b) 当前令牌数:5 / 10 请求到达 有令牌? 放行 拒绝 / 等待 漏桶算法 请求到达 漏桶(队列,容量 c) 排队请求数:3 / 10 漏出处理(速率 o) 放行处理 队列满则丢弃

3.8 固定窗口与滑动窗口边界效应对比

下图直观展示固定窗口的“临界突变”问题,以及滑动窗口如何通过子窗口粒度消除该问题:

固定窗口 vs 滑动窗口:边界效应对比(窗口大小 1s,阈值 5) 固定窗口(临界突变) 窗口 1(0~1s) 请求数:5(已满) 窗口 2(1~2s) 请求数:5(已满) 边界 ⚠️ 在 1.000s 和 1.001s 各放行 5 个请求 瞬时速率达到 10 req/s,超出阈值 2 倍 滑动窗口(平滑限流) 0~200ms 2 req 200~400ms 1 req 400~600ms 0 req 600~800ms 1 req 800~1000ms 当前 滑动窗口(1s)总请求数 = 2+1+0+1+1 = 5(已达阈值) ✅ 新请求到达时,滑动窗口实时计算最近 1s 的总请求数 若已达阈值,即使窗口边界处也不会放行额外请求

3.9 关键总结与选型建议

  • 令牌桶:gRPC 服务端限流的工业标准。推荐用于大多数微服务场景,通过调整 b/r 比例控制突发程度。
  • 漏桶:适用于绝对平滑输出的场景,如数据库连接池、硬件接口限流。不推荐用于对延迟敏感的在线服务。
  • 固定窗口:实现最简单,但临界突变问题可能导致限流失效。仅适用于非关键路径的粗略计数(如日志采样)。
  • 滑动窗口:精度最高,但需要更多内存。适用于分布式限流(如 Redis 集群分片限流)或需要精确控制的多租户场景。