限流与计费系统“稳定”在哪:计数口径、时间窗口与失败降级
稳定的限流与计费系统,关键通常不在于选择哪种算法,而在于同一次请求在限流侧和计费侧是否被记成同一件事,以及计数依赖失效时选择拒绝还是放过。文章按顺序给出用量事件定义、计数层取舍、时间窗口边界、限流与计费同源、降级策略和对账清单,并说明厂商侧参数需以官方文档核实。
稳定的限流与计费系统,难点通常不在限流算法本身,而在两件事:同一次请求在限流侧与计费侧是否被记成同一件事;以及依赖组件失效时,系统选择拒绝服务还是放过流量。下文按设计顺序展开,不引用任何厂商的实测数字。API.PLUS 的接口形态、配额单位与计费规则待核实,接入前应以官方文档为准。
一、先把“一次请求”定义成可重放的事件#
限流按“次”算、计费按 token 或时长算时,两侧口径不同,账就容易对不上。建议先定义一个用量事件(usage event),至少包含:幂等键、租户或密钥标识、计量单位与数量、事件发生时间、命中的策略版本。幂等键由网关或调用方生成,保证重试不会重复计数。这是设计建议,不代表某家实现的现状。
{
"idempotency_key": "YOUR_KEY-request-id",
"subject": "tenant-id",
"unit": "request",
"amount": 1,
"occurred_at": "2026-11-09T00:00:00Z",
"policy_version": "v1"
}
未执行的示例,仅示意字段结构,不对应任何真实接口。
二、计数放在哪一层#
三种常见形态各有代价,下表是工程取舍,不是性能实测,具体指标需按自己的流量压测。
| 形态 | 判定延迟 | 跨实例一致性 | 存储失效时 |
|---|---|---|---|
| 本地内存计数 | 低 | 弱,各实例独立 | 仍可限流,但总额度被放大 |
| 集中存储计数 | 取决于往返开销 | 强 | 判定链路整体受阻 |
| 本地预扣加集中对账 | 低 | 最终一致 | 可降级为粗粒度保护 |
三、时间窗口与时钟#
固定窗口在切换点附近可能放行接近两倍速率的突发,这是算法性质而非实现缺陷;滑动日志精度高但存储成本大;两个相邻桶加权的滑动窗口计数是常见折中。多实例各读本地时钟时,窗口边界会漂移,建议以存储侧时间或统一时间源为准,并同时记录事件时间与写入时间两个字段,便于事后判断归属窗口。
四、限流与计费读同一份事件#
如果限流判定和计费聚合走两条独立路径,就可能出现“限流放过了、计费没记上”。更稳妥的做法是让同一份用量事件同时驱动两者:限流读取短期聚合,计费读取长期聚合。账单最好由原始事件重放生成,而不是只依赖内存或缓存里的计数器;原始事件需保留一段可对账窗口,长度取决于业务结算周期。
五、失败时选择哪一种降级#
计数存储不可用时,fail-open 会把压力传给下游,fail-close 会造成大面积不可用。常见折中是每个实例本地预留一小份额度,降级期间只做粗粒度保护,并把这段区间显式标记出来,事后按事件补算。选哪种错误,取决于业务更能承受超卖还是中断,应由产品和运维共同确定,不宜由中间件默认决定。
六、上线前的核对清单#
- 幂等键是否覆盖所有重试与超时重发路径;
- 计量单位是否单一,是否存在“次”和“token”混算;
- 窗口边界是否使用统一时间源;
- 限流与计费是否读取同一条事件流;
- 降级策略是否写入文档,且在监控中可识别;
- 原始事件保留多久,能否重放生成账单;
- 被拒请求数、计数写入失败数、两侧数量差异是否有指标。
其中“两侧数量差异”比对账告警更早暴露问题:它不等到用户投诉,就能指出口径已经分叉。
七、边界条件#
重试与超时是最容易重复计费的地方,其次是并发的额度扣减顺序。若在 API.PLUS 上落地,需先核实它的配额单位、错误码语义与重试约定,再决定幂等键放在网关还是客户端;这部分本次材料未提供,应查阅官方文档后确定。