防线:三系统,而非单一

发布: (2026年2月28日 GMT+8 12:50)
12 分钟阅读
原文: Dev.to

I’m happy to translate the article for you, but I don’t have the ability to retrieve the full text from external links. Could you please paste the content you’d like translated (excluding any code blocks or URLs you want to keep unchanged)? Once I have the text, I’ll provide a Simplified‑Chinese translation while preserving the original formatting and markdown.

三种机制

“速率限制”常被用作统称,指任何拒绝或减慢请求的手段。实际上有三种不同的机制,每种针对不同的故障模式提供保护,并提出不同的问题。

机制提出的问题保护对象
负载抛弃“此服务器是否足够健康以处理任何请求?”服务器保护自身
速率限制此调用方是否发送了过多请求?”系统保护滥用的调用方
自适应限流下游现在是否在挣扎?”下游服务受到此服务器的保护

当服务器因内存耗尽(OOM)而崩溃时,速率限制器帮不了你——每个用户都在配额范围内,但服务器已经挂了。
负载抛弃无法阻止某个客户消耗你 80 % 的容量——总并发量没问题,只是分配不公平。
两者都无法防止你对已经在挣扎的下游服务进行猛烈冲击。

第1层 – 负载削减

保护 此服务器 不受自身影响。

  • 内存压力是否过高?
  • 并发请求是否过多?
  • 下游是否刚刚返回 RESOURCE_EXHAUSTED

如果上述任意情况成立,立即拒绝——不论用户是谁、请求是什么。大楼已满负荷。

第2层 – 限流

保护 系统 免受滥用用户的影响。

  • 这个特定用户、API 密钥或 IP 地址是否发送的请求超过了其允许的配额?

这就是经典的限流器——按用户计数器、滑动窗口、令牌桶。

第 3 层 – 自适应限流

保护 下游服务 不受此服务器影响。

  • 服务器在调用每个下游时会跟踪其成功率。
  • 如果对支付服务的调用失败率达到 20 %,服务器会以概率方式丢弃 20 %的外发调用——为支付服务提供恢复的空间。

为什么顺序很重要

  1. 负载抛弃(Load shedding)以最高优先级运行——在身份验证、请求解析以及其他任何操作之前。
  2. 如果 速率限制(Layer 2) 先运行,服务器会花 CPU 检查 Redis 计数器、计算滑动窗口数学以及进行每用户的查找。随后才到达 Layer 1,后者会说 “实际上服务器快挂了,全部拒绝”。所有前面的工作都白费了。
  3. 负载抛弃成本低——一次原子计数器检查或读取 GC 标志,耗时微秒。速率限制可能需要一次 Redis 往返。先做廉价检查。
  4. 类比 – 想象一家夜店:门口的消防官员(负载抛弃)不检查身份证。“建筑已满员,没人能进”。只有在建筑未满时,保镖(速率限制器)才会检查你的客人名单。

示例场景

场景Layer 1 的作用Layer 2 的作用Layer 3 的作用
部署错误 – 新的机器学习模型消耗 3 倍内存检测到 GC 压力激增,开始抛弃盲目 – 每个用户都在限制范围内盲目 – 下游正常
单个客户激增 10 倍 – 迁移脚本 bug若整体并发超过限制,可能最终捕获立即捕获 – 每用户计数器超过阈值盲目 – 下游正常
下游支付服务降级 – 40 % 返回 RESOURCE_EXHAUSTED对这些响应进行响应式回退盲目 – 用户仍在限制范围内概率性丢弃出站调用,为服务腾出空间
DDoS – 成千上万 IP,每个流量适中捕获总体并发激增捕获每 IP 限制(若已设置)盲目 – 只处理入站问题
依赖变慢 – DB 查询从 5 ms 变为 2 s看到并发请求计数向限制值飙升盲目 – 用户仍在限制范围内可能看不到错误(慢响应不算错误)
Layer 1 与 Layer 2 同时失效仍能防止向下游服务的级联影响

要点: 没有单一层能够处理所有情况。它们是互补的,而非冗余的。如果某一层失效,其他层仍能提供保护。

限流不是单一工具——而是两种

方法作用调用者体验
拒绝返回 429 Too Many Requests。请求超过限制,被拒绝调用者必须处理该错误。
延迟(排队)将请求放入队列中,待速率允许时再释放。请求被延迟,而非被拒绝。调用者会看到响应变慢,但没有错误。

两者都实现了相同的目标——强制速率限制——但提供了完全不同的体验。

关键问题: 何时拒绝,何时延迟?

  • 拒绝 当外部连接被保持打开时(例如用户的 HTTP 连接)。
  • 延迟 当你可以安全地缓冲请求并在稍后释放,而不会破坏客户端的预期时。

TL;DR

  • 负载削减 → 保护服务器本身。
  • 限流 → 保护系统免受滥用调用者的影响。
  • 自适应限流 → 保护下游服务。

顺序运行它们(负载削减 → 限流 → 自适应限流),以最大化效率和弹性。

Rate‑Limiting: Reject or Delay?

规则很简单:

Reject 当调用者仅仅在等待连接时。
Delay 当你可以负担等待时。

下面列出常见情形,说明为何选择很重要。

1. Connection‑Pool Exhaustion

“你正在占用那个连接——这意味着一个线程、一个套接字、内存。
延迟 500 个用户后,你的连接池就耗尽了。此时即使是符合配额的合法用户也拿不到连接。
你的限流器因为对坏用户太宽容而导致好用户宕机。
快速 Reject。 释放连接。让客户端的重试逻辑去处理。”

2. External API Rate Limits (e.g., Stripe)

“当你的系统需要请求成功时才延迟。
你正在调用 Stripe 的支付 API。你知道它的限制是:100 req/s
第 101 个请求不需要直接失败——只需要等 10 ms,等到下一秒的配额。
如果你改为 Reject,就必须编写重试逻辑、退避计时器、死信队列以及监控重试——这是一整套基础设施来处理一个**‘等一下’**就能解决的问题。”

3. Public API Burst Traffic

“你的公共 API 收到来自某客户的突发流量。Reject。 立即返回 429
客户的 SDK 已内置指数退避重试。你的服务器在微秒级处理完拒绝并继续工作。
如果改为 Delay,500 条连接会保持打开,连接池被耗尽,所有人都会遭遇宕机。”

4. Bulk Email Sends (SendGrid)

“你正在通过 SendGrid 发送 50 000 封营销邮件。
Delay。SendGrid 允许 500 req/s。把 50 000 条请求排队,以 500 /s 的速率滴出 → 需要 100 s,每封邮件都能送达。
若改为 Reject,49 500 封邮件会在第一秒被弹回。随后你需要死信队列和重试调度来处理一个**‘轮到你了’**就能彻底解决的问题。”

5. gRPC Internal Traffic

“你的 gRPC 服务器接收来自上游服务的内部流量。Reject。 返回 RESOURCE_EXHAUSTED
上游的自适应节流器(它们的第 3 层)看到错误后会自动退回。系统自行恢复。
若改为 Delay,上游的 gRPC deadline 会在请求排队期间到期。超时错误比干净的拒绝更糟——上游无法分辨是*‘服务器慢’还是‘我被限流了’*。”

6. Batch Job Scraping a Partner API

“一个批处理作业每晚从合作伙伴 API 抓取 10 000 条记录。
Delay。合作伙伴允许 50 req/s。完美地把速率控制在该值 → 3.3 分钟,所有请求成功,合作伙伴永远不会看到流量峰值。
若改为 Reject,9 950 条请求会立即失败,触发重试逻辑,结果你会在 20 分钟 内对合作伙伴造成冲击,而不是一次干净的 3 分钟 抓取。”

7. User‑Facing Payment Endpoint

“用户在结账时调用你的支付接口。Reject。
用户看到一个写着 ‘立即支付’ 的按钮。一次 200 ms 的拒绝并提示 ‘请重试’,远比 5 秒 的延迟要好——后者会让用户以为页面卡死,刷新页面,导致重复支付。”

TL;DR

情况操作原因
调用者只需要一个免费连接拒绝(例如 429,RESOURCE_EXHAUSTED)立即释放资源;客户端可以进行退避重试
您可以等待配额或节流延迟(队列、睡眠、令牌桶)确保成功处理,无需额外的重试基础设施
外部服务有已知的速率限制延迟直至配额可用避免不必要的失败和下游重试风暴
用户体验对延迟敏感快速拒绝并提供明确的消息防止 UI 卡顿和重复操作
0 浏览
Back to Blog

相关文章

阅读更多 »

2026年企业 Web 开发终极指南

企业网页开发在过去十年中经历了巨大的演变。随着企业对数字平台的依赖日益增加,创建可扩展的、安全的、以及 h...

设计 URL Shortener

设计一个 URL 短链接服务是最受欢迎的系统设计面试题之一。它看起来很简单,但它考察你对可扩展性、数据库……的理解。

国家疫苗预约与接种系统

🌱 它是如何开始的 几年前,我参加了一场系统设计面试。面试官给了我这样一个情景:> 设计一个全国疫苗预约系统…