把 API 限流做对:令牌桶、分布式配额与降级响应

API 限流与流量治理

限流不是简单地把“每秒 100 次”写进网关。突发请求是否允许、配额按用户还是按接口计算、多个实例如何共享计数、拒绝后客户端怎样恢复,都会决定系统是在高峰期有序降速,还是制造更多重试。可靠的限流应该既保护容量,也保持公平,并给调用方清晰、可执行的反馈。

先定义限流边界

配置算法之前,先回答“限制谁访问什么资源”。只按 IP 限流容易误伤共享出口后的用户,也可能被代理轮换绕过;只按接口限流,又会让一个大客户吃掉全部容量。常见的限流键可以组合多个维度:

1
limit_key = tenant_id + user_id + http_method + route_group
维度 适合解决的问题 注意事项
用户或 API Key 防止单个调用方占满容量 登录前要准备 IP 等备用身份
租户 保证多租户之间公平 大租户可配置独立套餐
接口组 隔离昂贵操作与普通查询 不要让健康检查消耗业务配额
全局入口 保护服务的硬容量上限 应放在细粒度配额之后兜底

限流值应来自容量测试和业务优先级,而不是拍脑袋。读缓存、生成报表、创建订单消耗的资源完全不同,统一使用一个阈值通常没有意义。

为什么令牌桶更实用

令牌桶吸收突发流量

固定窗口实现简单,但在窗口交界处可能连续放过两批请求;滑动窗口更平滑,却需要保存更多计数;漏桶强调恒定流出,适合整形队列;令牌桶则同时表达了长期速率和允许的突发量,常用于在线 API。

令牌桶有两个核心参数:每秒补充 rate 个令牌,桶最多保存 capacity 个令牌。请求到达时先按经过时间补充,再消耗一个令牌;令牌不足就拒绝。capacity 决定可接受多大的瞬时突发,rate 决定长期吞吐。

1
2
3
4
5
6
7
8
9
10
11
12
function allow(bucket, nowMs) {
const elapsed = Math.max(0, nowMs - bucket.updatedAt) / 1000;
bucket.tokens = Math.min(
bucket.capacity,
bucket.tokens + elapsed * bucket.rate
);
bucket.updatedAt = nowMs;

if (bucket.tokens < 1) return false;
bucket.tokens -= 1;
return true;
}

单进程实现应使用单调时钟计算间隔,避免系统校时让时间倒退。测试时至少覆盖空桶、满桶、长时间空闲、边界并发和大幅时钟跳变。

多实例下保证原子性

应用扩展到多个实例后,每个实例各放一只完整配额的桶,会把实际上限放大为“实例数 × 配额”。严格的全局限流需要把状态放到共享存储,并把“读取、补充、判断、扣减”作为一次原子操作执行,例如在 Redis 中使用 Lua 脚本。

分布式限流的共享状态

共享限流器也会成为依赖,因此要提前选择失败策略:支付、发券等高风险写入可在限流器不可用时保守拒绝;公开只读接口可以临时切换到实例级限流,但要缩小每个实例的额度。不要无条件放行,也不要让所有接口采用同一种策略。

还要控制状态规模。限流键设置过细会产生大量冷数据,应给不活跃桶设置过期时间;热点租户会形成单键竞争,可以为它单独分配更高容量或独立分片。对不要求绝对精确的场景,也可以把少量额度分发到各实例,用轻微误差换取更低延迟。

拒绝也要成为接口契约

触发限流时返回 429 Too Many Requests,并用 Retry-After 告诉客户端最早何时重试。响应体要提供稳定的错误码,而不是让客户端解析自然语言。

1
2
3
4
5
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/json

{"code":"RATE_LIMITED","retry_after_seconds":2}

客户端收到 429 后应遵守等待时间,并加入随机抖动,避免大量请求同时苏醒。服务端不要为被拒绝的请求执行昂贵鉴权、查询或日志序列化;限流位置越靠近入口,保护效果越明确,但身份识别仍应在可信认证之后完成。

上线时观察什么

只看 429 总数无法判断限流是否合理。至少按租户、接口组和结果记录允许量、拒绝量、剩余令牌、限流器延迟与存储错误。告警要区分“某个用户超额”和“全局入口持续拒绝”:前者可能符合预期,后者通常意味着容量不足或下游退化。

上线前可以按这份清单检查:

  • 明确每条规则的限流键、长期速率、突发容量和业务依据。
  • 用并发测试验证窗口边界与多实例下的总配额。
  • 为共享存储超时准备按接口区分的失败策略。
  • 确认 429、Retry-After、错误码和客户端退避形成完整契约。
  • 灰度观察误伤率、拒绝来源和保护对象的延迟,再逐步收紧阈值。

好的限流不是追求每一秒都绝对精确,而是在公平、容量、可用性和实现成本之间建立可解释的边界。把令牌桶算法、分布式状态和失败响应一起设计,流量高峰才会变成可治理的降速,而不是一次难以恢复的雪崩。