skip to content
IcyDesert's blog
Table of Contents

限流简介

在实际的系统中,服务器所能承载的请求速率是有上限的。给定这个硬上限时,不能要求服务器处理每一个请求,否则流量太大早晚会耗尽资源。为此,请求在进入服务之前,首先要满足一定的请求速率——否则我们认为服务器有出问题的风险,不予放行。

这个「控制请求速率不超标」的中间件,被称为限流中间件,使用的算法称为限流算法。

分类-时间窗口类

固定窗口

固定窗口很好理解:把时间轴划分成一个个固定长度的窗口,每个窗口内允许通过的请求数量是有限的。当一个请求到来时,我们检查当前窗口内已经处理了多少请求,如果超过了限制,就拒绝这个请求。

确定固定窗口的两个参数:

  • 时间窗口大小
  • 一个窗口内的请求限制

图示一下:

|---- 1s ----|---- 1s ----|---- 1s ----|
| * * * | * * * x | * * |
| 3 / 3 | 4 / 3 | 2 / 3 |
| Pass | Drop 1 req | Pass |

缺陷

在窗口的边缘处容易出现突发流量,也就是所谓的流量突刺。

|------------ 1s --|-- 1s -------------|
| ********|******** |
| 100 / 100 | 100 / 100 |
| Pass | Pass |

上图表示一种可能发生的情况:在第一个窗口的最后时刻,来了 100 个请求,全部通过;在第二个窗口的开始时刻,又来了 100 个请求,也全部通过。

但限流是为保护服务器不过载;我们原本希望限制一个窗口内的请求为 100,如果在短时间内来了 200 个请求,土豆服务器可能会吃不消,导致服务中断。

滑动窗口

滑动窗口在固定窗口的思路上加以改进。

滑动窗口同样划分时间轴为固定长度的时间窗口片,但不同点是:计数不看某一个固定的窗口,而是统计连续的若干小时间片。随着时间流逝,过期时间片被移除、其数据也从限流统计中剔除,新的时间片接收请求并纳入限流统计。其实就是更细粒度的计数(如果只有一片,则退化为固定窗口)。

确定滑动窗口的参数:

  • 时间片长度
  • 总时间窗口长度(即连续统计的小窗口的总长度)
  • 一个时间片内的请求限制
  • 总时间窗口内的请求限制

后面两条限制不一定要同时满足,通常只需要满足总时间窗口的。

还是以 窗口边缘突发流量 为例,来看看滑动窗口的表现。做一些参数和场景设定:

  • 时间片长度为 500ms,总时间窗口长度为 1000ms,总时间窗口内的请求限制为 100。
  • 突发请求发生在第 1s 的后半段和第 2s 的前半段,各打来 100 个请求。
Time (ms) | 0-500 | 500-1000 | 1000-1500 | 1500-2000 |
Incoming | 0 | 100 | 100 | 0 |
Accepted | 0 | 100 | *0* | 0 |
| | | | |
[ Pass 100 ]
[ Drop 100 ]
Fixed Win [ Pass 100 ][ Pass 100 ]

固定窗口在边缘处放行 200 个请求,把土豆服务器打爆了。

另一边,注意看滑动窗口:当滑动窗口统计 [500, 1500] 这 1s 内的请求时,前面 [500-1000] 已经放行了 100 个请求,达到了限制,因此 [1000-1500] 打来的 100 个请求全部被拒绝。如此我们有效避免了「流量突刺」问题,保护下游服务器。

滑动窗口限流最大的优点就是平滑,即把一个尖峰磨平。(好像滑动窗口类算法都能平滑?)缺点是实现复杂。

一个具体的实现参考 Sentinel-Go源码系列(三)滑动时间窗口算法的工程实现,看了才知道远远没有理论这么简单。

分类-桶类

时间窗口类算法的核心思想是:硬性限制某段时间内的请求数量。这些算法都无法良好处理突发流量——如滑动窗口,在短时间大流量的情况下仍会丢弃请求。

桶类算法不再硬性限制请求速率,而是通过维护一个缓冲桶有效地处理突发流量,减少请求丢失。

漏桶

漏桶的原理非常简单。想象一个固定大小的漏斗(桶):

  • 开口端:请求尝试进入桶中,如果桶已满请求就被丢弃;否则进入。
  • 出口端:桶以一个固定的速率漏出请求,即交给下游处理。

这样,即使在短时间内来了大量请求,桶也能缓冲住一部分,减少丢失。

确定漏桶的参数:

  • 桶的容量
  • 漏出速率

这么简单就不用画 ASCII 示意图了吧

令牌桶

令牌桶和漏桶的原理相近,区别只是桶里存的东西:桶里存的是令牌,而不是请求。这种算法引入一个「令牌」的概念:每个请求必须获取到一个令牌才能被处理。

  • 开口端:令牌尝试进入桶中,如果桶已满令牌就被丢弃;否则进入。
  • 出口端:每一个请求尝试从桶里取一个令牌,如果没有令牌,请求就被丢弃;否则被处理。
[T]
| Generated at a fixed rate
v
┌──────── ───────┐
│ \ [T] [T] / │
│ \ [T] [T] / │
│ \ [T] / │
│ \ / │
└──────── ───────┘
Request needs 1 [T] ──> ┼ ───> Succeed if [T] is available
|
v
Fail if no [T] is available, i.e. bucket is empty

确定令牌桶的参数:

  • 桶的容量
  • 令牌生成速率

令牌桶相比漏桶的优点有两个:

  1. 无排队时延:一个请求要做的只是尝试查询并扣减令牌,时延短且固定。漏桶的请求则需要在桶里排队,时延不固定。
  2. 容许更多突发流量:即便瞬时请求速率超过令牌生成速率,桶里仍可能有先前留存的令牌,可处理突发流量;而漏桶装的就是请求,强制以固定速率漏出,无法应对瞬时高并发流量。

    这可能冲击下游服务器,取决于桶的容量;但对外可用性确实更高