速率限制(rate limiting),也称为请求限流(request throttling),是一种控制用户或服务向 API 或服务器发送请求速率的机制,用于防止滥用、保证资源的公平分配并维持系统稳定。
理解速率限制
速率限制保护服务免受过量请求的冲击,避免性能下降、拒绝服务(DoS)攻击或资源耗尽。它确保系统资源对所有合法用户可用,并防止单一主体独占访问。代理服务在实施这些限制时通常扮演关键角色:既可以在客户端请求到达上游服务之前对其施加限制,也可以将上游的限制信息回传给客户端。
为什么要实施速率限制?
- 资源保护: 防止服务器被过多请求压垮,节省 CPU、内存和网络带宽。
- 防止滥用: 通过限制请求尝试次数,缓解暴力破解、撞库(credential stuffing)等恶意行为。
- 公平使用: 确保所有客户端公平地访问共享资源,防止单个客户端独占系统。
- 成本控制: 对于按用量计费的服务,速率限制可通过设定资源消耗上限来控制运营成本。
常见的速率限制算法
实现和执行速率限制有多种算法,它们在处理突发流量和资源占用方面各有特点。
令牌桶(Token Bucket)
Token Bucket 算法把限流建模为一个容量固定的桶,桶以恒定速率补充令牌。每个请求消耗一个令牌。桶为空时,请求被拒绝或排队。该算法允许一定程度的突发:只要桶内有令牌,请求可以在桶容量范围内一次消耗多个令牌。
漏桶(Leaky Bucket)
Leaky Bucket 算法以固定的输出速率处理请求。请求被加入队列(即"桶")。队列满时,新请求被拒绝。请求以恒定速率从桶中"漏出",保证处理流量平稳。该算法能削平突发,但会给必须在队列中等待的请求带来延迟。
固定窗口计数器(Fixed Window Counter)
在 Fixed Window Counter 算法中,先定义一个时间窗口(例如 60 秒),由计数器统计该窗口内的请求数。窗口到期后计数器归零。窗口内超出限额的请求被拒绝。其缺点是窗口边界处的"突发问题":客户端可能在两个相邻窗口内发送两倍于限额的请求。
滑动窗口日志(Sliding Window Log)
Sliding Window Log 算法为每个请求记录一个时间戳。新请求到达时,系统统计最近 N 秒(即窗口)内的时间戳数量。若该数量超过限额,请求被拒绝。该方法精确,但因为要保存所有时间戳,内存开销较大。
滑动窗口计数器(Sliding Window Counter)
该算法结合了 Fixed Window 与 Sliding Window Log 的特点,在不承担逐条记录请求的内存开销的前提下缓解边界问题。它使用两个固定窗口:当前窗口和上一个窗口。当前请求的时间戳决定其在当前窗口中的位置。允许的请求数按当前窗口已过去的比例,取上一窗口计数与当前窗口计数的加权平均值。
算法对比
| 特性 | Token Bucket | Leaky Bucket | Fixed Window Counter | Sliding Window Log | Sliding Window Counter |
|---|---|---|---|---|---|
| 突发处理 | 允许突发 | 削平突发 | 易受突发影响 | 处理良好 | 处理良好 |
| 资源占用 | 中等 | 中等 | 低 | 高(内存) | 低到中等 |
| 复杂度 | 中等 | 中等 | 低 | 高 | 中等 |
| 精确度 | 良好 | 良好 | 差(边界情况) | 高 | 良好 |
| 延迟影响 | 低(有令牌时) | 高(排队) | 低 | 低 | 低 |
识别速率限制
超出速率限制时,API 或服务通常会返回特定的 HTTP 状态码和响应头。
HTTP 状态码 429 Too Many Requests
速率限制的标准 HTTP 状态码是 429 Too Many Requests,表示用户在给定时间内发送了过多请求。
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{
"error": "Rate limit exceeded. Try again in 30 seconds."
}
响应头
API 通常会附带特定的响应头,提供更多关于限流状态及应对方式的信息。
Retry-After:(RFC 7231 第 7.1.3 节)指示 user agent 在发起后续请求前应等待多久。取值可以是表示秒数的整数,也可以是具体的日期/时间。X-RateLimit-Limit: 当前限流窗口内允许的最大请求数。X-RateLimit-Remaining: 当前限流窗口内剩余的请求数。X-RateLimit-Reset: 当前限流窗口重置的时间(通常为 Unix epoch 秒)。
这些 X-RateLimit-* 响应头很常见,但并未被任何 RFC 标准化;不同服务的具体命名和行为可能不同。
应对速率限制
在客户端妥善处理速率限制,是构建与外部服务交互的健壮应用的关键。
带抖动的指数退避
这是重试失败请求(包括因限流而失败的请求)的标准策略。
- 指数退避: 客户端在两次重试之间等待呈指数增长的时间(例如 1 秒、2 秒、4 秒、8 秒)。
- 抖动(jitter): 在退避时长上叠加一个小的随机延迟。这样可以避免限流重置后所有客户端同时重试,从而引发新一轮限流。
import time
import random
import requests
def make_request_with_retry(url, max_retries=5):
retries = 0
while retries < max_retries:
try:
response = requests.get(url)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 1))
print(f"Rate limited. Retrying after {retry_after} seconds.")
time.sleep(retry_after)
elif 200 <= response.status_code < 300:
return response
else:
response.raise_for_status() # 其他 HTTP 错误抛出异常
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
# 带抖动的指数退避
delay = (2 ** retries) + random.uniform(0, 1) # 2^retries + 0 到 1 之间的随机小数
print(f"Retrying in {delay:.2f} seconds...")
time.sleep(delay)
retries += 1
raise Exception(f"Failed to make request after {max_retries} retries.")
# 用法示例:
# response = make_request_with_retry("https://api.example.com/data")
# if response:
# print("Request successful:", response.json())
遵守 Retry-After 响应头
如果 API 返回了 Retry-After 响应头,客户端必须遵守该指令。其值规定了向同一 endpoint 再次发送请求前的最短等待时间。
客户端缓存
对访问频繁且不常变化的 endpoint 的响应做缓存。这能减少发往 API 的请求数量,间接帮助您保持在限额之内。
合并请求
如果 API 支持,请把多个小操作合并成一次较大的请求,从而减少 API 调用总数。
预测性限流
客户端可以监控自身的请求速率,在接近已知限额时主动降速或暂停,而不是等到收到 429 响应。这需要事先了解该 API 的速率限制。
面向速率限制的代理服务配置
健全的代理服务提供完善的限流管理能力,既覆盖通过代理访问服务的客户端,也覆盖代理自身与上游 API 的交互。
对入站流量施加限制
代理可以按多种维度对客户端的入站请求施加速率限制。
- 客户端 IP 地址: 限制来自单个 IP 的请求。
- API 密钥/令牌: 限制与特定认证凭据关联的请求。
- 用户 ID: 当代理能从响应头或令牌中提取用户信息时使用。
- 路径/endpoint: 为不同的 API endpoint 设置不同限额(例如
/search的限额可以高于/admin/delete)。
# 示例:按 IP 限流的代理配置
http:
routers:
api-router:
rule: "Host(`api.example.com`)"
service: api-service
middlewares: [rate-limit-ip]
middlewares:
rate-limit-ip:
rateLimit:
average: 100 # 每秒请求数
burst: 50 # 超出平均值的最大突发量
sourceCriterion: "ipStrategy" # 按来源 IP 分别限流
管理发往上游服务的出站流量
当代理自身调用上游 API 时,它可以实施自己的速率限制,以免压垮这些外部服务。在代理需要汇聚多个数据源的集成场景中,这一点尤为关键。
- 针对上游的独立限额: 为代理对接的每个上游服务分别配置速率限制。
- 熔断(circuit breaking): 将速率限制与熔断器模式结合,在上游服务无响应或持续对代理限流时隔离故障。
定制化与粒度
高级代理配置支持对速率限制进行精细控制:
- 动态限额: 根据后端健康状况、时段或其他运营指标调整限额。
- 分级限额: 为不同等级的客户端设置不同限额(例如免费用户与付费用户)。
- 配额管理: 在短期限流之外,按更长周期的配额(例如每月请求数)统计用量。
监控与告警
代理服务应提供监控速率限制统计数据的工具:
- 请求计数: 统计请求总数、成功请求数以及被限流的请求数。
- 限额突破: 当特定客户端或上游服务接近或超出限额时发出告警。
- 用量趋势: 可视化一段时间内的请求模式,识别潜在瓶颈或滥用行为。
监控有助于运维团队理解流量模式、优化速率限制配置,并在问题影响服务可用性之前主动处理。
