先说结论
限流用于把进入系统的工作量控制在可承受范围。令牌桶允许按速率补充并容纳一定突发,漏桶把输出整形成稳定速率,滑动窗口比固定窗口更平滑。算法只是基础,关键是限流维度、阈值来源、分布式一致性和拒绝后的响应策略。
阈值应来自压测容量、依赖瓶颈和 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 和连接池保护常按并发数;两者可以同时设置,配合超时和队列上限。
追问:分布式限流中心挂了怎么办?
入口应回退到保守本地限流或快速拒绝,避免无限放行;恢复后逐步同步配额,不能因中心故障让所有实例解除保护。