先说结论

限流用于把进入系统的工作量控制在可承受范围。令牌桶允许按速率补充并容纳一定突发,漏桶把输出整形成稳定速率,滑动窗口比固定窗口更平滑。算法只是基础,关键是限流维度、阈值来源、分布式一致性和拒绝后的响应策略。

阈值应来自压测容量、依赖瓶颈和 SLO,并预留故障期间的容量余量,不能凭机器配置猜测。

分层设计

边缘层拦截非法与总体洪峰,网关按租户、接口和用户配额,服务内部按线程池、数据库或第三方依赖做并发限制。关键交易和普通查询使用不同优先级,防止低价值流量耗尽资源。

四种算法的形状

算法 流量形状 突发能力 典型实现
固定窗口 窗口边界可能突刺 窗口内可突发 简单计数器
滑动窗口 比固定窗口平滑 取决于窗口 分桶计数、时间序列
令牌桶 平均速率 + 可控突发 bucket 容量 令牌按速率补充
漏桶 稳定输出速率 通常不保留大突发 队列按固定速率流出

令牌桶适合 API 访问既有平均配额又允许短突发;漏桶适合保护下游,让输出平滑;滑动窗口实现直观但存储和分布式计数成本更高。算法名称不是需求,先定义每秒、每分钟、并发数和队列长度的限制维度。

从容量反推阈值

假设数据库在 P99 目标下能承受 2000 QPS,当前有 4 个应用实例,每实例预留 20% 故障余量,则应用层安全配额可从约 1600 QPS 起步,再按接口成本分配。一个复杂报表请求不能和一个简单健康查询共用相同权重,最好使用 cost/weight 或独立资源池。

阈值要随扩缩容、下游降级和业务高峰调整。静态全局阈值在实例数变化后可能过严或过松,可使用中心配置、按实例权重分配并在中心故障时回退到保守本地限流。

分布式计数与误差

Redis Lua、网关插件或集中式令牌服务能提供统一视图,但每次请求多一次网络依赖,中心故障会影响入口。本地限流延迟低但多个实例会产生配额误差。严格金融额度要用原子存储和明确一致性;普通 API 防洪峰可接受近似和本地快速拒绝。

不要使用客户端时间直接计算分布式窗口,时钟漂移会造成边界错误;使用服务端时间或令牌服务的单调时间语义。

分布式实现

本地限流延迟低但只能约束单实例,可按实例权重分配配额并定期调整;集中式 Redis 或网关可统一计数但引入网络和可用性成本。极端流量下本地快速拒绝应作为兜底。

容易踩坑的地方

限流只返回 429 而没有重试建议、降级内容和监控,会把压力转成客户端重试风暴。阈值固定不变也无法适应扩缩容、依赖降级和昼夜流量。

限流后的用户体验

429 响应应包含稳定错误码、Retry-After 或可行动提示。对可异步任务,把请求转为排队受理并返回 operation_id;对实时查询,返回缓存、降级字段或明确失败。客户端 SDK 要有随机退避,不能收到 429 后立即无脑重试。

与削峰、熔断的组合

限流在入口控制速率,队列削峰保存可异步任务,熔断阻止已故障依赖继续被调用。三者都需要有界容量:无限队列只是延迟的 OOM,熔断没有恢复探测会永久失败。监控拒绝率、排队时长、令牌耗尽、下游利用率和用户错误率。

常见问题

追问:限流和削峰有什么区别?

限流拒绝或推迟超出容量的请求,削峰常通过队列把瞬时任务摊到更长时间;能异步的任务可排队,强实时且过期无价值的请求应快速拒绝。

追问:为什么固定窗口会有边界突刺?

窗口末尾用满配额,下一窗口开始又立刻用满,短时间内实际请求数可能接近两个窗口总量。滑动窗口或令牌桶可以缓和。

追问:限流阈值应该按 QPS 还是并发数?

短请求常按 QPS,慢 I/O 和连接池保护常按并发数;两者可以同时设置,配合超时和队列上限。

追问:分布式限流中心挂了怎么办?

入口应回退到保守本地限流或快速拒绝,避免无限放行;恢复后逐步同步配额,不能因中心故障让所有实例解除保护。