AI 流量时代的 API 限流与计费系统设计
核心是三层限流加三阶段计费:边缘层用 Nginx 挡量,应用层用 Redis 令牌桶原子扣减并按接口成本差异化消耗,租户层管住日/月配额与并发;计费走预扣、结算、对账,靠唯一请求 ID 保证幂等不重不漏。
- 限流分三层:Nginx 边缘挡量、Redis 令牌桶算账、租户配额控总量
- 必须带 Retry-After 与剩余额度,否则 AI 智能体会把限流变放大器
- 计费拆成预扣、结算、对账三阶段,唯一请求 ID 保证幂等不重不漏
- AI 客户端带来重试风暴、并发占用与 Key 克隆三类新问题
- 对账差额超千分之一就告警并冻结账户,不要自动修正
引言:调用方变了,限流与计费也必须跟着变#
2026 年的公共 API 平台上,请求的来源正在悄悄换人。过去是一个开发者写一段脚本,按需调用几次接口;现在是 IDE 里的 AI 编码助手、自动化智能体、批量任务框架在替你调用——它们会在失败后毫秒级重试,会在一分钟内并发打满配额,也会把别人调通的调用方式原样克隆过去。Valve 近期回应开发者对 AI 生成的低质内容与克隆作品泛滥的担忧,本质上和平台面对的是同一个问题:当机器成为主要生产者,规则必须从“相信调用方”转向“验证调用方”。
对 API 平台来说,这个转向最终落在两个子系统上:限流决定谁能用、用多快,计费决定用多少、付多少。二者共享同一份计量数据,却又有着完全不同的失败代价——限流误判只是让用户等几秒,计费出错则会让账目和信任同时崩掉。本文以 API.PLUS 这类提供 IP 查询、随机动漫壁纸等免费接口的公共服务为背景,讲一套可落地的设计。
一、先把“稳定”拆成四个可测量的目标#
“稳定”是个形容词,不是指标。设计之前,先把它拆成四条可以写进 SLO 的约束:
| 目标 | 含义 | 可观测方式 |
|---|---|---|
| 可用性 | 限流组件自身故障时,业务接口仍可服务 | Redis 不可用时的降级成功率 |
| 公平性 | 单个租户或单个 IP 不能挤占全局容量 | Top10 租户 QPS(Queries Per Second,每秒请求数) 占比 |
| 账目一致性 | 计费允许延迟,不允许重复或遗漏 | 日终对账差额率 |
| 可解释性 | 每个 429 都能回答“为什么被拒、何时恢复” | 429 响应体完整率 |
这四条里,最容易被忽略的是最后一条。AI 智能体拿到一个没有 Retry-After、没有错误码的 429,最常见的反应不是停下,而是立刻重试——于是限流器反而成了放大器。所以拒绝请求时,一定要把原因、剩余额度、恢复时间写清楚。
二、三层限流:边缘挡量、应用算账、租户控总量#
1. 边缘层:用 Nginx 做第一道粗筛
绝大多数刷量请求连业务代码都不该进。把基于 IP 和 API Key 的限流放在反向代理层,成本最低、见效最快:
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
limit_req_zone $binary_remote_addr zone=api_per_ip:20m rate=10r/s;
limit_req_zone $http_x_api_key zone=api_per_key:40m rate=50r/s;
limit_req_status 429;
server {
location /api/ {
limit_req zone=api_per_key burst=100 nodelay;
limit_req zone=api_per_ip burst=20 nodelay;
add_header Retry-After 1 always;
add_header X-RateLimit-Scope "key" always;
proxy_pass http://127.0.0.1:9000;
}
}边缘层的定位是“防雪崩”,不是“算钱”。它的速率必须比业务层宽松,否则真实用户会被误伤,而计费精度也不该依赖 Nginx 的日志。
2. 应用层:Redis 令牌桶,一次 Lua 脚本搞定读取与扣减
真正的配额判断要在应用层完成,因为这里能拿到租户身份、接口权重和套餐等级。用一个原子 Lua 脚本实现令牌桶,避免“先 GET 再 SET”带来的并发穿透:
-- KEYS[1] = 限流键(tenant:endpoint)
-- ARGV[1] = 桶容量,ARGV[2] = 每秒补充速率
-- ARGV[3] = 当前毫秒时间戳,ARGV[4] = 本次消耗令牌数
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1])
local ts = tonumber(data[2])
if tokens == nil then tokens = capacity; ts = now end
local delta = math.max(0, now - ts) / 1000.0
tokens = math.min(capacity, tokens + delta * rate)
local allowed = 0
if tokens >= cost then
tokens = tokens - cost
allowed = 1
end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(capacity / rate * 1000) + 1000)
if allowed == 0 then
return {0, math.floor(tokens), math.ceil((cost - tokens) / rate * 1000)}
end
return {1, math.floor(tokens), 0}返回值的第三个字段是“还需等待多少毫秒”,直接写进 Retry-After 和响应体,客户端就知道该怎么退避。对于像 IP 查询接口 这种轻量只读接口,一次请求消耗 1 个令牌;对于会触发上游抓取的 随机动漫壁纸接口,建议消耗 2~5 个令牌,让不同成本的操作在同一个桶里体现差异。
3. 租户层:日/月配额与并发上限
速率限流管的是“多快”,配额管的是“总共多少”。两者缺一不可:一个智能体完全可能以合规速率跑满整月额度。配额用 Redis 计数器实现,按 tenant:2026-10-28 建键,超出后返回业务错误码而不是 429,便于调用方区分“限速”和“用完了”。另外对长耗时接口再加一个并发信号量,防止少量慢请求把工作进程全部占住。
三、计费:预扣、结算、对账三步走#
计费最容易出事的地方,是把“扣费”和“业务调用”写在同一个事务里。正确做法是拆成三阶段,允许最终一致:
- 预扣:请求进入时按最大可能消耗预占额度,写一条 pending 记录;
- 结算:调用成功后按实际消耗修正,失败则释放;
- 对账:日终把明细汇总到日账单,与额度账户核对。
支撑这一切的核心是幂等。每个请求必须携带或生成一个全局唯一 ID(推荐 ULID,时间有序便于分片),数据库以它建唯一索引:
CREATE TABLE usage_event (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
request_id CHAR(26) NOT NULL,
tenant_id BIGINT UNSIGNED NOT NULL,
api_key_id BIGINT UNSIGNED NOT NULL,
endpoint VARCHAR(128) NOT NULL,
units INT NOT NULL DEFAULT 1,
cost_micro BIGINT NOT NULL,
occurred_at DATETIME(3) NOT NULL,
UNIQUE KEY uk_request (request_id),
KEY idx_tenant_time (tenant_id, occurred_at)
) ENGINE=InnoDB;有了唯一索引,重试、消息重投、补偿任务都不会重复计费。业务侧的骨架大致如下:
def handle(request):
rid = request.headers.get('X-Request-Id') or new_ulid()
if not reserve(tenant_id, rid, estimated_units):
return error(429, retry_after_ms=next_available(tenant_id))
try:
result = call_upstream(request)
except RetryableError:
release(tenant_id, rid) # 上游失败不计费
raise
settle(tenant_id, rid, actual_units(result))
return result注意两点:其一,estimated_units 要给足冗余,宁可预扣多一点在结算时退回,也不要中途追加导致扣费失败;其二,明细表写入必须异步批量落库,同步写库会把接口 P99(99 分位延迟) 直接拖垮。对账时只做一件事:把 usage_event 按租户汇总,与账户流水比对,差额率超过千分之一就告警并冻结异常账户,而不是自动修正——账目自动修正会掩盖真实故障。
四、AI 客户端带来的三个新问题#
重试风暴
智能体默认会重试,而且不区分错误类型。对策是明确区分可重试与不可重试:429 和 5xx 返回结构化错误体并附 Retry-After,4xx 校验错误直接返回终态。同时在文档中建议指数退避加随机抖动——这一点比任何服务端优化都有效。
并发占用与慢请求
AI 工具倾向于一次性发起大批并发。如果只用令牌桶,瞬时并发仍可能打满连接池。补充一个“在途请求数”限制,并对超过阈值的长尾请求直接拒绝,比让它们排队更友好。
克隆与滥用
当调用方式可以被轻易复制,API Key 就会变成事实上的公共资源。常见做法是 Key 与来源绑定(可选 Referer/Origin 白名单)、单 Key 设备指纹数量上限、以及基于调用模式的异常检测(同一 Key 在极短时间内出现大量不同 UA 与出口 IP)。这些手段不必全上,但至少要能回答“这个 Key 现在被谁在用”。
五、观测与压测:让限流器自己也被监控#
- 指标:限流命中率、429 占比、令牌桶平均余量、预扣未结算笔数、对账差额率;
- 日志:每个 429 记录租户、接口、触发规则、剩余额度,采样不必 100%,但被投诉的请求必须可查;
- 压测:不要只压正常路径,专门压“限流器与被限流器同时高负载”的场景,观察 Redis 抖动时降级逻辑是否生效;
- 灰度:新增限流规则先在 1% 流量上以“只记录不拦截”的影子模式跑一天,确认真实用户不会被打到再放开。
压测时可以直接用平台上的真实接口作为被测目标,例如 curl -s 'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8' 与 curl -s 'https://www.api.plus/api/dongman?type=json',前者是纯计算型,后者可能涉及外部数据拉取,两者混合压测更能暴露配额分配的偏斜问题。更多响应格式约定可以参考 API 文章中心 中关于 JSON 与图片接口响应设计的说明,POST 类接口的幂等示例可查看 POST 示例接口。
六、总结#
一套稳定的 API 限流与计费系统,本质是把“信任调用方”换成“验证调用方”,同时保留可解释性:边缘层挡住雪崩,应用层用原子令牌桶算准每一次消耗,租户层管住总量,计费层用唯一请求 ID 保证不重不漏,最后用日终对账兜底。面向 AI 客户端时,还要额外关照重试、并发与克隆这三件事——它们不会因为限流做得好就消失,只会因为响应头写得好而收敛。
如果你正在为项目挑选稳定的公共接口,可以直接在 API.PLUS 注册账号获取免费 API Key,先用 IP 查询接口 或 随机动漫壁纸接口 跑通调用链路,再结合上面的限流思路做客户端侧的退避与缓存;遇到配额或错误码相关的问题,可通过 技术支持 反馈。