速率限制基础
一个客户端每秒能发起一千个请求。HTTP 本身没有任何机制能阻止这种事,而你的服务器会试图应答每一个请求。
速率限制(Rate limiting)控制着一个服务能接受多大的流量。它决定了一个接口是能扛过糟糕的一下午,还是会因为一个写得太"起劲"的脚本而彻底崩掉。
它解决的三个问题
公平使用。 如果一个客户端占用了大量资源,其他人就都得等着。设置限制能防止单个调用方消耗掉整个用户群所需要的资源,这样在拦下某个失控循环的同时,正常流量依然能被正常处理。
安全性。 一个没有限制的登录接口,等于是邀请别人一次猜上成千上万个密码。限制能让暴力破解慢到毫无意义,也能削弱那些更简单的拒绝服务攻击。
资源。 过载会先让服务变慢,然后才彻底不可用,而"变慢"正是用户开始流失的时刻。限制吞吐量能让所有仍在限额内的用户获得可预期的响应时间。
单靠限流并不能防御 DDoS
分布式拒绝服务攻击来自成千上万台不同的机器,每一台看起来都像是普通客户端。按地址设置的限制对此几乎不起作用。
要应对它,需要更上游的手段:地理位置过滤、行为分析,或者由一个专门的服务商挡在你前面。速率限制只是其中一层防护,而不是全部答案。
限制每个人能请求的频率,正是让一个应用既难以被滥用,又能让所有其他用户用得舒心的关键所在。
拒绝还是放慢
有两种经常被混用、但实际行为完全不同的控制手段。
| 速率限制 | 限流(Throttling) | |
|---|---|---|
| 它做什么 | 拒绝超出上限的请求 | 在接近限制时放慢请求速度 |
| 客户端看到什么 | 一个错误,通常是 429 Too Many Requests | 一个响应,只是比平时来得晚 |
| 最适合 | 需要严格执行的硬性上限 | 在不打断客户端的前提下平滑突发流量 |
速率限制在超过上限后予以拒绝,限流则是放慢速度。
两者各有用武之地,也可以结合使用:随着流量上升先放慢速度,超过硬性上限后再彻底拒绝。限流一文详细讲了后半部分。
一种会直接打断客户端的请求,另一种只是让它等一等。哪种更"友善",完全取决于是谁在请求、又是为了什么。
限制是针对谁计数的
限制需要一个计数的对象。有四个候选项,各有不同的权衡:
- IP 地址。 每个请求(包括匿名请求)都能拿到,但同一个办公网络或移动运营商背后的所有人会共享同一个地址。
- 已认证的用户 ID。 精确且公平,但只有登录之后才存在。
- API 密钥。 对机器客户端来说很理想,因为一个密钥能精确标识一个调用方。
- JWT 中的某个声明(claim)。 当身份信息本来就携带在令牌里时很有用。
识别客户端一文讲的就是如何在这几者之间做选择。简单来说:匿名流量只能靠地址,其他方式只要能用,效果都会更好。
限制还需要一个时间窗口。按秒、按分钟、按小时——这个选择对行为的影响,不亚于数字本身。
每一条速率限制规则其实都是这三个决定的组合,改变其中任何一个,限制实际发挥的作用就会完全不同。
动手试试
某个 API 只有一条限制:每个 IP 地址每小时 1000 次请求。想一想它防不住什么。
- 一个大学网络里的用户,在学期中无法正常使用这个应用。为什么?
- 攻击者想每小时发起 10000 次请求。这会让他付出什么代价?
- 一个脚本在每小时开始的头两秒钟内,就把 1000 次请求全部发完。这算不算在限制范围内?
核对你的答案
1. 这条限制计数错了对象。 那个网络上的所有人共享同一个出口地址,也就是说成千上万人在共用一份"一千次"的额度。修复方法是:只要用户已认证,就按用户计数;地址限制只用于匿名流量。
2. 十个地址。 住宅代理池以极低的价格成批出售这类地址。按地址限制只是让滥用付出一点代价,并没有真正让滥用变得困难。这也是为什么按 IP 限制对分布式攻击几乎不起作用。
3. 完全在限制范围内。 规则并没有规定这一千次请求应该如何分布,所以它们完全可以一次性同时到达,而这个接口从一开始就没有针对这种突发流量做过防护。这正是时间窗口长度决定的问题:选一个小时作为窗口,就会放行一分钟窗口绝不会允许的流量尖峰。
三个问题,三种不同的薄弱环节,只有第三个才真正跟算法有关。前两个都关乎你计数的对象是什么,以及它被替换的成本有多低。
接下来看什么
一条限制需要三个决定:多少次、在多长的窗口内、针对谁计数。算法则决定了你如何追踪这些数字。
固定窗口计数器是这些算法里最简单的一种,而它放行的那种突发流量,正是后续每一种算法要解决的问题。

