代表性面试主题

系统设计面试:如何设计多租户 API 限流器?

系统设计困难
OfferCC 编辑团队发布 更新

题干

请为多租户 API 平台设计限流器:免费租户每分钟 100 次、付费租户每分钟 10,000 次,平台有多个应用实例和区域。请比较算法,说明共享状态、429 响应、故障降级与验证方法。

题干与适用场景

请为多租户 API 平台设计限流器:免费租户每分钟 100 次、付费租户每分钟 10,000 次,平台有多个应用实例和区域。请比较算法,说明共享状态、429 响应、故障降级与验证方法。

这道题适合后端、平台和系统设计岗位。公开面试记录给出了按用户维护 token bucket、按时间补充令牌的编码题;系统设计资料则把限流列为多租户 API 的典型讨论。分类依据是跨实例共享状态、租户策略和流量保护,不是某个云厂商的配置记忆。

面试官考察点

  • 能否先区分平均速率、突发容量、租户公平和全局保护目标。
  • 能否解释 token bucket、fixed window、sliding window 的边界与代价。
  • 能否把限流状态放到原子共享存储,并处理热键、分区和时钟问题。
  • 能否定义 429、重试提示、降级和观测,使限流器本身不会成为单点。

回答前需要澄清的问题

  • 限制对象是 API key、租户、用户、IP,还是它们的组合?匿名请求如何归属?
  • 限制是平均速率、严格滑动窗口,还是允许可控突发?是否还需要日配额?
  • 多区域要求全局精确,还是允许短暂超额换取可用性?
  • 被拒请求应立即返回 429,还是进入有界队列等待?下游是否能承受任何突发?

30 秒回答框架

我会先在网关做粗粒度的 IP 和未认证流量保护,再在应用层按租户策略做精确限流。若 API 允许短突发,默认选 token bucket;每个桶保存令牌数和上次更新时间,按租户维度放在可原子更新的共享存储。超限返回 429 和可计算的重试提示。多区域若不要求精确全局计数,就按区域配额和超额监控换取低延迟;存储不可用时采用明确的 fail-open 或 fail-closed 策略,并持续告警。

分步骤深入解答

先定义预算与公平性

免费租户的 100 次/分钟是长期预算,付费租户的 10,000 次/分钟也是长期预算;两者都需要独立 burst capacity,不能只用一个全局计数器。全局保护还应限制所有租户合计的 RPS,防止单个大租户把数据库连接池耗尽。策略键至少包含租户 ID、API 动作和策略版本,避免不同接口意外共享额度。

选择算法

算法主要语义成本与风险适用条件
Token bucket平均速率受限,允许容量以内突发两个状态值,需原子补充与扣减面向用户的 API,短突发可接受
Fixed window固定时间段计数窗口边界可能接近两倍流量规则简单、允许近似
Sliding window log精确统计窗口内每个请求保存时间戳,内存和清理成本高小规模且需要严格精度
Sliding window counter用相邻窗口加权估算近似但内存低,需说明误差大规模公平限流

AWS API Gateway 文档明确说明其 throttling 使用 token bucket,令牌速率和 burst 容量分别表达稳态速率与突发上限;超限可能返回 429,但限制是 best effort,不应当当作数学上绝对的天花板。回答时要把“策略目标”和“平台保证”分开。

Token bucket 的不变量

令牌数 tokens 始终位于 [0, capacity]。请求到达时,先用经过时间乘 refill rate 补充令牌并截断到 capacity,再检查是否至少有一个令牌;允许请求就扣减一个。这个顺序保证空闲期间只积累到 burst 上限,不会因为停机时间制造无限额度。

~~~text allow(key, now): state = atomicRead(key) elapsed = max(0, now - state.lastRefill) refilled = min(capacity, state.tokens + elapsed * rate) if refilled < 1: atomicWrite(key, refilled, now) return reject(429) atomicWrite(key, refilled - 1, now) return allow ~~~

伪代码必须在真实实现中由一次 Lua 脚本、事务或等价 compare-and-swap 原子执行;先读后写的两个独立网络请求会在并发下超发令牌。时间戳应由可信的单调时间来源处理,不能让客户端提交时间。

部署层级与共享状态

网关层负责挡住明显的 IP 洪泛和未认证流量;应用层按租户、用户或接口做业务限额。所有实例若只在本地内存计数,负载均衡后一个租户可以把请求分散到多个实例而获得多份额度。共享 Redis、带原子脚本的键值存储或具备条件写的数据库都可以承载状态,选择取决于延迟、精度和故障模型。

多区域有三种取舍:单一全球存储提供更精确的额度但增加跨区延迟;每区独立桶延迟低但可能产生短暂超额;租户先分配区域配额并设全局保护则介于两者之间。必须先问清“精确”是否比“可用”重要,不能把区域拆分后仍声称全局严格不超限。

拒绝、降级与观测

超限返回 429,并提供 Retry-After 或响应头中的剩余额度;客户端应使用有上限的退避,不能立即重试形成反馈环。限流存储故障时,高风险写接口通常 fail-closed 或进入有界队列,低风险读接口可短时 fail-open 但必须有本地熔断、过期时间和总量保护。监控应区分允许率、拒绝率、每租户额度命中、存储延迟、热键、脚本错误和实际下游负载。

高质量示范回答

我会把限流分两层:网关先按 IP 和认证状态做粗粒度保护,应用层再按租户和接口执行业务策略。免费租户和付费租户分别拥有 rate 与 burst 两个参数;默认用 token bucket,因为 API 通常允许短突发而仍需限制长期平均速率。每个桶保存令牌数和上次补充时间,补充、检查和扣减由共享存储中的原子脚本完成。

多个区域如果不要求每次请求都得到全球精确结果,我会按区域分配配额,并用全局指标检测异常超额;若支付或配额结算必须严格,就把决策放到一致性更强的中心服务并接受延迟。超限返回 429 与重试提示,存储故障按接口风险选择 fail-closed、短时 fail-open 或有界队列。最后用并发压测验证突发容量、窗口边界、租户公平、故障恢复和下游负载,而不是只检查 429 数量。

常见错误

  • 只说“用 Redis 计数” → 没有算法语义和原子更新 → 写出桶状态、脚本边界与故障策略。
  • 所有租户共用一个限额 → 大租户可以挤占小租户 → 按租户和接口拆分策略,并补充全局保护。
  • 把 fixed window 当成严格每分钟上限 → 窗口边界可形成近两倍短时流量 → 说明误差并换 sliding 或 token bucket。
  • 限流存储故障就无限放行 → 保护组件失效后下游先崩溃 → 按业务风险选择有界降级并告警。
  • 让客户端立即重试 429 → 拒绝流量变成更高负载 → 返回重试提示并要求退避、抖动和上限。

追问及应对

同一租户的请求被分到 20 个实例,如何避免额度放大?

把桶状态放到所有实例可见的共享存储,并以租户策略键做原子更新。若暂时只能本地计数,就必须把它表述为近似保护,并设置网关总量上限,不能声称全局精确。

跨区域必须严格执行每分钟 100 次,怎么办?

优先选择一致性更强的全球决策点或按租户固定主区域串行化扣减;代价是跨区延迟和区域故障时的可用性下降。若业务允许短暂超额,可改用区域配额加全局对账,并把误差写进 SLO。

限流器本身变慢,如何防止它拖垮 API?

给限流调用设置严格超时和熔断,避免无限等待;预留本地保护上限,在共享存储不可用时按接口风险降级。把限流延迟、超时和脚本错误单独监控,不能只看业务接口的最终成功率。

什么时候应该排队而不是返回 429?

如果请求可异步处理、等待不会超过用户预算且下游必须平滑输入,可以使用有界队列;队列满时仍需拒绝。交互式读请求或不可取消的长队列通常直接 429 更诚实。

如何验证 fixed window 的边界问题?

构造窗口结束前发送一批、窗口开始后立即发送一批的测试,统计任意连续窗口内的真实请求数;再对 token bucket 的空闲积累、满桶突发和并发扣减做同样的时间驱动测试。

公开来源

同类题目

相关面试工具

用 Solve 整理系统设计回答

从澄清需求开始,展开规模、架构、组件选择和取舍。

查看工具