限流思路
四种基本限流算法 (单机限流)
固定窗口算法
- 原理:将时间划分为固定的“窗口”(例如,每秒一个窗口),在每个窗口内维护一个计数器。当请求到来时,计数器加一。如果计数器超过阈值,则拒绝请求。当时间窗口结束时,计数器重置为零。
- 优点:
- 实现非常简单,容易理解。
- 缺点:
- 存在临界突变问题:在两个窗口的边界处,可能承受两倍于阈值的流量。例如,限流100 QPS,一个请求在第1秒的最后100毫秒内发起了100个请求,并在第2秒的前100毫秒内又发起了100个请求,这200个请求在200毫秒内是合法的,但实际负载很高。
- 流量控制不平滑,无法应对突发流量。
滑动窗口算法
- 原理:对固定窗口算法的改进。它将一个大的时间窗口(如1分钟)划分为多个更小的子窗口(如60个1秒的子窗口)。随着时间的推移,窗口会“滑动”:每过1秒,一个新的子窗口被添加进来,同时最旧的一个子窗口被移除。请求总数是当前所有子窗口内的请求计数之和。
- 优点:
- 相比固定窗口,流量控制更平滑、更精确。
- 有效解决了固定窗口的临界突变问题。
- 缺点:
- 实现相对复杂。
- 需要存储更多的数据(每个子窗口的计数),内存消耗更大。
漏桶算法
- 原理:将请求想象成水流,系统是一个固定容量、固定流出速率的“漏桶”。无论流入速率有多快,流出的速率始终是恒定的。当“桶”满了之后,新流入的请求将被直接丢弃。
- 优点:
- 可以强行平滑流量,使得请求以一个恒定的速率被处理。
- 有效防止系统被突发流量冲垮。
- 缺点:
- 无法应对突发流量。即使系统当前处于空闲状态,也必须以固定的慢速率处理请求,导致资源浪费。
- 不具备灵活性,流出速率固定,无法动态调整。
令牌桶算法
- 原理:系统以一个固定的速率向一个固定容量的“令牌桶”中放入令牌。每个进来的请求都需要先从桶中获取一个令牌,获取成功才能被处理;如果桶内没有令牌,则请求被拒绝或排队等待。
- 优点:
- 既能限流,又能应对突发流量。当流量较小时,令牌会累积起来;当突发流量到来时,累积的令牌可以一次性被消耗,从而允许系统在短时间内处理超过平均速率的请求(突发量不能超过桶的容量)。
- 灵活性高,可以通过调整令牌生成速率和桶容量来控制流量。
- 缺点:
- 实现比漏桶算法稍复杂。
- 如果令牌生成速率持续高于消耗速率,会导致令牌累积,可能造成资源浪费。
| 算法 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 固定窗口 | 实现简单,容易理解和部署。 | 存在临界突变问题,流量控制不平滑。 | 对流量平滑度要求不高的稳定场景。 |
| 滑动窗口 | 流量控制更平滑、精确,解决了临界问题。 | 实现复杂,内存消耗较高。 | 需要平滑处理突发流量的场景。 |
| 漏桶算法 | 强制平滑流量,输出速率恒定。 | 无法应对突发流量,缺乏灵活性,可能浪费资源。 | 需要固定输出速率的场景,如API网关的流量整形。 |
| 令牌桶 | 既能限流又能应对突发流量,非常灵活。 | 实现相对复杂,需要精确的时间控制。 | 绝大多数需要限流且允许一定突发流量的场景。 |
分布式限流方案
基于中心化存储的方案 (如 Redis)
- 原理:将限流的计数器或令牌桶状态存储在一个中心化的服务(如 Redis)中。所有服务实例在处理请求前,都先向该中心化服务申请许可。通常利用 Redis 的原子操作(如
INCR)或 Lua 脚本来实现。 - 存在问题:
- 性能瓶颈:所有请求都依赖中心化服务,可能导致其成为系统瓶颈。
- 单点故障:如果中心化服务宕机,整个限流功能将失效。
- 网络延迟:每次请求都需要一次网络调用,增加了响应时间。
基于负载均衡的本地限流方案
- 原理:通过负载均衡器(如 Nginx)将请求均匀分发到各个服务实例上。每个实例各自维护一个独立的单机限流器(如令牌桶)。
- 存在问题:
- 精度问题:这并非全局精确限流。如果流量分配不均,可能导致某些实例过载而其他实例空闲,整个系统的总请求量可能超过预设的全局阈值。
- 动态扩缩容适应性差:当服务实例数量变化时,需要重新调整每个实例的限流配置。
- 负载均衡器单点故障:负载均衡器本身可能成为单点。
基于分布式协调服务的方案
- 原理:利用 ZooKeeper 或 etcd 这类服务的分布式锁或顺序节点特性来实现全局限流。例如,在 ZooKeeper 中创建一个节点来存储令牌数量,各个服务通过获取分布式锁来原子性地增减令牌数。
- 存在问题:
- 实现复杂:方案设计和实现都比较复杂。
- 性能问题:分布式协调服务通常不适合高频的读写操作,大量的令牌申请和释放请求可能使其成为系统瓶颈。