题干与适用场景
题目考察系统设计者能否把“每分钟 N 次”变成可扩展、可运营的控制面与数据面。限流既要保护服务容量,也要保留公平性和业务优先级;分布式节点、时钟、共享状态不可避免会带来近似误差。回答应覆盖策略、算法、存储、响应、降级、发布和监控。
面试官考察点
强回答会先澄清限流维度、窗口、突发容量和失败语义,再比较 token bucket、leaky bucket、fixed/sliding window。它会把策略分发与请求判定分离,说明原子计数、热点 key、跨区域和 Redis 故障处理,并返回 Retry-After 等可执行信号。最后要说明 shadow rollout、错误预算和绕过保护的审计。
回答前需要澄清的问题
- 按 API、租户、用户、IP 还是组合键限流?是否有管理员、付费客户等优先级?
- 目标是平均速率、突发容量、并发数,还是同时控制多种资源?
- 超限返回拒绝、排队、降级,还是允许小比例超发?跨区域是否要求严格一致?
- 限流器不可用时 fail-open 还是 fail-closed?哪些接口必须保护?
- 策略多久变更一次,是否需要灰度、审计、回滚和实时生效?
30 秒回答框架
“我会把策略控制面和请求数据面分开。每个请求按租户、用户和路由生成规范化 key,数据面用 Redis Lua 或原子脚本执行 token bucket,返回剩余配额和重试时间。策略按版本缓存,节点本地缓存只做快速拒绝,最终判定走共享状态。共享存储短暂不可用时,低风险接口按租户预算 fail-open,高风险接口 fail-closed 并告警。通过 shadow 模式、burn rate、热点 key 和跨区误差监控逐步发布。”
分步骤深入解答
第一步:定义容量与公平性
先将容量分为 sustained rate、burst size 和并发上限。租户级额度防止大客户挤占全局资源,用户级额度防止单个租户内部噪声;关键接口可以有独立预算。公平性是业务规则,不应藏在算法默认值中。
第二步:选择算法
Token bucket 允许受控突发,适合 API;leaky bucket 更强调平滑出流;滑动窗口更直观但状态和成本较高。用单调时间计算 token,不依赖跨节点墙上时钟;明确过期 key 的清理和精度误差。
第三步:设计 key 与策略模型
规范化 method、route、tenant、principal 和 region,避免不同节点生成不同 key。策略包含 limit、burst、scope、优先级、版本、生效时间和 owner。拒绝未知策略默认进入安全基线,不让客户端自带额度。
第四步:实现原子判定
共享存储中一次完成读取、补充 token、扣减和 TTL 更新。Redis Lua、原子数据库操作或专用 sidecar 都可行;关键是避免“读后写”竞态。响应返回剩余 token、reset 时间和策略版本,便于客户端退避。
第五步:处理热点与多区域
热门租户会把单 key 变成热点。可将额度分片并由协调器合并,但要说明超发误差;多区域可以采用区域配额,异步汇总全局使用量。严格全球一致会牺牲可用性和延迟,必须由业务选择。
第六步:设计故障与降级
限流器故障时,按接口风险选择 fail-open 或 fail-closed,并设置本地应急预算。策略服务不可用时使用最后已知版本和短 TTL;共享状态恢复后不能一次性释放全部积压。所有绕过和降级都写审计事件。
第七步:发布与变更
新策略先 shadow 计算只记录不拒绝,比较预计拒绝量和业务影响;再按租户或百分比灰度,最后全量。策略版本进入请求日志,支持回滚。变更接口需要 owner、审批和过期时间,避免无限临时豁免。
第八步:监控效果
监控允许/拒绝率、剩余配额、p95 判定延迟、热点 key、存储错误、策略版本、误拒绝申诉和后端过载。将限流指标与 5xx、队列长度、尾延迟关联;限流过少保护不足,过多则伤害业务。
原子 token bucket 伪代码
now = monotonic_time()
state = load(key) or {tokens: burst, at: now}
elapsed = now - state.at
state.tokens = min(burst, state.tokens + elapsed * rate)
allowed = state.tokens >= cost
if allowed:
state.tokens -= cost
state.at = now
save_atomically(key, state, ttl)
return allowed, state.tokens, retry_after(state)设计取舍与边界
| 选择 | 适用 | 主要代价 |
|---|---|---|
| Token bucket | API 突发流量 | 需要共享原子状态 |
| 本地限流 | 低延迟保护 | 多节点额度不精确 |
| 区域配额 | 多区域可用性 | 全局公平存在误差 |
| Fail-closed | 支付、鉴权等高风险 | 限流器故障会扩大可用性影响 |
限流不是队列,也不能替代容量规划、熔断或身份鉴权。排队适合可等待请求,限流适合在资源边界快速拒绝;两者混用会把延迟隐藏起来。
落地计划与证据
先为一个高流量 API 建立策略模型和 token bucket 数据面,shadow 一天,再灰度到少数租户。Microsoft Well-Architected 将 throttling 视为过载时的主动控制手段;DataInterview 与 System Design School 的面试材料都强调算法选择、共享状态和 Retry-After 响应。
试点的退出条件
在演练突发流量、共享存储故障、策略回滚和热点租户后,服务仍满足延迟目标;误拒绝有可追踪原因;降级预算有效;所有策略有 owner、版本和审计记录。
怎样证明收益不是巧合
比较启用前后的后端过载次数、5xx、p99 延迟、拒绝率、误拒绝率和存储成本,并按租户、路由和区域分层。流量变化时用容量归一化,不把自然低峰误判为限流成功。
常见误区与追问
每个节点本地计数就够了
节点会各自放行额度,客户端切换节点后可能超发。若接受近似,要量化误差;关键接口需要共享原子状态或区域预算。
只用固定窗口
窗口边界会允许短时间双倍突发。可用 token bucket 或滑动窗口,并说明状态成本与精度。
限流器挂了就全部放行
高风险接口会失去最后保护。按业务风险设置本地应急预算、fail-open/closed 和告警,不做一刀切。
如何避免误伤大客户?
按租户配置额度和优先级,分离共享池与保底池,并在 shadow 阶段评估预计拒绝量。临时豁免必须有期限和审计。
客户端应如何重试?
尊重 Retry-After,使用带抖动的退避,不对 429 无限重试。幂等请求才适合自动重试。
如何灰度一条新策略?
先 shadow 只计算,随后按租户或百分比启用,比较拒绝量、业务转化和后端负载;策略带版本以便快速回滚。