前十一章教你逐个处理故障,这一章要求你把它们同时挡在库存之前。顶流开票进入 D-7,你第一次以技术负责人的身份签下完整作战方案。
创业第 4 年,「秒抢」累计用户约 1000 万。公司 230 人,后端 42 人;你已经是技术负责人。七天后的 20:00,顶流场次开票。会议室里坐着网关、订单、库存、支付、DBA 与 SRE 的负责人,白板中央只有一个问题:1000 万入口请求同时到达,怎样让库存层只看见它能承受的那一小部分?
你把库存量、流量和恢复条件并排放上大盘。6 万张票意味着超过库存的购买意图终究不能成交;真正的目标不是让 1000 万请求都触碰库存,而是让被授权的一小股、可审计的请求进入交易核心。
Mia 问能否“扩到每个人都排得上”。你摇头:入口与库存容量相差约三个数量级,扩容不可能替代准入。作战方案必须先是一只漏斗,再是一组隔离舱,最后才是数据库和缓存。
A的问题是目标本身不成立。 1000 万首波请求不等于 1000 万有效交易;为无票请求扩库存层既昂贵,也会把锁、数据库和支付推入不可验证区。容量应服务业务漏斗,而不是追逐入口峰值。
B不但没帮上忙,还在加码。 入口已经过载,立即重试会把一次购买意图变成多次尝试;第 7 章的重试乘法会在开票最脆弱的时刻重新上演。客户端应拿到明确排队凭证或退避结果。
C该先做的就是它:先签容量契约,再谈组件。 库存层目标 1 万 QPS 低于约 1.2 万的压测容量,漏斗每层都能拿到明确通过率、拒绝动作和负责人。这样 CDN、网关、排队与库存不是各自优化,而是在执行同一个端到端预算。
D方向没错,但不能先做。 库存扣减必须正确而高效,第 10 章已经讲过它的单一真相;但如果 1000 万请求仍能直达库存,再快的扣减也会被淹没。先定入口预算,再验证库存实现能稳定接住预算。
秒杀容量不是把入口峰值原样传到库存,而是让每层只把一部分请求交给下一层。静态化挡住无需进入源站的读,网关执行总量与公平性,排队层只释放库存能持续处理的节奏。
注意 ① 必须逐层写,不能把所有配额攒到最后一起取最小值——上游一旦被配额截断,下游的通过率就该乘在截断之后的输出上。把 min 放在末尾,等于假装上游没被截过,算出来的数会偏大。
按作战方案逐步验算:
1000 万 → 100 万 → 30 万。CDN 挡住 90%,剩 100 万次请求;网关这一层的动作要按顺序说清楚,否则算式就不成立:先按账号与设备去重(同一账号的重复点击只留一个),再按预先拍板的候选配额截断到 30 万个合格购票意图。注意配额是设定出来的(定成库存的 5 倍),不是乘出来的;30% 只是事后回看的有效放行率(30 万 ÷ 100 万)。顺序不能反:去重不是保数量的操作,先取 30% 再去重,剩下的唯一意图通常不足 30 万。下文一律说「意图」,不说「人」——一个人可能发出多次请求,也可能被风控合并。而票只有 6 万张,供需比 5:1。
这 30 万只是候选,不等于 30 万个都进队列。准入层还有第二道门:按库存发凭证——发约 8 万张(6 万库存的 1.3 倍,留出弃单余量),其余约 22 万当场收到「未获得本轮资格」。真正入队的 8 万,以 1 万 QPS 释放,约 8 秒排空。
这里有一个容易被会议 PPT 抹掉的关键转折:前几层削减的是「请求数」,而库存层关心的是「速率」——百分比相乘不会把次数变成 QPS。入队的 8 万个意图究竟是 1 万 QPS 还是 5 千 QPS,取决于它们被摊在几秒里,而这恰恰是排队层自己决定的。所以排队层的参数不是「通过率」,而是释放速率:按库存约 1.2 万 QPS 的压测容量留出余量,定在 1 万 QPS(水位约 83%),8 万个入队意图约 8 秒放完。(若不设第二道门、让 30 万全部入队,同样的释放速率要排 30 秒——那正是下面 D2 里 A 方案的代价。)
这里还藏着一个常见误读:事后说「网关有效放行率 30%」,分母是 CDN 后的 100 万,不是原始的 1000 万——对原始入口而言这个比例只有 3%。每层都必须记录自己的输入、输出、单位与分母,否则会议里的百分比无法拼成一条可执行链。更要紧的是别把观测值当控制值:网关真正的控制参数是「候选配额 30 万」,30% 是配额除以当时到达量算出来的结果;到达量一变,这个百分比就变,而配额不该跟着变。
全景图的重点不是组件数量,而是入口请求在每一箭头上都有明确去向:静态读不进源站,无资格请求不进排队,无凭证请求不进库存,扣减失败不进支付,订单落库失败进入补偿而不是让用户盲目重试。
| 层 | 正常契约 | 红线 | 触发动作 | 恢复条件 |
|---|---|---|---|---|
| CDN / 静态化 | 挡住约 90% 详情与资源请求 | 回源率 > 15% | 延长安全静态内容、关闭个性化 | 回源率连续五分钟低于 10% |
| 网关 / 风控 | 去重后按候选配额截断至 30 万(事后有效放行率约 30%) | 候选意图超过 30 万,或发出凭证超过 8 万 | 收紧令牌、按用户与设备拒绝 | 下游 P99 与错误预算恢复 |
| 排队 / 凭证 | 向库存释放约 1 万 QPS | 队龄或积压超过用户预算 | 停止发新凭证、快速告知售罄概率 | 消费速率持续高于生产速率 |
| 库存 | 原子扣减、幂等、拒绝负库存 | 容量水位 > 0.85 或对账差异 | 收紧上游配额、暂停新扣减 | 水位回落且库存/订单对平 |
| 订单 / 支付 | 异步建单、支付有重试预算 | 积压单调增长或渠道熔断 | 降级非核心读、切渠道、停止盲重试 | 积压可在承诺时间内追平 |
这只漏斗不是凭空出现:第 6 章提供网关限流与降级,第 8 章提供热点场次独立通道,第 9 章用消息队列削峰,第 10 章守住库存真相,第 11 章给重复请求加幂等护栏。第 16 章会把这些预案放进生产演练,检验箭头遇到真实故障时是否仍然成立。
本章不新增编号。作战方案要做的是把前半书的病历卡钉到具体层:哪一层负责挡、看什么形状、失败后开什么开关。下面 13 张卡覆盖从入口到订单落库的主要故障面。
病历卡不是复习清单,而是上线门禁。任一防线若没有指标、开关、负责人和恢复条件,就不能在 D-1 战备会上被标成绿色。
超过库存处理能力的请求不能继续向下。长队列给用户“还有机会”的体验,快速随机拒绝保护系统并缩短等待,短队列加准入凭证则折中两者。三个方案都成立,但公平性、资源占用与产品承诺不同。
A · 全部排队:先来后到容易解释,用户能看见位置,库存释放节奏也可控。 账单:超长队列占状态与连接,位置不等于可买到票;若预计等待超过开票窗口,继续排只是在延迟失败。适合供给会持续补充、等待时间可预测的场景。
B · 快速随机拒绝:系统成本低,用户迅速得到结果,超量流量不会形成积压。 账单:随机公平难向用户解释,网络与设备差异可能影响机会;拒绝后的客户端还要被禁止立即重试。适合库存极少、开票窗口短、系统存活优先的场景。
C · 短队列 + 凭证:只维护库存层能在用户预算内消化的队列,凭证让准入可审计,超龄请求及时退出。 账单:凭证签发、防伪、过期与多端同步增加复杂度;队列长度和超时必须随消费能力动态校准。适合高价值秒杀且需要兼顾公平、体验和容量的场景。
「秒抢」的选择:采用 C。只给约 8 万个合格购票意图发短期凭证(约为 6 万库存的 1.3 倍,留出弃单余量),其余约 22 万当场返回「未获得本轮资格」,不进队列空耗;客户端按凭证轮询,不自行重试。理由是 6 万张票无法让全部 30 万个候选意图都进入长队,而可审计凭证比纯随机拒绝更容易处理公平与风控争议。
假设产品把公平性规则改了:不再是「先到先得」,而是实名抽签——30 万个合格购票意图全部先登记,开票后统一摇号抽出 6 万个中签名额,与谁先点进来无关。你在 D2 的选择还成立吗?
翻转变量是「决胜规则」——到达顺序还是随机摇号。D2 的队列、凭证、超时,本质上都是在给「谁先到」这件事赋予意义:位置代表先后,凭证代表「你比后面 22 万人更早」。一旦规则改成摇号,到达顺序和中签概率不再相关,A 这个方案立刻不成立——用户排的每一分钟队都是纯粹的沉没成本,系统还要为一个不再决定结果的顺序去维护连接和状态,纯浪费。B 才是对的:既然顺序不影响结果,就不该用「先筛掉 22 万」的方式制造顺序假象,而应该把全部合格意图都当场确认收到、后台批量摇号——这样最快,用户体验也最诚实。C 是这道题最容易踩的陷阱:它嘴上说「抽签」,做法却是先用到达顺序砍掉 22 万人的资格,只在剩下的 8 万里摇号——这不是随机,是给「先到先得」换了个更好听的名字,公平性承诺和实际机制对不上。
规则翻了,但账没有翻:入口限流(第 6 章)、热点场次独立通道(第 8 章)、消息队列削峰(第 9 章)、库存真相(第 10 章)、幂等护栏(第 11 章)——摇号制下这五道防线一个都不能少,这里要把两件事分开,否则前后会打架:登记是一次轻量写入(记下「这个实名用户想要」),它可以全量接住 30 万——这正是摇号制相对排队制的优势;但中签之后进入下单与支付链路的那 6 万,走的仍是原来那条重路径,入口限流、热点通道、削峰、库存真相、幂等护栏一道都不能少。实名去重照样要靠幂等键做,交易侧的容量预算一分钱没省。翻的只是「谁能进最后这 6 万」的判定方式,不是「系统能不能扛住 30 万」这道题。
回放从 D-7 到开票的正确顺序——先定契约,再压测,随后演练失败,最后才面对真实流量:
冻结漏斗预算。确认「1000 万次 → 100 万次 → 30 万个候选意图 → 发凭证 8 万」的逐层目标,以及排队层 1 万 QPS 的释放速率;每层登记 owner、红线和降级开关;释放速率低于压测容量。
全链路压测。使用隔离测试数据回放百倍入口,验证 CDN 回源 9.4%、网关 29.4%(据此产生候选意图约 27.6 万,按库存发出凭证 8 万);排队层实测释放约 9,000 QPS,库存峰值约 9,000 QPS,订单对账无差异。
故障演练。依次注入缓存 miss、支付超时和一个订单消费者下线;限流、熔断、积压告警与自动回滚均触发,发现并修复一处客户端立即重试。
按契约开票。入口峰值接近预估,库存层峰值约 9,700 QPS;核心 P99 420ms,库存与订单最终对平。售罄后停止发凭证,队列没有拖成亡灵流量。
订单消息积压曲线呈 ▁▂▃▄▅▆▇,生产速率持续高于消费速率。漏斗哪一层应立即收紧?
B。单调爬升不回落是 No.11 消息积压,生产持续大于消费时要从上游减量。D 很诱人,因为积压发生在订单域,但扩大查询池不改变消息消费差额,还可能抢走消费者的数据库资源。
首波 1000 万次请求,CDN 后通过 10%。网关去重后按候选配额截断至 30 万个合格意图;准入层按库存发 8 万张凭证;排队层以 1 万 QPS 向库存释放。问:网关的有效放行率与实际排空时间?
B。有效放行率的分母是网关的到达量,也就是 CDN 后的 100 万:300,000 ÷ 1,000,000 = 30%。排空时间只看真正入队的 8 万张凭证:80,000 ÷ 10,000/s = 8 秒。A 是最诱人的错误项——它把分母取成了原始入口 1000 万(30 万 ÷ 1000 万 = 3%);百分比脱离分母就没有意义。C 默认「候选数 = 入队数」,漏掉了准入层这道门,30 秒是假如不设门才会发生的事。D 把入队数当成了放行数(8 万 ÷ 100 万)。
库存稳定容量约 1.2 万 QPS,排队层当前释放 1.5 万 QPS。最正确的处理是什么?
A。释放速率必须服从已验证的持续容量,还要给长尾与故障留余量。B 很诱人,因为 1.5 万只高出 25%,但容量边缘的排队会非线性恶化,平均能扛不等于峰值可持续。
顶流场次让一个订单分片独占大部分流量,其他分片仍空闲。漏斗里最对症的组合是?
B。这是 No.10 热点分片,隔离、打散与配额分别控制爆炸半径、分布和总量。C 是最诱人的错误项,但冷分片扩池帮不了热 key,热分片扩池还可能加重数据库争用。