Skip to content
This page has been auto-translated and may contain errors.View in English

固定窗口计数器

想象一个有五个车位、装着道闸的停车场。每三十秒道闸就会重置,车位又空出来了。

这就是固定窗口计数器,最简单的限流算法。在一个固定长度的窗口内统计请求数,超过限制就拒绝,窗口结束时把计数清零。

看一个窗口是怎么填满的

限制为每三十秒五个请求:

时间到达情况计数结果
0秒1个请求1接受
5秒2个请求3接受
15秒2个请求5接受,道闸落下
25秒1个请求5拒绝,窗口已满
30秒第二个窗口开启0计数重置

这里涉及的状态只有两个值:现在处于窗口的什么位置,以及目前的计数是多少。上一个窗口结束后,什么都不会留到下一个窗口。

这种简单性正是它的全部魅力所在。存储成本低,检查成本低,也很难把逻辑搞乱。

Juno看一个窗口是怎么填满的 注意25秒时那个请求。它被拒绝的时候,离窗口结束还有五秒,要是它晚来六秒就能顺利通过了。

这说明这个算法是直来直去的,而不是不公平——它没有"差不多"的概念,只有"在这个窗口里"或者"不在"这两种情况。

Juno看一个窗口是怎么填满的 "上一个窗口的任何信息都不会被记住"正是这个算法运行成本低的原因。每个客户端只需要一个计数器和一个时间戳,重置时直接把旧值丢掉就行。

对比一下下一个算法需要做的事:为每一个请求都记录一个时间戳。存储上的这个差异,正是整个对比要围绕的核心权衡。

Juno看一个窗口是怎么填满的 除了突发流量,还有一个反过来的问题:干涸期。如果五个请求都在窗口的第一秒内到达,那么剩下的二十九秒里,即使服务完全空闲,合法的客户端也会被拒绝。

所以固定窗口对那种"突发但合理"的流量并不友好,而这恰恰是大多数真实流量的样子。一个页面同时加载六个资源,可能瞬间就把整个窗口的额度用光了。

另外还要考虑窗口是按时钟对齐,还是按客户端首次请求的时间对齐。按时钟对齐意味着所有客户端的窗口在同一时刻重置,这会让大家的重试请求同步成一个尖峰。

边界处的突发流量

现在来看连续两个窗口,限制仍然是每三十秒五个:

时间窗口请求数累计计数
50秒窗口224
55秒窗口226,实际接受5个
60秒窗口3开启22

不要看窗口,看时钟。在50秒到60秒这十秒之间,一共有七个请求被接受了。

限制是每三十秒五个,结果十秒里来了七个。

这里没有任何故障。窗口2末尾和窗口3开头的请求分属不同的窗口,所以任何一个窗口的计数都没有超过五。算法完全按照它被告知的规则在运行。

这就是窗口边界问题,也是这种方法最根本的弱点。在最坏的情况下,客户端可以在重置前一瞬间发送满额请求,紧接着在重置后一瞬间再发送一次满额请求,在极短时间内打入两倍限额的流量。

Juno边界处的突发流量 关键在于你站在哪里看问题。在每个窗口内部,一切都是正确的,也都没有超过限制。

但如果你退后一步,看跨越边界的一段时钟时间,就会发现限制其实根本没有真正生效过。

Juno边界处的突发流量 值得算一笔账:最坏情况下,瞬间涌入的流量是限额的两倍,所以设置限额时就该按这个来考虑。如果你的服务能扛住瞬间200个请求,那么设置固定窗口限额为100是说得过去的。

如果攻击者知道你的窗口长度,就能故意瞄准边界发起攻击,而窗口长度其实很容易通过观察重置发生的时间点推断出来。

Juno边界处的突发流量 这不是纸上谈兵,而是可以精确复现的现象。以固定窗口限额100为例,跨边界发送请求可以放行200个。

滑动窗口日志算法在同样情况下只放行100个,滑动窗口计数器算法放行101个,这正是后面几章要展开对比的内容。

尽管如此,还继续使用固定窗口的原因在于成本:每个客户端只需要一个整数和一个时间戳,原子递增即可。在限流最重要的那种规模下,存储和协调成本上的这点差异并不算小。

实际生产环境中常见的做法是:把固定窗口的容量设置得能扛住两倍限额的突发流量,然后在少数几个突发会造成实际损害的端点上,使用更精确的算法。

动手试一试

某个API允许每60秒窗口内10个请求,窗口按时钟对齐,从整分钟开始。

  1. 一个客户端在12:00:59发送了10个请求,又在12:01:01发送了10个请求。有多少个被接受?限制被打破了吗?
  2. 一个客户端在12:00:00时发送了全部10个请求。它在12:00:30发送的请求会怎样?
  3. 你需要保证任何客户端在任意60秒的时间段内都绝不会得到超过10个请求的响应。固定窗口能做到吗?
对照一下你的答案

1. 全部20个都被接受,规则也没有被打破。 前十个落在12:00这个窗口,后十个落在12:01这个窗口,两个窗口的计数都没有超过10。

二十个请求在两秒内到达,这就是边界突发流量。而这个结果,只需要留意分钟切换的那一刻就能察觉。

2. 被拒绝。 窗口已满,而且还有三十秒才结束。服务完全空闲,客户端却还是要等待,这正是突发流量之后紧跟着的那种干涸期。

3. 不能。 不管限额设成多少,固定窗口都做不到这一点。任何跨越边界的时钟时段,都可能承载多达两倍限额的流量,这是把时间切成固定区块这种做法本身的属性,不是调调参数就能解决的问题。

要拿到这种保证,就得让统计的窗口随请求本身移动,而不是卡在日历上的某个固定区块——这正是下一章要讲的内容。

接下来往哪走

固定窗口成本低、逻辑简单,但在边界处最多可能出现两倍的误差。这重不重要,取决于这个端点具体是做什么的。

在动手修复它之前,先把它造出来会更有帮助。构建一个限流器会把这个算法变成一个真正能跑起来的Express中间件,还会带上客户端需要用来正确应对限流的那些响应头。