⚡ 背压 & 流控

当消费者跟不上生产者时怎么办——从管道到 Agent 的流控全解

429
Too Many Requests — 最常见的背压信号
5 种
主流限流算法
~100ms
NGINX 10r/s 限流粒度
90%+
Stripe 请求限流器月拒绝量
📑 目录 什么是背压?为什么你一定会遇到 管道模型:从物理世界理解流控 5 种限流算法详解 交互模拟:令牌桶 vs 漏桶 Stripe 的 4 层限流架构 LLM 推理的背压问题 SaaS 突发流量的保护模式 Redis 限流实操 NGINX 限流配置 Agent 系统的背压设计 算法选择矩阵 上线检查清单

🤔 什么是背压?为什么你一定会遇到

背压(Backpressure)是一个来自流体动力学的概念:当管道下游的处理能力不足以消化上游的输入时,压力会反向传导回上游。

在分布式系统中,这个"压力"表现为:

🔄 生产者-消费者失衡的三种场景
场景生产者消费者后果
LLM API 调用用户请求涌入GPU 推理速度有限429 Too Many Requests
消息队列事件大量入队Worker 处理慢队列堆积 → OOM
数据库写入批量导入任务磁盘 I/O 瓶颈写入超时 → 数据丢失
⚠️ 核心洞察:背压不是"异常",而是分布式系统的常态。任何有容量限制的系统都会遇到背压。问题不是"会不会遇到",而是"遇到时系统会怎样"——优雅降级还是级联崩溃。

三种应对策略:

🛑 Blocking(阻塞)

超过限制直接拒绝。HTTP 429 就是典型。简单粗暴但用户感知差。

最常见

🐌 Throttling(降速)

允许通过但降低质量/速度。如视频降码率。用户体验更平滑。

更友好

⏳ Shaping(整形)

超额请求降低优先级,不影响正常用户。CDN 常用策略。

最精细

🔧 管道模型:从物理世界理解流控

想象一条水管系统——这是理解背压最直观的方式:

生产者 (水龙头) ──→ [缓冲区 (水桶)] ──→ 消费者 (排水口) 情况 1: 水龙头流量 ≤ 排水口流量 → 水桶永远不满 → 无背压 情况 2: 水龙头流量 > 排水口流量 → 水桶逐渐满 → 背压产生 → 策略 A: 溢出丢弃 (Drop/Block) → 策略 B: 关小水龙头 (Throttle) → 策略 C: 加大水桶 (Buffer) → 策略 D: 增加排水口 (Scale Out)

每种策略都有代价:

策略优点缺点适用场景
溢出丢弃实现简单数据丢失可重试的请求(API 调用)
关小水龙头保护下游上游需要配合可减速的场景(流式处理)
加大水桶吸收突发内存/存储成本短暂的流量尖峰
增加排水口根本解决资源成本高持续的高流量
💡 独立开发者的实际选择:对于大多数 SaaS,策略 A(溢出丢弃,即 HTTP 429)+ 适量的 C(缓冲突发)是最务实的选择。D(扩容)需要等到有收入支撑之后再考虑。

📐 5 种限流算法详解

1. 固定窗口计数器(Fixed Window Counter)

最简单的算法。将时间划分为固定窗口(如每分钟),每个窗口内维护一个计数器。

工作原理
# 伪代码
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

致命缺陷——边界突发问题

时间线: |---- 第1分钟 ----|---- 第2分钟 ----| 限制: 每分钟 100 请求 用户在 0:59 发送 100 请求 ✓ 用户在 1:01 发送 100 请求 ✓ → 2秒内 200 请求通过!(2x 超限)

优点:实现简单,内存占用低
缺点:窗口边界处可能 2x 突发
使用者:简单 API 限流、内网服务保护

2. 滑动窗口日志(Sliding Window Log)

记录每个请求的精确时间戳,统计滑动窗口内的请求数。

工作原理
# 伪代码
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 下每用户可能存上千条记录)
使用者:金融交易限流、合规要求精确计数的场景

3. 滑动窗口计数器(Sliding Window Counter)

固定窗口的升级版——用当前窗口和上一个窗口的加权平均来近似滑动窗口。

工作原理
# 伪代码
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 网关的默认选择

4. 令牌桶(Token Bucket)⭐

最流行的限流算法。以固定速率向桶中添加令牌,每个请求消耗一个令牌,桶满时丢弃新令牌,桶空时拒绝请求。

核心参数
# 令牌桶的两个关键参数
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
🔑 关键洞察:令牌桶天然支持突发流量——桶满时可以瞬间消费 BURST 个令牌。这使它成为 API 限流的首选。Stripe 在生产环境中使用令牌桶算法(来源:Stripe 博客 "Scaling your API with rate limiters")。

优点:支持突发、实现简单、内存占用极低(2 个数值)
缺点:需要考虑时钟漂移(分布式场景)
使用者:Stripe、AWS API Gateway、Kong Gateway

5. 漏桶(Leaky Bucket)

请求进入桶中排队,以固定速率从桶底流出(被处理)。桶满则溢出(被拒绝)。

工作原理
# 漏桶参数
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 GatewayNGINX rate limiting
⚠️ NGINX 使用的是漏桶:根据 NGINX 官方博客,limit_req 模块基于漏桶算法。这意味着 NGINX 的限流是"匀速输出"的语义,与令牌桶的"允许突发"不同。理解这个区别对正确配置 burstnodelay 参数至关重要。

🎮 交互模拟:令牌桶 vs 漏桶

参数设置
令牌桶水位
漏桶水位
拒绝请求
通过请求

点击"发送突发请求"模拟一次 10 个请求同时到达,观察两种算法的不同反应。令牌桶允许突发通过,漏桶严格排队。

🏦 Stripe 的 4 层限流架构

Stripe 是限流设计的教科书案例。根据其官方博客 "Scaling your API with rate limiters",Stripe 在生产环境中运行4 种不同的限流器,层层保护:

1️⃣ 请求速率限流器

作用:限制每个用户 N 请求/秒。这是最重要的限流器

算法:令牌桶

触发频率:极高——每月拒绝数百万请求,尤其是测试模式下的失控脚本。

特殊设计:允许短暂突发(burst),以适应闪购等实时事件。

第一道防线

2️⃣ 并发请求限流器

作用:限制同一用户的在途请求数(如最多 20 个同时处理)。

解决问题:某些端点极其耗资源,用户等不及就重试 → 重试加剧负载 → 恶性循环。

触发频率:较低(每月约 12,000 次),但完全解决了 CPU 密集端点的资源争用

第二道防线

3️⃣ 集群用量负载脱落器

作用:为关键请求预留集群容量。

机制:将流量分为"关键"(如创建支付)和"非关键"(如列出交易),始终预留 20% 基础设施给关键请求。非关键请求超配额返回 503。

触发频率:极低,但已多次防止大规模宕机。

第三道防线

4️⃣ Worker 利用率负载脱落器

作用:最终防线——当 Worker 积压时逐步丢弃低优先级请求。

优先级:关键方法 > POST > GET > 测试模式流量。先丢测试流量,再丢 GET,依此类推。

关键设计:丢弃和恢复都必须缓慢,避免"flapping"(丢弃→恢复→再丢弃→再恢复的振荡)。

最终防线
💡 Stripe 的经验教训

🤖 LLM 推理的背压问题

LLM API 调用是当今最典型的背压场景——GPU 推理速度有限,但用户请求可能瞬间涌入。

OpenAI 的限流机制

根据 OpenAI 官方文档(数据采集日期:2026-05-18),OpenAI 使用多维度限流:

OpenAI 限流维度
维度缩写含义
Requests Per MinuteRPM每分钟最大请求数
Requests Per DayRPD每天最大请求数
Tokens Per MinuteTPM每分钟最大 token 数
Tokens Per DayTPD每天最大 token 数
Images Per MinuteIPM每分钟最大图片数(图像模型)

关键:限流在组织级别项目级别生效,不是用户级别。多个模型可能共享同一限流配额。

OpenAI 用量层级(Usage Tiers)
层级资格月用量上限
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 限额重置时间

处理 429 的正确姿势:指数退避 + 抖动

Python 实现指数退避
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
    )
⚠️ 关键陷阱:不成功的请求也会计入每分钟限额。如果你被限流后疯狂重试,只会让情况更糟。必须使用指数退避,而不是固定间隔重试。

LLM 批量处理:Batch API 作为背压释放阀

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

🏗️ SaaS 突发流量的保护模式

对于独立开发者或小团队,不可能像 Stripe 那样建设 4 层防线。以下是务实的最小方案:

🛡️ 第 1 层:CDN/WAF 限流

在哪里:Cloudflare / Vercel Edge

做什么:IP 级别的基础限流,拦截 DDoS 和恶意爬虫

成本:Cloudflare 免费计划即包含基础 DDoS 防护

据 Cloudflare 博客,其 Rate Limiting 服务的真实案例:Buycraft(Minecraft 电商平台)在遭受 DDoS 时,Cloudflare 在边缘节点直接拦截恶意 IP,保护了 50 万+商户的在线商店。

零成本起步

⚡ 第 2 层:API Gateway 限流

在哪里:NGINX / Kong / Vercel Serverless

做什么:用户级别 RPM 限流,每个 API Key 每分钟 N 请求

关键:返回 429 + Retry-After

必须有

🔄 第 3 层:应用层队列

在哪里:Redis + BullMQ / Celery / AWS SQS

做什么:请求排队,异步处理,削峰填谷

适用:AI 生成任务、批量邮件、数据导出

突发场景

📉 第 4 层:降级策略

做什么:高峰期主动降级——关闭非核心功能

实现:Feature flag 控制

案例:LLM 高峰时切换到更小/更便宜的模型

高级策略
降级策略: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 限流实操

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 下每用户可能有数千条记录。

令牌桶(适合 API 限流)

# 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 限流配置

NGINX 使用漏桶算法实现限流。关键指令:limit_req_zonelimit_req

基础配置:每 IP 10 请求/秒
# /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 限流 + 自定义响应
# 按 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}';
    }
}
📖 NGINX burst + nodelay 的含义(这是最常见的混淆点):

🤖 Agent 系统的背压设计

多 Agent 系统是背压问题的重灾区——Agent 之间的调用链可能形成复杂的有向无环图(DAG),一个 Agent 的背压会沿调用链向上传播。

用户请求 │ ▼ [路由 Agent] ─────→ 限流检查 ──→ 429? → 排队/降级 │ ├──→ [搜索 Agent] ──→ 搜索 API 限流 ──→ 降级: 缓存结果 │ ├──→ [代码 Agent] ──→ LLM API 限流 ──→ 降级: 小模型 │ └──→ [数据库 Agent] ──→ DB 连接池满 ──→ 降级: 只读查询 每个 Agent 独立管理自己的背压策略 背压沿调用链向上传播 → 路由 Agent 汇总 → 用户体验降级

Agent 背压的三种模式

模式 1:工具级限流
# 每个 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")
模式 2:Agent 级优先级队列
# 不同优先级的 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}")
模式 3:Circuit Breaker——断路器
# 断路器:当下游持续失败时停止发送请求
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)
断路器状态机: 成功 失败 ≥ N 次 CLOSED ───────────→ CLOSED ────────────────→ OPEN │ 超时恢复 │ ←─────────────────┘ │ ▼ HALF_OPEN │ │ 成功 │ │ 失败 ▼ ▼ CLOSED OPEN

📊 算法选择矩阵

算法 突发支持 内存占用 精确度 实现复杂度 适用场景 典型使用者
固定窗口 ❌ 2x 突发 ⭐ 极低 ⭐⭐ 低 ⭐ 简单 简单 API 限流 内网服务
滑动窗口日志 ✅ 精确 ⭐⭐⭐ 高 ⭐⭐⭐ 最高 ⭐⭐ 中等 金融交易 合规场景
滑动窗口计数 ⚠️ 近似 ⭐⭐ 低 ⭐⭐ 高 ⭐⭐ 中等 API 网关 Stripe
令牌桶 ⭐ ✅ 优秀 ⭐ 极低 ⭐⭐ 高 ⭐⭐ 中等 API 限流 AWS/Stripe/Kong
漏桶 ❌ 匀速 ⭐⭐ 中等 ⭐⭐ 高 ⭐⭐ 中等 流量整形 NGINX
🎯 选择决策树
你需要限流吗?
├── 是 → 需要允许突发吗?
│   ├── 是 → 用令牌桶 ⭐
│   │   └── 例:API 限流、LLM 调用控制
│   └── 否 → 需要严格匀速吗?
│       ├── 是 → 用漏桶
│       │   └── 例:数据库写入限流、消息匀速发送
│       └── 否 → 需要精确计数吗?
│           ├── 是 → 用滑动窗口日志
│           │   └── 例:金融合规、审计
│           └── 否 → 用固定窗口或滑动窗口计数
│               └── 例:简单 API 限流
└── 不确定 → 先用固定窗口,够用就行

独立开发者推荐起步方案:
  NGINX 漏桶(层 1) + Redis 令牌桶(层 2) + 指数退避(客户端)

✅ 上线检查清单

🛡️ 限流上线前必须确认的 12 项
🚨 常见反模式(不要这么做)
反模式为什么不行正确做法
被 429 后固定间隔重试所有客户端同时重试 → 惊群效应指数退避 + 随机抖动
限流器 fail-closeRedis 一挂,全站 429限流器必须 fail-open
只按 IP 限流NAT 后多用户共享 IP按 API Key / User ID 限流
无限大队列内存溢出 → 服务崩溃队列有上限 + 背压传播
忽略 LLM 响应头不知道剩余配额解析 x-ratelimit-remaining 头
测试和生产不同限流脚本上线后遇到意外 429保持一致(Stripe 经验)

🔗 相关阅读

📜 Event Sourcing

请求队列的 Event Sourcing 视角——每个限流决策也是一个事件

🔄 分布式事务

背压传播本质上是分布式事务的 Saga 模式——逐级补偿

👁️ 可观测性

限流器必须有监控——429 率、队列深度、拒绝率是核心指标

🔄 二阶效应

加限流→用户重试→流量加倍→更多 429→级联失败(二阶效应的典型案例)


📅 数据采集日期:2026-05-18 | 来源:Stripe Blog、OpenAI Docs、Cloudflare Blog、NGINX Blog、ByteByteGo、Redis.io | ← 返回分布式系统