固定窗口计数器
想象一个有五个车位、装着道闸的停车场。每三十秒道闸就会重置,车位又空出来了。
这就是固定窗口计数器,最简单的限流算法。在一个固定长度的窗口内统计请求数,超过限制就拒绝,窗口结束时把计数清零。
看一个窗口是怎么填满的
限制为每三十秒五个请求:
| 时间 | 到达情况 | 计数 | 结果 |
|---|---|---|---|
| 0秒 | 1个请求 | 1 | 接受 |
| 5秒 | 2个请求 | 3 | 接受 |
| 15秒 | 2个请求 | 5 | 接受,道闸落下 |
| 25秒 | 1个请求 | 5 | 拒绝,窗口已满 |
| 30秒 | 第二个窗口开启 | 0 | 计数重置 |
这里涉及的状态只有两个值:现在处于窗口的什么位置,以及目前的计数是多少。上一个窗口结束后,什么都不会留到下一个窗口。
这种简单性正是它的全部魅力所在。存储成本低,检查成本低,也很难把逻辑搞乱。
这说明这个算法是直来直去的,而不是不公平——它没有"差不多"的概念,只有"在这个窗口里"或者"不在"这两种情况。
边界处的突发流量
现在来看连续两个窗口,限制仍然是每三十秒五个:
| 时间 | 窗口 | 请求数 | 累计计数 |
|---|---|---|---|
| 50秒 | 窗口2 | 2 | 4 |
| 55秒 | 窗口2 | 2 | 6,实际接受5个 |
| 60秒 | 窗口3开启 | 2 | 2 |
不要看窗口,看时钟。在50秒到60秒这十秒之间,一共有七个请求被接受了。
限制是每三十秒五个,结果十秒里来了七个。
这里没有任何故障。窗口2末尾和窗口3开头的请求分属不同的窗口,所以任何一个窗口的计数都没有超过五。算法完全按照它被告知的规则在运行。
这就是窗口边界问题,也是这种方法最根本的弱点。在最坏的情况下,客户端可以在重置前一瞬间发送满额请求,紧接着在重置后一瞬间再发送一次满额请求,在极短时间内打入两倍限额的流量。
但如果你退后一步,看跨越边界的一段时钟时间,就会发现限制其实根本没有真正生效过。
动手试一试
某个API允许每60秒窗口内10个请求,窗口按时钟对齐,从整分钟开始。
- 一个客户端在12:00:59发送了10个请求,又在12:01:01发送了10个请求。有多少个被接受?限制被打破了吗?
- 一个客户端在12:00:00时发送了全部10个请求。它在12:00:30发送的请求会怎样?
- 你需要保证任何客户端在任意60秒的时间段内都绝不会得到超过10个请求的响应。固定窗口能做到吗?
对照一下你的答案
1. 全部20个都被接受,规则也没有被打破。 前十个落在12:00这个窗口,后十个落在12:01这个窗口,两个窗口的计数都没有超过10。
二十个请求在两秒内到达,这就是边界突发流量。而这个结果,只需要留意分钟切换的那一刻就能察觉。
2. 被拒绝。 窗口已满,而且还有三十秒才结束。服务完全空闲,客户端却还是要等待,这正是突发流量之后紧跟着的那种干涸期。
3. 不能。 不管限额设成多少,固定窗口都做不到这一点。任何跨越边界的时钟时段,都可能承载多达两倍限额的流量,这是把时间切成固定区块这种做法本身的属性,不是调调参数就能解决的问题。
要拿到这种保证,就得让统计的窗口随请求本身移动,而不是卡在日历上的某个固定区块——这正是下一章要讲的内容。
接下来往哪走
固定窗口成本低、逻辑简单,但在边界处最多可能出现两倍的误差。这重不重要,取决于这个端点具体是做什么的。
在动手修复它之前,先把它造出来会更有帮助。构建一个限流器会把这个算法变成一个真正能跑起来的Express中间件,还会带上客户端需要用来正确应对限流的那些响应头。

