当消费者跟不上生产者时怎么办——从管道到 Agent 的流控全解
背压(Backpressure)是一个来自流体动力学的概念:当管道下游的处理能力不足以消化上游的输入时,压力会反向传导回上游。
在分布式系统中,这个"压力"表现为:
| 场景 | 生产者 | 消费者 | 后果 |
|---|---|---|---|
| LLM API 调用 | 用户请求涌入 | GPU 推理速度有限 | 429 Too Many Requests |
| 消息队列 | 事件大量入队 | Worker 处理慢 | 队列堆积 → OOM |
| 数据库写入 | 批量导入任务 | 磁盘 I/O 瓶颈 | 写入超时 → 数据丢失 |
三种应对策略:
超过限制直接拒绝。HTTP 429 就是典型。简单粗暴但用户感知差。
最常见允许通过但降低质量/速度。如视频降码率。用户体验更平滑。
更友好超额请求降低优先级,不影响正常用户。CDN 常用策略。
最精细想象一条水管系统——这是理解背压最直观的方式:
每种策略都有代价:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 溢出丢弃 | 实现简单 | 数据丢失 | 可重试的请求(API 调用) |
| 关小水龙头 | 保护下游 | 上游需要配合 | 可减速的场景(流式处理) |
| 加大水桶 | 吸收突发 | 内存/存储成本 | 短暂的流量尖峰 |
| 增加排水口 | 根本解决 | 资源成本高 | 持续的高流量 |
最简单的算法。将时间划分为固定窗口(如每分钟),每个窗口内维护一个计数器。
# 伪代码
counter = 0
window_start = now()
def allow_request():
if now() - window_start >= WINDOW_SIZE:
counter = 0
window_start = now()
if counter < LIMIT:
counter += 1
return True
return False
致命缺陷——边界突发问题:
优点:实现简单,内存占用低
缺点:窗口边界处可能 2x 突发
使用者:简单 API 限流、内网服务保护
记录每个请求的精确时间戳,统计滑动窗口内的请求数。
# 伪代码
log = [] # 请求时间戳列表
def allow_request():
now = current_time()
# 移除窗口外的记录
log = [t for t in log if now - t < WINDOW_SIZE]
if len(log) < LIMIT:
log.append(now)
return True
return False
优点:最精确,无边界问题
缺点:内存消耗与请求数成正比(高 QPS 下每用户可能存上千条记录)
使用者:金融交易限流、合规要求精确计数的场景
固定窗口的升级版——用当前窗口和上一个窗口的加权平均来近似滑动窗口。
# 伪代码
def allow_request():
now = current_time()
current_window = now // WINDOW_SIZE
elapsed_ratio = (now % WINDOW_SIZE) / WINDOW_SIZE
current_count = get_count(current_window)
previous_count = get_count(current_window - 1)
# 加权估计
estimated = previous_count * (1 - elapsed_ratio) + current_count
if estimated < LIMIT:
increment_count(current_window)
return True
return False
优点:内存占用低(只需 2 个计数器),比固定窗口精确得多
缺点:仍是估计值,非完全精确
使用者:Stripe 使用此算法(据其博客描述),大多数 API 网关的默认选择
最流行的限流算法。以固定速率向桶中添加令牌,每个请求消耗一个令牌,桶满时丢弃新令牌,桶空时拒绝请求。
# 令牌桶的两个关键参数
RATE = 10 # 令牌填充速率 (tokens/second)
BURST = 20 # 桶容量 (最大突发量)
# 状态
tokens = BURST # 初始满桶
last_refill = now()
def allow_request():
# 计算自上次填充以来的新令牌
now = current_time()
elapsed = now - last_refill
tokens = min(BURST, tokens + elapsed * RATE)
last_refill = now
if tokens >= 1:
tokens -= 1
return True
return False
优点:支持突发、实现简单、内存占用极低(2 个数值)
缺点:需要考虑时钟漂移(分布式场景)
使用者:Stripe、AWS API Gateway、Kong Gateway
请求进入桶中排队,以固定速率从桶底流出(被处理)。桶满则溢出(被拒绝)。
# 漏桶参数
RATE = 10 # 固定流出速率 (requests/second)
CAPACITY = 20 # 桶容量 (队列长度)
queue = []
last_leak = now()
def allow_request():
# 先漏水
now = current_time()
elapsed = now - last_leak
leak_count = int(elapsed * RATE)
for _ in range(min(leak_count, len(queue))):
queue.pop(0) # 处理并移除
last_leak = now
if len(queue) < CAPACITY:
queue.append(request)
return True
return False # 桶满,拒绝
令牌桶 vs 漏桶——这是最常见的混淆点:
| 特性 | 令牌桶 | 漏桶 |
|---|---|---|
| 允许突发 | ✅ 是(桶满时瞬间放行 BURST 请求) | ❌ 否(严格固定速率输出) |
| 输出速率 | 可变(<= RATE + BURST) | 恒定(= RATE) |
| 语义 | "平均速率不超过 RATE,允许短时突发" | "严格匀速,削峰填谷" |
| 适用场景 | API 限流(用户不会均匀发请求) | 流量整形(匀速写入数据库) |
| 典型使用者 | Stripe、AWS API Gateway | NGINX rate limiting |
limit_req 模块基于漏桶算法。这意味着 NGINX 的限流是"匀速输出"的语义,与令牌桶的"允许突发"不同。理解这个区别对正确配置 burst 和 nodelay 参数至关重要。
点击"发送突发请求"模拟一次 10 个请求同时到达,观察两种算法的不同反应。令牌桶允许突发通过,漏桶严格排队。
Stripe 是限流设计的教科书案例。根据其官方博客 "Scaling your API with rate limiters",Stripe 在生产环境中运行4 种不同的限流器,层层保护:
作用:限制每个用户 N 请求/秒。这是最重要的限流器。
算法:令牌桶
触发频率:极高——每月拒绝数百万请求,尤其是测试模式下的失控脚本。
特殊设计:允许短暂突发(burst),以适应闪购等实时事件。
第一道防线作用:限制同一用户的在途请求数(如最多 20 个同时处理)。
解决问题:某些端点极其耗资源,用户等不及就重试 → 重试加剧负载 → 恶性循环。
触发频率:较低(每月约 12,000 次),但完全解决了 CPU 密集端点的资源争用。
第二道防线作用:为关键请求预留集群容量。
机制:将流量分为"关键"(如创建支付)和"非关键"(如列出交易),始终预留 20% 基础设施给关键请求。非关键请求超配额返回 503。
触发频率:极低,但已多次防止大规模宕机。
第三道防线作用:最终防线——当 Worker 积压时逐步丢弃低优先级请求。
优先级:关键方法 > POST > GET > 测试模式流量。先丢测试流量,再丢 GET,依此类推。
关键设计:丢弃和恢复都必须缓慢,避免"flapping"(丢弃→恢复→再丢弃→再恢复的振荡)。
最终防线LLM API 调用是当今最典型的背压场景——GPU 推理速度有限,但用户请求可能瞬间涌入。
根据 OpenAI 官方文档(数据采集日期:2026-05-18),OpenAI 使用多维度限流:
| 维度 | 缩写 | 含义 |
|---|---|---|
| Requests Per Minute | RPM | 每分钟最大请求数 |
| Requests Per Day | RPD | 每天最大请求数 |
| Tokens Per Minute | TPM | 每分钟最大 token 数 |
| Tokens Per Day | TPD | 每天最大 token 数 |
| Images Per Minute | IPM | 每分钟最大图片数(图像模型) |
关键:限流在组织级别和项目级别生效,不是用户级别。多个模型可能共享同一限流配额。
| 层级 | 资格 | 月用量上限 |
|---|---|---|
| Free | 允许地区 | $100/月 |
| Tier 1 | 已付 $5 | $100/月 |
| Tier 2 | 已付 $50 | $500/月 |
| Tier 3 | 已付 $100 | $1,000/月 |
| Tier 4 | 已付 $250 | $5,000/月 |
| Tier 5 | 已付 $1,000 | $200,000/月 |
随着消费增加,自动升级到更高层级,RPM/TPM 限制也会相应提高。
OpenAI 的响应头提供了实时限流状态:
# 响应头示例
x-ratelimit-limit-requests: 60 # 最大请求数
x-ratelimit-limit-tokens: 150000 # 最大 token 数
x-ratelimit-remaining-requests: 59 # 剩余请求数
x-ratelimit-remaining-tokens: 149984 # 剩余 token 数
x-ratelimit-reset-requests: 1s # 请求限额重置时间
x-ratelimit-reset-tokens: 6m0s # Token 限额重置时间
import time
import random
from openai import OpenAI
client = OpenAI()
def call_llm_with_retry(messages, max_retries=5):
"""带指数退避和抖动的 LLM 调用"""
for attempt in range(max_retries):
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
max_tokens=1000
)
return response
except Exception as e:
if hasattr(e, 'status_code') and e.status_code == 429:
# 指数退避 + 随机抖动
base_delay = min(2 ** attempt, 60) # 最大 60 秒
jitter = random.uniform(0, base_delay * 0.5)
delay = base_delay + jitter
print(f"Rate limited, retrying in {delay:.1f}s (attempt {attempt+1})")
time.sleep(delay)
else:
raise
raise Exception(f"Max retries ({max_retries}) exceeded")
# 使用 tenacity 库更优雅
from tenacity import retry, wait_exponential_jitter, stop_after_attempt
@retry(
wait=wait_exponential_jitter(max=60),
stop=stop_after_attempt(5)
)
def call_llm(messages):
return client.chat.completions.create(
model="gpt-4o",
messages=messages
)
OpenAI 的 Batch API 允许提交大量请求,在 24 小时内异步处理,不影响同步请求的限流配额,且价格减半。这是处理突发背压的理想方式——不需要实时响应的任务全部走 Batch。
# Batch API 使用示例
import json
from openai import OpenAI
client = OpenAI()
# 1. 准备批量请求文件
requests = []
for i, prompt in enumerate(prompts):
requests.append({
"custom_id": f"request-{i}",
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": "gpt-4o",
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 500
}
})
# 2. 上传文件
file = client.files.create(
file=open("batch_requests.jsonl", "rb"),
purpose="batch"
)
# 3. 创建批量任务(不影响同步限额!)
batch = client.batches.create(
input_file_id=file.id,
endpoint="/v1/chat/completions",
completion_window="24h"
)
print(f"Batch {batch.id} submitted, status: {batch.status}")
# 输出: Batch batch_xxx submitted, status: validating
对于独立开发者或小团队,不可能像 Stripe 那样建设 4 层防线。以下是务实的最小方案:
在哪里:Cloudflare / Vercel Edge
做什么:IP 级别的基础限流,拦截 DDoS 和恶意爬虫
成本:Cloudflare 免费计划即包含基础 DDoS 防护
据 Cloudflare 博客,其 Rate Limiting 服务的真实案例:Buycraft(Minecraft 电商平台)在遭受 DDoS 时,Cloudflare 在边缘节点直接拦截恶意 IP,保护了 50 万+商户的在线商店。
零成本起步在哪里:NGINX / Kong / Vercel Serverless
做什么:用户级别 RPM 限流,每个 API Key 每分钟 N 请求
关键:返回 429 + Retry-After 头
在哪里:Redis + BullMQ / Celery / AWS SQS
做什么:请求排队,异步处理,削峰填谷
适用:AI 生成任务、批量邮件、数据导出
突发场景做什么:高峰期主动降级——关闭非核心功能
实现:Feature flag 控制
案例:LLM 高峰时切换到更小/更便宜的模型
高级策略# 智能模型路由——高峰期自动降级
import time
from collections import defaultdict
class ModelRouter:
def __init__(self):
self.request_counts = defaultdict(int)
self.window_start = time.time()
def get_model(self, complexity: str = "normal"):
"""根据系统负载选择模型"""
now = time.time()
if now - self.window_start > 60:
self.request_counts.clear()
self.window_start = now
rpm = sum(self.request_counts.values())
load_factor = rpm / 1000 # 假设 1000 RPM 是警戒线
if load_factor > 0.8:
# 高负载:全部使用小模型
return "gpt-4o-mini"
elif load_factor > 0.5:
# 中等负载:复杂任务用大模型,简单任务用小模型
return "gpt-4o" if complexity == "complex" else "gpt-4o-mini"
else:
# 低负载:按需分配
return "gpt-4o" if complexity != "simple" else "gpt-4o-mini"
router = ModelRouter()
model = router.get_model(complexity="complex")
Redis 是实现限流的事实标准——INCR + EXPIRE 两个命令就够了。
# Redis 固定窗口限流——每用户每分钟 60 请求
# Key 格式: rate_limit:{user_id}:{minute_timestamp}
import redis
import time
r = redis.Redis()
def is_rate_limited(user_id: str, limit: int = 60, window: int = 60) -> bool:
"""返回 True 表示被限流"""
now = int(time.time())
window_key = now // window # 当前窗口编号
key = f"rate_limit:{user_id}:{window_key}"
# 原子操作:INCR + EXPIRE
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
count, _ = pipe.execute()
return count > limit
# 使用
if is_rate_limited("user_123"):
return {"error": "Rate limit exceeded"}, 429
优点:2 个 Redis 命令,极低延迟。每个窗口仅 1 个 key,自动过期清理。
缺点:窗口边界 2x 突发问题。
# Redis 滑动窗口限流——Lua 脚本保证原子性
# 使用 Sorted Set + 时间戳作为 score
import redis
import time
import uuid
r = redis.Redis()
SLIDING_WINDOW_SCRIPT = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local id = ARGV[4]
-- 移除窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 添加当前请求
redis.call('ZADD', key, now, id)
redis.call('EXPIRE', key, window / 1000)
return 1 -- 允许
else
return 0 -- 拒绝
end
"""
def is_rate_limited(user_id: str, limit: int = 60, window_ms: int = 60000) -> bool:
"""滑动窗口限流,返回 True 表示被限流"""
key = f"rate_limit:{user_id}"
now = int(time.time() * 1000)
request_id = f"{now}:{uuid.uuid4()}"
allowed = r.eval(
SLIDING_WINDOW_SCRIPT, 1,
key, limit, window_ms, now, request_id
)
return allowed == 0
优点:精确无边界问题,Lua 脚本保证原子性。
缺点:Sorted Set 内存开销比简单计数器大。高 QPS 下每用户可能有数千条记录。
# Redis 令牌桶——Lua 脚本实现
# 支持突发流量,Stripe 风格
TOKEN_BUCKET_SCRIPT = """
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 令牌填充速率 (tokens/sec)
local burst = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间 (毫秒)
local requested = tonumber(ARGV[4]) -- 请求消耗的令牌数
-- 获取当前桶状态
local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1])
local last_refill = tonumber(bucket[2])
-- 初始化
if tokens == nil then
tokens = burst
last_refill = now
end
-- 计算新令牌
local elapsed = (now - last_refill) / 1000
tokens = math.min(burst, tokens + elapsed * rate)
last_refill = now
-- 检查并消耗
if tokens >= requested then
tokens = tokens - requested
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
redis.call('EXPIRE', key, burst / rate + 10) -- 自动过期
return {1, math.floor(tokens)} -- 允许, 剩余令牌
else
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
redis.call('EXPIRE', key, burst / rate + 10)
return {0, math.floor(tokens)} -- 拒绝, 剩余令牌
end
"""
import redis, time
r = redis.Redis()
def check_rate_limit(user_id: str, rate: float = 10, burst: int = 20,
tokens_needed: int = 1) -> dict:
"""令牌桶限流检查"""
key = f"token_bucket:{user_id}"
now = int(time.time() * 1000)
allowed, remaining = r.eval(
TOKEN_BUCKET_SCRIPT, 1,
key, rate, burst, now, tokens_needed
)
return {
"allowed": bool(allowed),
"remaining": remaining,
"limit": burst,
"reset_ms": int((burst - remaining) / rate * 1000) if remaining < burst else 0
}
# 使用
result = check_rate_limit("user_123", rate=10, burst=20)
if not result["allowed"]:
return {"error": "Rate limit exceeded", "retry_after": result["reset_ms"]}, 429
优点:支持突发、精确控制、返回剩余令牌数(可用于 X-RateLimit-Remaining 头)。
这是生产 API 限流的推荐方案。
NGINX 使用漏桶算法实现限流。关键指令:limit_req_zone 和 limit_req。
# /etc/nginx/conf.d/rate-limit.conf
# 定义限流区域
# $binary_remote_addr — 以 IP 为 key(比 $remote_addr 省内存)
# zone=mylimit:10m — 共享内存区域名和大小(10MB ≈ 16万 IP)
# rate=10r/s — 10 请求/秒 = 每 100ms 允许 1 请求
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
server {
# 登录接口——严格限流
location /login/ {
# burst=20: 允许 20 个请求排队
# nodelay: 排队的请求立即处理(而非匀速释放)
limit_req zone=mylimit burst=20 nodelay;
limit_req_status 429; # 默认返回 503,改为 429 更语义化
proxy_pass http://backend;
}
# API 接口——两阶段限流(NGINX 1.15.7+)
location /api/ {
# delay=8: 前 8 个突发请求立即处理,后续排队等待
limit_req zone=mylimit burst=12 delay=8;
proxy_pass http://backend;
}
}
# 按 API Key 限流(而非 IP)
# 从查询参数或 Header 提取 key
limit_req_zone $api_key zone=api_limit:10m rate=60r/m;
map $http_authorization $api_key {
~^Bearer\s+(\S+)$ $1; # 从 Authorization header 提取
default $binary_remote_addr; # fallback 到 IP
}
server {
location /api/ {
limit_req zone=api_limit burst=10 nodelay;
limit_req_status 429;
# 自定义 429 响应
error_page 429 /rate_limited.html;
# 添加限流相关 Header
add_header X-RateLimit-Limit 60 always;
add_header X-RateLimit-Remaining $limit_remaining always;
proxy_pass http://backend;
}
location = /rate_limited.html {
internal;
default_type application/json;
return 429 '{"error":"rate_limit_exceeded","message":"Too many requests. Please retry later.","retry_after":60}';
}
}
burst=20:允许 20 个超额请求排队等待nodelay:排队的请求立即被处理(而不是按 100ms 间隔匀速释放),但"占位"仍按 100ms 释放burst=20 nodelay,既允许突发又不让用户感觉延迟delay=8:前 8 个立即处理,后续排队——更精细的控制多 Agent 系统是背压问题的重灾区——Agent 之间的调用链可能形成复杂的有向无环图(DAG),一个 Agent 的背压会沿调用链向上传播。
# 每个 MCP 工具独立限流
from dataclasses import dataclass
from typing import Optional
import asyncio
@dataclass
class ToolRateLimit:
"""工具级别的限流配置"""
tool_name: str
rpm: int # 每分钟最大调用次数
concurrent: int = 5 # 最大并发调用数
timeout: float = 30.0 # 单次调用超时
fallback: Optional[str] = None # 限流时的替代工具
# 工具限流注册表
TOOL_LIMITS = {
"web_search": ToolRateLimit("web_search", rpm=30, concurrent=3),
"code_execute": ToolRateLimit("code_execute", rpm=60, concurrent=5),
"llm_generate": ToolRateLimit("llm_generate", rpm=100, concurrent=10,
fallback="llm_generate_fast"),
"llm_generate_fast": ToolRateLimit("llm_generate_fast", rpm=200, concurrent=20),
"database_query": ToolRateLimit("database_query", rpm=120, concurrent=8),
}
class ToolRateLimiter:
"""带限流的工具调用器"""
def __init__(self):
self.semaphores = {
name: asyncio.Semaphore(limit.concurrent)
for name, limit in TOOL_LIMITS.items()
}
self.counters = defaultdict(int) # RPM 计数
self.window_start = time.time()
async def call(self, tool_name: str, **kwargs):
limit = TOOL_LIMITS.get(tool_name)
if not limit:
return await self._execute(tool_name, **kwargs)
# 检查 RPM
if self.counters[tool_name] >= limit.rpm:
if limit.fallback:
return await self.call(limit.fallback, **kwargs)
raise RateLimitError(f"Tool {tool_name} rate limited")
# 并发控制
async with self.semaphores[tool_name]:
self.counters[tool_name] += 1
try:
return await asyncio.wait_for(
self._execute(tool_name, **kwargs),
timeout=limit.timeout
)
except asyncio.TimeoutError:
raise ToolTimeoutError(f"Tool {tool_name} timed out")
# 不同优先级的 Agent 使用不同的队列
import asyncio
from enum import IntEnum
class Priority(IntEnum):
CRITICAL = 0 # 支付、安全检查
HIGH = 1 # 用户直接请求
NORMAL = 2 # 后台分析
LOW = 3 # 批量任务
class PriorityExecutor:
"""优先级执行器——高负载时丢弃低优先级任务"""
def __init__(self, max_workers: int = 10):
self.queues = {p: asyncio.Queue() for p in Priority}
self.max_workers = max_workers
self.active_workers = 0
self.running = True
async def submit(self, priority: Priority, coro):
"""提交任务到优先级队列"""
await self.queues[priority].put(coro)
self._schedule()
def _schedule(self):
"""调度:优先处理高优先级"""
if self.active_workers >= self.max_workers:
return
# 从高到低检查队列
for priority in sorted(Priority):
if not self.queues[priority].empty():
task = asyncio.create_task(self._worker(priority))
self.active_workers += 1
break
async def _worker(self, priority: Priority):
"""工作线程"""
try:
while self.running:
try:
coro = self.queues[priority].get_nowait()
await coro
except asyncio.QueueEmpty:
break
finally:
self.active_workers -= 1
self._schedule()
def shed_load(self, keep_up_to: Priority = Priority.HIGH):
"""负载脱落:丢弃低优先级任务"""
for priority in Priority:
if priority > keep_up_to:
q = self.queues[priority]
dropped = 0
while not q.empty():
try:
q.get_nowait()
dropped += 1
except asyncio.QueueEmpty:
break
if dropped:
print(f"Shed {dropped} tasks from priority {priority.name}")
# 断路器:当下游持续失败时停止发送请求
from enum import Enum
import time
class CircuitState(Enum):
CLOSED = "closed" # 正常:允许请求
OPEN = "open" # 熔断:拒绝请求
HALF_OPEN = "half_open" # 探测:允许少量请求测试
class CircuitBreaker:
"""断路器——保护下游服务"""
def __init__(self, failure_threshold: int = 5,
recovery_timeout: float = 30.0,
half_open_max: int = 3):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.half_open_max = half_open_max
self.state = CircuitState.CLOSED
self.failure_count = 0
self.last_failure_time = 0
self.half_open_count = 0
async def call(self, func, *args, **kwargs):
"""通过断路器调用函数"""
if self.state == CircuitState.OPEN:
# 检查是否到恢复时间
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = CircuitState.HALF_OPEN
self.half_open_count = 0
else:
raise CircuitOpenError("Circuit breaker is OPEN")
if self.state == CircuitState.HALF_OPEN:
if self.half_open_count >= self.half_open_max:
raise CircuitOpenError("Circuit breaker HALF_OPEN limit reached")
self.half_open_count += 1
try:
result = await func(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise
def _on_success(self):
"""成功回调"""
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.CLOSED # 恢复正常
self.failure_count = 0
def _on_failure(self):
"""失败回调"""
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN # 熔断
print(f"⚠️ Circuit OPEN after {self.failure_count} failures")
# 使用:保护 LLM API 调用
llm_breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=30)
async def safe_llm_call(prompt):
try:
return await llm_breaker.call(call_openai, prompt)
except CircuitOpenError:
# 断路器打开时使用缓存或降级
return get_cached_response(prompt)
| 算法 | 突发支持 | 内存占用 | 精确度 | 实现复杂度 | 适用场景 | 典型使用者 |
|---|---|---|---|---|---|---|
| 固定窗口 | ❌ 2x 突发 | ⭐ 极低 | ⭐⭐ 低 | ⭐ 简单 | 简单 API 限流 | 内网服务 |
| 滑动窗口日志 | ✅ 精确 | ⭐⭐⭐ 高 | ⭐⭐⭐ 最高 | ⭐⭐ 中等 | 金融交易 | 合规场景 |
| 滑动窗口计数 | ⚠️ 近似 | ⭐⭐ 低 | ⭐⭐ 高 | ⭐⭐ 中等 | API 网关 | Stripe |
| 令牌桶 ⭐ | ✅ 优秀 | ⭐ 极低 | ⭐⭐ 高 | ⭐⭐ 中等 | API 限流 | AWS/Stripe/Kong |
| 漏桶 | ❌ 匀速 | ⭐⭐ 中等 | ⭐⭐ 高 | ⭐⭐ 中等 | 流量整形 | NGINX |
你需要限流吗?
├── 是 → 需要允许突发吗?
│ ├── 是 → 用令牌桶 ⭐
│ │ └── 例:API 限流、LLM 调用控制
│ └── 否 → 需要严格匀速吗?
│ ├── 是 → 用漏桶
│ │ └── 例:数据库写入限流、消息匀速发送
│ └── 否 → 需要精确计数吗?
│ ├── 是 → 用滑动窗口日志
│ │ └── 例:金融合规、审计
│ └── 否 → 用固定窗口或滑动窗口计数
│ └── 例:简单 API 限流
└── 不确定 → 先用固定窗口,够用就行
独立开发者推荐起步方案:
NGINX 漏桶(层 1) + Redis 令牌桶(层 2) + 指数退避(客户端)
| 反模式 | 为什么不行 | 正确做法 |
|---|---|---|
| 被 429 后固定间隔重试 | 所有客户端同时重试 → 惊群效应 | 指数退避 + 随机抖动 |
| 限流器 fail-close | Redis 一挂,全站 429 | 限流器必须 fail-open |
| 只按 IP 限流 | NAT 后多用户共享 IP | 按 API Key / User ID 限流 |
| 无限大队列 | 内存溢出 → 服务崩溃 | 队列有上限 + 背压传播 |
| 忽略 LLM 响应头 | 不知道剩余配额 | 解析 x-ratelimit-remaining 头 |
| 测试和生产不同限流 | 脚本上线后遇到意外 429 | 保持一致(Stripe 经验) |
请求队列的 Event Sourcing 视角——每个限流决策也是一个事件
背压传播本质上是分布式事务的 Saga 模式——逐级补偿
限流器必须有监控——429 率、队列深度、拒绝率是核心指标
加限流→用户重试→流量加倍→更多 429→级联失败(二阶效应的典型案例)
📅 数据采集日期:2026-05-18 | 来源:Stripe Blog、OpenAI Docs、Cloudflare Blog、NGINX Blog、ByteByteGo、Redis.io | ← 返回分布式系统