3. 限流基础理论:令牌桶、漏桶、固定窗口与滑动窗口算法原理与对比
在分布式服务边界实施限流,本质上是控制请求到达后端服务的速率。不同的限流算法决定了流量整形效果的平滑度、突发容忍能力以及资源消耗。本节深入剖析四种核心算法的原理、数学模型与适用场景,为后续 gRPC 限流配置提供理论基石。
3.1 令牌桶算法(Token Bucket)
令牌桶是业界最广泛使用的限流算法,gRPC 内置的 gRPC-LB 及许多服务网格(如 Istio)均基于此实现。其核心思想是:以固定速率向桶中投放令牌,请求必须获取令牌才能被处理。
- 关键参数:
r(rate):令牌生成速率,即平均允许的请求速率(如 100 req/s)。b(burst):桶的最大容量,即允许的突发请求数量(如 200)。
- 工作流程:
- 系统以恒定速率
r向桶中添加令牌,桶满则丢弃多余令牌。 - 每个请求到达时,尝试从桶中取出 1 个(或多个)令牌。
- 若桶中有足够令牌,请求被放行,令牌数减少;否则请求被拒绝或排队。
- 系统以恒定速率
- 核心特性:允许一定程度的突发流量(由
b控制),同时保证长期平均速率不超过r。
💡 工程启示: 令牌桶的突发能力对 gRPC 流式调用尤为重要。当客户端发起一个包含 100 条消息的 Server Streaming RPC,若桶容量足够大,前 100 个消息可瞬间通过,后续消息则被平滑至
r 速率,避免因逐条限流导致连接中断。
3.2 漏桶算法(Leaky Bucket)
漏桶算法与令牌桶相反,它将请求视为水滴,以固定速率从桶底漏出。桶本身是一个缓冲队列,用于暂存超出处理能力的请求。
- 关键参数:
c(capacity):桶的容量,即最大排队请求数。o(outflow rate):漏出速率,即请求被处理的最大速率。
- 工作流程:
- 请求到达后,若桶未满则放入桶中排队;若桶已满则直接丢弃(或返回限流错误)。
- 系统以恒定速率
o从桶底取出请求进行处理。
- 核心特性:强制输出速率绝对平滑,不允许任何突发。适用于必须严格整形流量的场景(如数据库连接池、硬件设备接口)。
⚠️ 适用性注意: 漏桶算法在 gRPC 服务端限流中较少直接使用,因为它会强制所有请求排队等待,增加尾部延迟(Tail Latency)。但在客户端侧(如 gRPC 连接池)或对延迟不敏感的批处理系统中,漏桶能有效防止后端过载。
3.3 固定窗口算法(Fixed Window)
固定窗口算法将时间划分为固定长度的时间窗口(如 1 秒),在每个窗口内统计请求总数,超过阈值则拒绝后续请求。窗口结束后计数器重置。
- 关键参数:
w(window size):窗口大小(如 1s、10s)。limit:每个窗口允许的最大请求数。
- 工作流程:
- 每个请求到达时,检查当前窗口计数器。
- 若计数器 <
limit,请求放行,计数器 +1。 - 若计数器 ≥
limit,请求被拒绝。 - 窗口切换时,计数器归零。
- 核心缺陷:存在“临界突变”问题。在窗口边界处,流量可能瞬间翻倍(如窗口最后 1ms 和下一窗口第 1ms 各允许
limit个请求,实际瞬时速率达到 2×limit)。
3.4 滑动窗口算法(Sliding Window)
滑动窗口算法是对固定窗口的改进,它将时间窗口细分为多个小格子(如 1 秒窗口分为 10 个 100ms 格子),并记录每个格子的请求数。当前窗口的请求总数 = 当前格子 + 前 N-1 个格子的总和。
- 关键参数:
w:总窗口大小。s(slot):子窗口大小(精度)。limit:滑动窗口内允许的最大请求数。
- 工作流程:
- 请求到达时,计算其所属的当前子窗口。
- 累加当前子窗口及之前
w/s - 1个子窗口的请求数。 - 若总和 <
limit,放行并更新当前子窗口计数器;否则拒绝。
- 核心特性:解决了固定窗口的临界突变问题,限流边界平滑,精度由子窗口大小决定。是分布式限流组件(如 Sentinel、Envoy 的本地限流)的常用实现。
3.5 四种算法对比总览
| 维度 | 令牌桶 | 漏桶 | 固定窗口 | 滑动窗口 |
|---|---|---|---|---|
| 突发容忍 | ✅ 允许(由桶容量控制) | ❌ 不允许 | ⚠️ 边界处存在突发 | ✅ 允许(窗口内平滑) |
| 输出平滑度 | 中等(突发后平滑) | 极高(绝对平滑) | 低(窗口内无控制) | 较高(取决于子窗口粒度) |
| 内存/计算开销 | 低(仅维护桶状态) | 低(仅维护队列) | 极低(仅一个计数器) | 中(需维护多个子窗口计数器) |
| 临界突变问题 | 无 | 无 | ⚠️ 严重 | 无 |
| gRPC 适用场景 | 服务端限流(首选) | 客户端连接池限流 | 简单计数器(不推荐) | 分布式限流(精确控制) |
| 典型实现 | gRPC 内置、Istio | Nginx limit_req |
Redis INCR + TTL | Sentinel、Envoy 本地限流 |
3.6 算法选择决策流程
在服务边界限流配置中,可参考以下决策树选择算法:
- 是否需要容忍突发流量?
- 是 → 进入步骤 2。
- 否 → 选择漏桶算法(如数据库连接池限流)。
- 是否需要精确的窗口边界控制?
- 是 → 选择滑动窗口算法(如多租户配额管理)。
- 否 → 选择令牌桶算法(gRPC 服务端默认推荐)。
- 资源极度受限(如嵌入式设备)?
- 是 → 可退化为固定窗口算法,但需接受临界突变风险。
🎯 最佳实践: 在 gRPC 服务端限流中,令牌桶算法是默认首选。它既能通过
b 参数允许合理的业务突发(如秒杀、批量查询),又能通过 r 参数保护后端资源。若需要更精细的分布式限流(如按用户 ID 分桶),可结合滑动窗口算法实现。
3.7 算法核心流程对比图
下图从请求处理视角,对比令牌桶与漏桶两种经典算法的核心流程差异:
3.8 固定窗口与滑动窗口边界效应对比
下图直观展示固定窗口的“临界突变”问题,以及滑动窗口如何通过子窗口粒度消除该问题:
3.9 关键总结与选型建议
- 令牌桶:gRPC 服务端限流的工业标准。推荐用于大多数微服务场景,通过调整
b/r比例控制突发程度。 - 漏桶:适用于绝对平滑输出的场景,如数据库连接池、硬件接口限流。不推荐用于对延迟敏感的在线服务。
- 固定窗口:实现最简单,但临界突变问题可能导致限流失效。仅适用于非关键路径的粗略计数(如日志采样)。
- 滑动窗口:精度最高,但需要更多内存。适用于分布式限流(如 Redis 集群分片限流)或需要精确控制的多租户场景。