云架构学习路线 · 学习站

架构取舍案例 · 75–100 分钟 · 先会读 HTTP 请求、数据库行与基本队列

超时后再点一次,会生成两份报告吗?

财务点“生成月报”,两秒后页面报超时。后台还在计算,他又点了一次。现在队列里有两份相同工作,费用记了两次。有人建议把 HTTP 超时改成一分钟,有人建议上消息队列。两个建议都没有回答:第一次到底有没有被受理?第二次是不是同一个业务意图?这节课先追踪这两个问题,再选择组件。

以下时间和容量是完整的教学输入,不是产品压测。你要画出请求和状态,手算一个突发,处理三个失败,再为另一种负载改方案。交付的是可以被反例检验的决定,而不是一张服务 logo 图。

先把“马上有结果”拆成两种承诺

案例给定条件
条件明确数值或边界
用户交互2 秒内知道是否受理;接受后可离开页面,5 分钟内取得报告
计算单个 worker 串行计算,一份报告固定 20 秒;本题忽略调度和下载开销
负载平时每分钟 1 个新意图;某次同时提交 10 个不同意图
存储受理接口最迟 80 ms 完成账本写入;事务成功后才算接受
排队容量1 个运行中任务 + 最多 8 个等待任务;FIFO,无优先级插队
业务边界同一用户、同一幂等键、同一参数只能创建一个任务;允许重新发起不同意图
计费边界本题只记录内部费用账本;真实支付、发邮件等外部副作用不在此保证内

2 秒约束针对受理,5 分钟约束针对完成。把它们都写成“响应时间”会导向错误方案。纯同步接口若等 20 秒才答复,即使最终报告正确,也不满足受理体验;把超时调长并没有改变这一点。异步接口可以尽快返回任务 ID,却必须提供后续状态和失败结果,不能把“进队列”叫成“已完成”。

HTTP 202 可表达请求已接受而尚未完成;本例采用它作为接口约定,不代表协议承诺任务一定成功。用户稍后用任务 ID 查询。对于耗时小于交互预算的简单查询,同步返回结果仍然合适,不需要为了“架构先进”加队列。

同步受理、异步执行与同步状态查询组合成一个旅程用户提交幂等键和参数,接口持久化任务后返回任务ID。Worker处理任务并提交结果,用户以任务ID查询;超时重发使用原幂等键。用户 / 客户端受理接口 + 账本持久待处理任务POST:同一意图沿用键 K202 + job IDworker:结果 + 费用GET /jobs/J42 → pending / running完成后 → succeeded + result超时没有告诉你是否提交;按原键查询 / 重发,不能随意创建新键。
图 1 · 一条用户旅程可以组合多种模式。主图之外还需要说明状态和失败,不能用“只能同步或异步”代替分析。

走过一次响应丢失,而不是只画成功箭头

幂等键由客户端为一个业务意图生成;服务端的唯一约束使用 (user_id, key),同时保存规范化参数的摘要。同一个键重来时,参数相同返回已有任务;参数不同则拒绝冲突,不能悄悄复用旧任务。键的保留期、过期后语义需要成为接口合同,本例假定 24 小时内保留,超过后要求用户查询历史并明确发起新意图。

完整请求轨迹:原键 K17,参数为“2026-09 月报”
时刻事件账本与客户端分别知道什么
0.00 s用户 U 提交 K17尚未证明受理
0.05 s事务创建 J42 与待处理记录持久化已成功;返回包丢失
0.10 sworker 开始 J42J42=running
2.00 s客户端等不到响应客户端只知道超时;不能推出服务端失败
2.10 s用户用原键 K17 重试返回 J42 的当前状态,不再创建任务
20.10 sworker 提交结果和内部费用J42=succeeded;内部费用一次
20.20 s确认消息丢失,引发再次处理检查已完成 J42,返回旧结果;不再次记费用

“请求已经超时”属于客户端观察;“事务是否提交”属于服务端事实。服务端若先提交任务再独立发布消息,进程在两者之间崩溃,就可能留下永远不执行的任务。小系统可以直接轮询数据库中的待处理任务;更大的系统可在创建任务的同一事务里写待发布记录,再由投递器重试发布。后者通常叫事务 outbox。重复投递仍然可能发生,所以消费者仍需按任务 ID 判定,而不是相信队列会替业务自动去重。

受理与执行的状态边界任务经过pending running到succeeded或failed。超时不是服务端任务状态;重试查询原任务。结果提交与内部费用写入必须作为同一业务事务,外部副作用需另外设计。pendingrunningsucceededfailed客户端 timeout ≠ 服务端 failed;处理权恢复也不等于重复执行外部副作用。结果 + 内部费用一起提交;真实支付必须另有幂等或对账补偿。
图 2 · 状态必须可查。worker 崩溃后的重新领取还需要租约与并发控制;本案例没有把这些未实现机制宣称为生产保证。

队列解决等待,不创造容量

一个 worker 每 20 秒完成一份,即每分钟 3 份。平时每分钟 1 个意图,在本题稳定耗时模型下,工作占用比例为 1/3;这不是实测 CPU 利用率。突发 10 个不同意图时,一个立即运行,八个等待,最后一个被容量限制拒绝。已接纳的第九个在 160 秒开始、180 秒完成,仍在 300 秒预算内。拒绝应明确可重试与退避方式,不能返回“受理成功”再丢任务。

如果只把等待容量改为 20,十个都能接纳,第十个 200 秒完成;但源源不断每分钟 4 个的长期到达率仍超过每分钟 3 个的服务能力,任何有限队列都会满。加 worker 能否线性提升还取决于数据库、下游和资源,不能从本题串行模型直接推广。这里的架构决定同时包含接纳、等待时限、执行容量和用户可见状态。

小检查:队列容量 8 和总容量 8 是一回事吗?

不是。本题最多 8 个等待,加 1 个运行,共 9 个接受。若约定为“系统总共最多 8 个”,同样突发则拒绝 2 个;第八个完成于 160 秒。必须先说明分母与状态,再写数字。

你来为三次扰动做决定

  1. 沿上表逐行画客户端观察与服务端状态。然后把第二次请求改成新键 K18,写出为什么会形成第二个任务,以及 UI 如何保留原意图的键。
  2. 原键 K17 再次提交,但月份改为 2026-10。说明为什么不能返回旧报告或覆盖参数;给出错误响应与用户下一步。
  3. worker 在“提交任务”之后、“发布消息”之前崩溃。比较数据库轮询与 outbox 两种恢复方式,写出各自的简单性、延迟与运营代价;不要假定只能有一种正确答案。
  4. 将突发改为 16 个意图,等待容量仍为 8,计算接受、拒绝、最长完成时间。再把长期到达改为每分钟 4 个,解释增大队列不能解决的事。
参考与评分边界

K18 是新业务意图,在本例中可以生成另一个任务;前端需把请求键和任务 ID 与操作关联,超时后保留,明确“重新生成”才换键。相同 K17 却换月份应返回冲突(本例用 409)并保留 J42;用户确认新意图后用新键。轮询直接以持久任务表为真相,简单但需索引和控制查询成本;outbox 需要投递进度与清理,但任务和待投递记录可一起提交。16 个突发接受 9、拒绝 7,最后一份 180 秒完成。长期到达 4/min 会以平均 1/min 积压,扩队列只是推迟拒绝。通过需说明至少一个失败分支、参数绑定、容量边界及一种替代方案;只写“用了 MQ 所以可靠”不通过。

陌生负载:这里仍然需要队列吗?

另一个接口只读已经生成的报告摘要,给定端到端 P99=120 ms,用户预算 300 ms,无写副作用;峰值容量经过本次教学假设验证可满足。这时请给一条同步方案和一项仍需观察的指标,再解释为何不把“生成报告”的架构原样照搬。

参考

同步查询可直接满足给定预算,队列会增加状态与等待复杂度。继续观察尾延迟、错误率、超时与缓存版本;若负载改变,需要重新测容量。120 ms 是题设,不是你已在真实环境测出的成绩。

完成稿应只有一页架构决定、两条时序(正常/响应丢失)、状态表、突发算例与三项未知。回到原版集成模式继续学习补偿与契约,再用可靠性章节定义恢复目标。查阅 HTTP 202 定义时,找出它没有保证什么;别把接口码当成业务完成证据。