API 限流与计费系统怎么设计:先定用量记录,再谈流控与账单
限流与计费不应塞进同一个中间件:先在请求路径上写不可变的用量记录,再让限流消费它、让账单异步重算它。限流要分层并区分速率超限与配额耗尽;计费要版本化价格表并能回溯到单条请求;多实例计数、时钟、幂等与降级路径需要显式定义。本文不引用外部来源,算法与结构属通用工程做法,具体阈值与存储选型需按实际流量核实。
稳定的限流与计费系统,关键不在把两个功能合并进一个中间件,而在确定一条顺序:先在请求路径上留下不可变的用量记录,再让限流和计费各自消费这份记录。如果反过来先限流后计量,被拒绝的请求算不算用量、算不算进配额,就会变成随时可以改变的说法。本文没有可引用的外部来源,下面属于通用工程做法与建议,具体阈值、存储选型和一致性方案需按实际流量核实。
一、计量单位决定采样点#
计费对象可能是请求数、输入输出 token 数、字节数或调用时长。计量单位一旦确定,采样点位置也就确定:按请求计数在入口处采样即可;按 token 数必须等上游返回后才能结算;按时长则要处理超时与中断。建议把每次调用拆成“预扣”和“结算”两条记录,共享同一个 request_id,结算数量允许为 0,但要写明原因。
用量记录应追加写、不修改,至少包含:账户、密钥标识、路由、统一时间戳、计量单位、数量、计量口径版本。口径版本很重要,它让“上个月按次计、这个月按 token 计”这种变更不会污染历史数据。
二、限流放在计量之后,并分层#
先计量再限流,好处是被拒绝的请求同样有记录,事后能回答“为什么这个账户在 10:03 被拒了 200 次”。分层通常包括:账户级(预算或月度配额)、密钥级(防止单个泄漏密钥打满账户额度)、路由级(保护昂贵上游)、全局级(保护自身)。
| 算法 | 适用位置 | 代价 |
|---|---|---|
| 固定窗口计数 | 日/月配额等粗粒度 | 窗口边界允许双倍突发 |
| 滑动窗口日志 | 需要精确的小流量场景 | 存储随请求量线性增长 |
| 滑动窗口计数 | 常规速率限制 | 近似值,边界有误差 |
| 令牌桶 | 允许受控突发的接口 | 需定义速率与桶容量两个参数 |
| 漏桶 | 向下游输出平滑流量 | 排队会引入延迟 |
| 并发信号量 | 控制同时在途请求 | 与速率限制互补,不能替代 |
拒绝时要区分语义:速率超限返回 429 并带 Retry-After;配额耗尽应给出明确的错误码和重置时间,让客户端知道重试无用。两者混用会让调用方写出无效的重试逻辑。
三、账单是异步重算出来的#
不要在请求路径上算钱。请求路径只负责写用量事实,账单由异步任务按价格表重算,并在账单中记录引用的价格版本。这样调价、补算、退款都变成“用同一份用量加另一版价格重算”,而不是修改历史账单。对账口径应能下钻到 request_id:用量汇总与账单金额的差异,必须能定位到具体记录,否则对账只是两个数字碰巧相等。
四、容易出问题的边界#
- 多实例计数:本地计数快但会漂移,集中计数准但有延迟和单点风险。常见折中是本地预限流加集中校准,但误差范围要写进可观测指标,而不是假设它不存在。
- 时钟:窗口边界和计费时间戳不要依赖各节点本地时钟,应使用统一时间源。
- 幂等:客户端重试必须带幂等键,否则“预扣成功、结算失败”的重放会造成重复计费。
- 降级:计量存储不可用时,放行并标记待补,还是直接拒绝,必须显式选择并告警。把这个问题留给异常分支,实际就是默认选择“不确定”。
# 未执行示例:仅示意调用顺序,不是可运行实现
def handle(req):
rid = req.headers["Idempotency-Key"] # 占位,需客户端提供
meter.reserve(rid, account=req.account, unit="request", qty=1)
if not limiter.allow(account=req.account, route=req.path):
meter.settle(rid, qty=0, reason="rate_limited")
return 429, {"error": "rate_limited"}
resp = upstream.call(req)
meter.settle(rid, qty=measure(resp))
return resp
这段伪代码假定计量与限流是两个可独立失败的组件,接口名均为占位。落到具体网关或代理上时,其限流与计量能力需按实际文档核实,本文不声称任何产品支持上述接口。
结论#
设计顺序应当是:用量记录 → 分层限流 → 异步计费 → 对账。限流和计费都依赖同一份用量事实,系统在调价、补算和故障复盘时才有一致的解释。阈值大小、集中计数的实现方式、降级策略的具体取舍,本次材料不足以给出结论,需按实际流量和一致性要求验证。