本节摘要:优化闭环省钱,容量规划保命。本节做三件事。其一,容量测算:从日调用量出发,经有效时长与峰值系数得到峰值 QPS,再乘单次 token 量得到峰值 token/分钟,用利特尔法则(并发 = 到达率 × 平均时延)得到稳态并发——一条算式链把"我们的系统要多大"变成四个可核对的数。其二,双向设限:向上对照供应商限额(TPM/RPM 与并发池,各档位以官方文档为准),向下对齐自身预算(第 3.3 节日预算折算峰值分钟预算),两个方向取严。其三,限速策略:全局、功能、用户三层令牌桶(附纯标准库实现),配优先级排队与 429 降级链(退避→切备用模型→排队→拒绝),并把限速当作可演练、可验收的能力——正如第 1.2 节第 16 问的精神:最坏情况下的损失必须是设计值。本节末尾收束全书:治理不是项目,是节奏。
四个数依次推出来(全部以示意参数演算):
① 平均 QPS = 日调用量 ÷ 有效时长(秒) = 20,000 ÷ (12 小时 × 3,600) ≈ 0.46 ② 峰值 QPS = 平均 QPS × 峰值系数(业务节奏,2~4,示意取 3) ≈ 1.4 ③ 峰值 TPM = 峰值 QPS × 60 × 单次 token(输入 2,000 + 输出 500) ≈ 1.4 × 60 × 2,500 ≈ 21 万 token/分钟 ④ 稳态并发 = 峰值 QPS × 平均时延(秒)——利特尔法则(经典排队论结论) ≈ 1.4 × 6 ≈ 8~9 路并发
三个经验注解(社区经验值,示意):有效时长别用 24 小时自欺——办公类产品流量集中在营业时段,用 24 小时分母会把峰值低估数倍;峰值系数没有万能值,从自己的分摊账本(第 3.2 节按小时聚合)里看真实曲线,大促、发薪日、月初账单日的尖峰要单独建模;token 口径用账本里的实测均值(输入加输出),别用想象值。
算出峰值 TPM 后,从两个方向核对:
向上——供应商侧。 各家供应商按档位给 TPM(token/分钟)、RPM(请求/分钟)与并发池上限(以各官方定价与限额文档当期版本为准,本书不列具体数值)。示例场景:峰值 21 万 token/分钟,某档位限额 10 万 token/分钟(示意)——差距的处置三选一:升档(花钱买容量)、多密钥分池(把流量按功能哈希到多个池,注意分池会稀释每个池的突发缓冲)、申请专项提额(大促前提前报备,社区常见做法)。
向下——自身侧。 第 3.3 节的日预算折算峰值分钟预算:日预算 240 美元(示意),峰值时段按消耗占比 15%(示意)折算,则峰值分钟预算约 240×15%÷60 ≈ 0.6 美元/分钟——超出它,要么是流量超预期(限速该拦),要么是单次成本恶化(闭环该修,第 7.1 节)。
两个方向取严者执行:供应商给 10 万、预算只撑 8 万,那系统级限速就配 8 万。别忘了与第 3.3 节三线联动:限速是第一道闸(事前),熔断是第二道(事中),预算线是第三道(事后核算)。
限速的单位用 token 而不是请求数——一次 8,000 token 的调用和一次 500 token 的调用对上游压力差一个数量级(第 3.3 节"token 与美元双轨"的同样道理)。
# rate_limit.py —— 令牌桶限速:token/分钟三层(全局/功能/用户)(纯标准库) import threading import time class TokenBucket: """容量即突发额度;每分钟匀速补充。try_take 返回(是否放行,建议等待秒数)。""" def __init__(self, capacity: int, refill_per_min: float): self.capacity, self.rate = capacity, refill_per_min self.tokens = float(capacity) self.updated = time.monotonic() self.lock = threading.Lock() def try_take(self, n: int) -> tuple[bool, float]: with self.lock: now = time.monotonic() self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.rate / 60) self.updated = now if self.tokens >= n: self.tokens -= n return True, 0.0 return False, (n - self.tokens) * 60 / self.rate class RateLimiter: """三层:全局桶 → 功能桶 → 用户桶。注:简化实现先扣后拒不回滚, 真实系统应预留或回滚,避免被拒请求白扣上层额度(工程细节,示意)。""" def __init__(self, global_tpm: float): self.g = TokenBucket(int(global_tpm * 0.2), global_tpm) # 突发=速率 20%(示意) self.features: dict[str, TokenBucket] = {} self.users: dict[str, TokenBucket] = {} def check(self, feature: str, user: str, planned: int) -> tuple[bool, str]: f = self.features.setdefault(feature, TokenBucket(8_000, 20_000)) # 示意 u = self.users.setdefault(user, TokenBucket(4_000, 5_000)) # 示意 for name, bucket in (("全局", self.g), ("功能", f), ("用户", u)): ok, wait = bucket.try_take(planned) if not ok: return False, f"[限速] {name}桶不足,建议 {wait:.0f} 秒后重试或降级" return True, "放行" if __name__ == "__main__": rl = RateLimiter(global_tpm=60_000) for i in range(4): print(f"请求 {i + 1}(约 4 千 token)→", *rl.check("客服问答", "u-42", 4_000)) print("另一用户 u-77 →", *rl.check("客服问答", "u-77", 4_000))
输出(示意,瞬时不等待):
请求 1(约 4 千 token)→ True 放行 请求 2(约 4 千 token)→ False [限速] 用户桶不足,建议 48 秒后重试或降级 请求 3(约 4 千 token)→ False [限速] 功能桶不足,建议 12 秒后重试或降级 请求 4(约 4 千 token)→ False [限速] 全局桶不足,建议 4 秒后重试或降级 另一用户 u-77 → False [限速] 全局桶不足,建议 4 秒后重试或降级
三层各防一种事故:用户桶防单用户刷量与失控会话的突发(第 3.3 节第 14 问的异常消费者);功能桶防一个功能的热点把别人的容量吃光(租户隔离);全局桶防供应商侧 429 雪崩——一旦触发上游限流,重试风暴会让局面指数恶化。planned 的取法与第 3.3 节熔断预扣同源:预计输入加 max_tokens,宁可误杀。
被限速的请求不该直接甩给用户一个错误。降级链从轻到重:
(文字流程图) 触发限速/429 ──▶ ① 指数退避重试(1s/2s/4s,上限 3 次) ──▶ ② 切备用模型或备用池(第 5.1 节降级类动作) ──▶ ③ 低优先级请求进队列(付费/核心请求优先) ──▶ ④ 拒绝并给用户可读话术("当前排队较多,请稍后")
验收把限速从"配置"变成"能力"(第 1.2 节按最坏情境提问的精神):
| 演练 | 做法 | 通过标准(示意) |
|---|---|---|
| 单用户突发 | 压测脚本单用户连发大请求 | 用户桶 10 秒内拦截,其他用户无感 |
| 功能热点 | 模拟某功能流量 ×10 | 功能桶兜住,其他功能 P95 无恶化 |
| 上游 429 | 混沌注入:模拟供应商限流 | 降级链自动走完,错误率有界、无重试风暴 |
| 大促扩容 | 按 ③ 的峰值 TPM 过一遍算式链 | 提前升档/分池到位,演练即验收 |
被限速与被降级的量本身要进观测面板——它们是容量规划偏差的最早信号(下一轮容量复评的输入)。
至此全书收束。三支柱让你看得见、说得清;告警与排障树让你行动有据;总装与选型让体系落地;闭环与限速让治理转动起来。最后一件事:拿出第 1.2 节的 21 问,每季度重答一遍——三层答案的逐季变好,就是这套体系真正的验收标准。附录三件(术语表、工具与指标速查、练习题)供你随查随练。