fedorthinks

速成课 · No. 18

当系统的一部分需要另一部分做点事情时,简单的做法是调用它并等待答复。强大的做法是把一条 message 丢进 queue 里然后继续往前走,让对方在准备好的时候再来取。这一个转变——从调用并等待,变成发消息并继续——正是系统保持快速、在故障中存活并实现扩展的方式。

21 个概念 · 每个都有一张图 · 结尾有速查表

§ 01

要明白 queue 为什么存在,先得切身体会一下那个简单替代方案的痛:一个服务调用另一个服务并等待它。它一直管用,直到真正要紧的那一刻。

地图:六组共 21 个概念

一张地铁图:先看清整条线路,再一站一站地坐过去。

先讲问题——为什么直接调用会让你失望。然后每组一章:queue(想法本身)、角色(谁写、谁读、谁保管)、command 与 event(两种消息)、好处(你付出代价换来了什么)和难题(什么会出问题)。最后一章讲什么时候该用 queue、什么时候不该用,另附关系图和速查表。

The direct-call problemSynchronous callFailure propagationBackground workThe queueMessage + queueTemporal decouplingAsynchronousRolesProducer / consumerBrokerCompeting consumersCommands & eventsCommandEventPub/Sub · fan-outEvent-drivenPayoffsResilienceLoad levelingIndependent scalingHard partsIdempotencyOrderingEventual consistencyDead-letter queue
六组概念——第 1–6 章依次讲它们。第 7 章讲如何选择,外加速查表。

如何阅读每个概念

一本护照:每个人的栏目都一样,所以很容易比较。

从这里开始,每个概念都是同一个结构:它的英文名称(记住它——这是你会在 broker 文档和面试里遇到的词)、一幅日常生活的画面、一张图,以及一张卡片:问题、思路、例子、代价,以及它与其他概念的关联。最后是关系图和速查表。

Synchronous Call——一次直接调用把两个服务绑在一起

给某人打电话,然后一直占着线直到他们把事情做完——你不能挂断,不能做别的任何事,而如果对方不接,你就被卡住了。

服务 A 让服务 B 做点事,最显而易见的办法是 synchronous call(同步调用):A 发问,然后等待,一直扣着手上的一切,直到 B 回答。这把它们紧紧地耦合在一起——A 的速度被 B 的速度绑架,B 没做完 A 就没法继续。如果只是快速答复,这没问题。但对缓慢或繁重的工作,A 的时间都花在了原地冻结、等别人上。

Service AService Bworkblocked · waiting on BworkB does the slow workcallanswerthe caller is frozen until the callee answers
A 调用了 B 然后原地不动:B 的所有慢活儿,都是 A 被阻塞的时间。
ConceptSynchronous Call· request–response(请求–响应)
问题
调用方被冻结,直到被调用方回答。
思路
发问、等答复、再继续。最简单的交谈方式。
例子
订单服务通过 HTTP 调用价格服务,等拿到价格后才显示购物车。
代价
紧密的耦合:A 的速度和可用性取决于 B。
关联
它的痛点由 Message Queue 来治;需要即时答复时,它仍是最佳选择。

Failure Propagation——被调用方挂了,调用方也跟着坏

打电话给仓库确认订单,因为没人接,你就拒绝接收任何新订单——一扇关上的门,堵死了整家店。

更深的问题是故障。如果 A 调用时 B 挂了或过载了,A 的请求也会失败——故障沿着调用链一路向上传播。一个缓慢或损坏的服务就能让所有依赖它的东西停摆,流向 B 的流量激增也会把 A 一起拖垮。紧密耦合意味着弱点会蔓延,系统的可用性只取决于它最脆弱的那一环。

CheckoutcallerOrderscaller + calleeWarehousedown / overloadedcallscalls✗ fails✗ failsone broken service stalls everything above it
仓库挂了——错误一路向上爬:订单失败,结账失败。
ConceptFailure Propagation· cascading failure(级联故障)
问题
一个服务出故障,会变成所有调用方都出故障。
思路
同步调用链的可用性,只取决于最弱的那一环。
例子
仓库不响应 → 订单报错 → 结账不能用了。
代价
流向一个服务的流量激增,会拖垮它上面的一切。
关联
用缓冲来治:Message Queue 带来 Resilience——故障变成延迟。

Background Work——有些工作不该让用户等

在收银台,你付了钱,拿着小票就走——不会站在柜台前看他们打包、发货、给你发邮件。慢的部分在你走后才进行。

很多工作不需要在用户收到响应之前完成——发确认邮件、生成发票、缩放照片。这就是 background work(后台工作)。让用户等这些全部做完,既慢又没必要。你想要的是接下请求、快速答复,再让慢的部分随后进行。直接调用做不到这一点;它让所有人为所有事情等待。

UserAPIaccept + replyfast replyQueueafter the replySend emailMake invoiceResize photothe user never waits for the slow parts
API 立刻答复;邮件、发票和照片稍后处理——从 queue 里取出来做。
ConceptBackground Work· background jobs(后台任务)
问题
用户在等一些其实与他们的响应无关的工作。
思路
接下请求,快速答复,慢的部分之后再做。
例子
付款成功——马上给小票;邮件、发票和照片缩放放到后台。
代价
后台工作的结果不会立刻出现。
关联
用 Message Queue 和 Asynchronous Processing 来实现。

直接调用把两个服务在时间上耦合起来:调用方要等,还要承受被调用方的缓慢和故障。对于缓慢或不紧急的工作,这就是个陷阱。

§ 02

解决办法是别再调用,改为发消息。queue 是中间的缓冲,让一边留下工作、另一边来取——在时间上把两者解耦。

Message Queue——丢下消息,继续往前走

留一条语音留言,而不是在电话那头干等:说清你要什么,挂断,继续过你的一天——他们有空时自会处理。

A 不再调用 B 并等待,而是写一条描述工作的 message(消息),把它丢进 queue——一条让消息排队等待处理的队伍。A 立刻就自由了,可以继续;它不在乎 B 什么时候处理。这条消息是一个小包裹,写着“需要做的事”,是被交接出去,而不是当场执行。这次交接就是全部的想法。

Service AsendermsgmsgmsgmsgService Breceiverqueuedrops & moves onmessages wait in linepicks up when ready{ "type": "SendEmail", "orderId": 42 }
A 丢下一条消息就继续走;消息在 queue 里等待;B 准备好后取走一条。下方——一条消息本身可能长什么样。
PatternMessage Queue· queue(队列)
问题
直接调用让发送方一直占着线。
思路
把工作描述成一条 message 放进 queue;接收方自己来取。
例子
不去调用邮件服务——而是在 queue 里放一条带订单号的 SendEmail 消息。
代价
多了一个需要运行和理解的活动部件。
关联
带来 Temporal Decoupling;由 Broker 保管;由 Consumer 读取。

Temporal Decoupling——queue 是时间上的缓冲

办公桌上的收件盘:工作堆在里面,被平稳地处理,所以突然涌来的一堆文件不会压垮那个人——只是让盘子暂时变高了一点。

queue 夹在两边之间,作为缓冲,在消息被处理之前先保管着。这就在时间上把它们解耦(temporal decoupling):发送方和接收方不再需要同时保持快速、可用,甚至不需要同时在运行。突发产生的工作可以被平稳地消费。queue 吸收了工作到来的速度与能完成的速度之间的差距。

arrivals: burstyQueuebufferprocessing: steadysender and receiver no longer need to run at the same time
左边工作成批涌来,右边被平稳处理。中间的 queue 吸收了两者的差距。
ConceptTemporal Decoupling· buffer in time(时间上的缓冲)
问题
双方必须在同一时刻都活着、都够快。
思路
在它们之间放一个缓冲:发送方和接收方各按各的节奏工作。
例子
一阵突发的订单堆在 queue 里,以稳定的速度被处理。
代价
工作可能要在 queue 里等——结果不是即时的。
关联
Resilience 和 Load Leveling 的基础。

Asynchronous Processing——不等结果

寄一封信,而不是当面交谈——寄出后就继续忙自己的,相信对方之后会读到并处理,不用你站在旁边。

这就是异步工作:发送方不等结果。它把消息发出去就继续,结果在之后、在带外发生。你放弃了直接调用那种即时答复,换来的是不再被阻塞。对于任何不需要立刻答复的事,这笔交易是快速、有韧性的系统的基础。

syncAblocked · waiting for resultA continuesasyncAA keeps working — never blockedworkerdoes the work latermessageasync: give up the instant answer, stop being blocked
上:同步,A 站着等。下:异步,A 发出消息继续干活,由 worker 稍后完成任务。
ConceptAsynchronous Processing· async(异步)
问题
在等一个你此刻并不需要的结果。
思路
发出消息,继续往前;结果在之后、在带外发生。
例子
报告在后台生成;准备好后用户再拿到。
代价
没有即时答复;更难追踪发生了什么。
关联
Synchronous Call 的反面;会带来 Eventual Consistency。

queue 是发送方与接收方之间的缓冲。丢下一条消息就继续走——工作在时间上被解耦,等对方准备好时再异步完成。

§ 03

三种角色让 queue 运转起来,给它们起好名字,就能理清大部分行话。一边制造消息,一边处理消息,中间有个东西把消息稳妥地保管起来。

Producer / Consumer——一边制造工作,一边完成工作

一间厨房:服务员把点菜单钉在架子上,厨师一张张取下来做。两边从不直接说话——那排单子架就是全部接口。

创建消息的一方是 producer(或 publisher);处理消息的一方是 consumer(或 worker)。producer 丢下一张单子;consumer 取走一张,做完活,再取下一张。它们互相不认识也不互相等待——只认识 queue。这种干净的分离,让每一边都能各自构建、扩展和修改。

Web appproducerMobile APIproducerQueuethe ticket railWorker 1consumerWorker 2consumerproducers and consumers never talk directly — they only know the queue
producer 往 queue 里写,consumer 从 queue 里读。它们从不直接碰面。
TermProducer / Consumer· publisher / worker
是什么
producer 创建消息;consumer 取走消息并完成工作。
思路
双方只认识 queue,不认识彼此。
例子
网页应用丢下“缩放这张照片”;worker 取走消息并完成缩放。
陷阱
producer 不知道工作何时、由谁来完成。
关联

Broker 位于两者之间;一个 queue 上有很多 consumer,就是 Competing Consumers。

Message Broker——broker 稳妥地保管消息

寄件人和收件人之间的邮局:它收下你的信,妥善保管,直到收件人来取——所以即使对方不在,也不会丢。

中间是 message broker——像 RabbitMQ 或 Apache Kafka 这样的软件,负责接收消息、可靠地存储,并把它们交给 consumer。它就是系统的邮局。可靠性是关键:即使所有 consumer 都挂了,broker 也会把消息保管好,直到有人准备处理,所以工作是被搁置,而不是丢失。

ProducerBrokerRabbitMQ · Kafkamsgmsgmsgmsgstored durablyConsumerdownevery consumer down? the messages stay parked, not lost
broker 接收并可靠地存储消息。consumer 挂了——消息在等待,而不是消失。
ToolMessage Broker· broker(消息代理)
问题
消息需要一个安全的地方存放,直到有人取走。
思路
专门的软件接收、存储并投递消息——系统的邮局。
例子
RabbitMQ、Apache Kafka。
代价
又多一个需要运行和维护的系统。
关联
保管 Message Queue;Resilience 建立在它的可靠性之上。

Competing Consumers——加 consumer 就能更快

点菜单堆起来时,你就往流水线上多加几个厨师——每人取下一张单子,积压清得更快,而订单进来的方式不用改。

因为 consumer 只是取下一条消息,所以你可以让很多 consumer 并行地消费同一个 queue——这就是 Competing Consumers 模式。积压在增长?多加几个 worker,它们会自动分担负载——每个抓取不同的消息。这是 queue 的一大优势:你可以通过增加 consumer 独立地扩展慢的那一部分,而不用碰 producer 或 queue 本身。

Queuem1 · m2 · m3 · m4 …Worker 1takes m1Worker 2takes m2Worker 3just added · takes m3backlog growing? add workers — they share the queue automatically
一个 queue 上有三个 worker:各取各的消息。再加一个——积压消得更快。
PatternCompeting Consumers· worker pool(工作池)
问题
一个 worker 跟不上;积压越来越多。
思路
多个 consumer 读同一个 queue;每条消息只交给其中一个。
例子
大促之前——照片缩放 worker 从两个加到十个。
代价
并行处理会打乱顺序——见 Message Ordering。
关联
Independent Scaling 的基础;适用于 Command 消息。

producer 创建消息,consumer 处理消息,broker 在中间稳妥保管。需要更高吞吐?加 consumer——它们自动分担这个 queue。

§ 04

一条消息可能是两种截然不同的东西:一道要求做某事的命令,或一则某事已经发生的通告。第二种会悄悄改变你设计整个系统的方式。

Command——command 告诉一个 worker 去做一项任务

交给某个特定部门的一张工单:“缩放这张图片。”它指明任务,期望恰好一个团队去做,然后就结束了。

command 消息是指向一个 consumer 的直接指令:“发这封邮件”“处理这笔付款”。这是把 queue 当作待办清单来用——producer 知道该发生什么,把任务交出去,由取走它的那个来做一次。这是 queue 最简单的用法:把具体的工作卸出去,稍后恰好做一次。

CheckoutproducerSendEmailEmail workerthe one consumercommand = “do this”a to-do item: done once, by whoever pulls it
SendEmail 命令:“做这件事。”恰好一个 consumer 取走它。
TermCommand· command message(命令消息)
是什么
一条命令式的消息:“做这个”。用动词命名。
如何运作
producer 知道该发生什么;由一个 consumer 完成任务。
例子
SendEmail、ProcessPayment、ResizeImage。
陷阱
producer 仍然知道需要什么工作——还留有一些耦合。
关联
Event 的反面;常由 Competing Consumers 来处理。

Event——event 宣告某件事已经发生

布告栏上的全公司通告:“刚刚有一位新客户注册了。”它不告诉任何人该做什么——谁关心,谁就按自己的方式反应。

event(事件)不一样:它陈述一个关于过去的事实——“订单已下”“用户已注册”——而不说谁该做什么。producer 只是宣告它,不知道也不关心谁在听。也许没人反应;也许有五个服务反应。这颠倒了关系:你不再命令某个特定的 worker,而是广播某件事成立了,让感兴趣的各方自己决定。

OrdersproducerOrderPlacedevent = “this happened”listener?listener?listener?the producer doesn't know who listens —maybe nobody, maybe five services
OrderPlaced 事件:“这件事发生了。”producer 不知道谁在听。
TermEvent· event message(事件消息)
是什么
关于过去的事实:“这件事发生了”。用过去时命名。
如何运作
producer 宣告它,不知道谁在听:零个、一个或五个服务。
例子
OrderPlaced、UserRegistered。
陷阱
更难看清最终会发生什么——反应分散在各个服务里。
关联
Command 的反面;通过 Pub/Sub 投递;Event-driven 的基础。

Publish/Subscribe——一个 event,多种反应

一份报纸只印一次,送到每位订阅者手里,每个人出于各自的原因阅读——体育迷、投资人、填字游戏爱好者——都来自同一期。

这种广播模型就是 publish/subscribe(pub/sub,发布/订阅):producer 把 event 发布到一个 topic,每个感兴趣的 consumer 订阅后拿到自己的一份副本。一个“订单已下”的 event 可以同时触发邮件服务、分析服务和发货服务——这就是 fan-out(扇出)。发布者甚至不知道它们的存在。以后可以随时加新的反应,完全不用动发布者。

OrderspublisherTopicorder.placedEmail serviceown copyAnalyticsown copyShippingown copyfan-outone event, many reactions — the publisher doesn't know they exist
一个 event 进入 topic——每个订阅者都拿到自己的副本:这就是 fan-out。
PatternPublish/Subscribe· pub/sub, topic, fan-out
问题
一个 event 对很多服务都重要,而且名单还在增长。
思路
发布到一个 topic;每个订阅者拿到自己的副本(fan-out)。
例子
“订单已下”→ 邮件、分析、发货同时做出反应。
代价
每份副本各自单独处理;整条反应链很难看清。
关联
投递 Event;Event-driven Architecture 建立在它之上。

Event-driven Architecture——event 解开了“谁认识谁”的耦合

给报纸增加一位新订阅者,丝毫不会改变报纸的写法——发布者根本不需要知道谁在读。

这就是 event-driven(事件驱动)设计的深层力量:event 的 producer 与所有对它做出反应的一方完全解耦。想加一个响应“订单已下”的新功能,只需订阅一个新的 consumer——订单服务不用改。这样构建的系统靠增加监听者来成长,而不是靠修改被监听的对象。大型系统就是这样演进的,而不至于所有东西互相依赖。

Order servicenever changesorder.placedEmailAnalyticsShippingLoyalty pointsnew featureadd a feature by adding a listener — not by editing the publisher
新功能就是新订阅者。订单服务不变。
PatternEvent-driven Architecture· EDA
问题
每个新功能都意味着要去修改“什么都在那里发生”的那个服务。
思路
服务宣告 event;功能作为新的监听者加入。
例子
按订单发放积分——让一个新服务订阅“订单已下”。
代价
Eventual Consistency,而且在消息之间跳转的请求更难调试。
关联
建立在 Event 和 Pub/Sub 之上;带来 Independent Scaling。

command 告诉一个 consumer 去做一项任务。event 宣告一个事实,让任何人都能反应——而 pub/sub 把一个 event 扇出给许多方,解开了“谁认识谁”的耦合。

§ 05

queue 增加了活动部件,所以它必须物有所值。它确实做到了,因为它换来了三样别处很难得到的东西:韧性、平稳应对流量高峰,以及独立扩展。

Resilience——consumer 挂了,只是工作被延迟

如果厨房关门一小时,点菜单就在架子上等着——重新开门后,厨师把积压的单子逐一做完。没有一张订单丢失。

因为 broker 保管着消息,consumer 崩溃不会丢工作——消息会等到它回来,然后被处理。对比直接调用,服务挂了就是彻底失败。queue 把“服务挂了”从硬错误变成了暂时的延迟。这种韧性(resilience)——工作经受住故障并恢复——往往是团队选用 queue 的最大单一理由。

direct callService AService Bdown✗ hard errorrequest failswith a queueService AQueuework parkedService Bdown → backa crash becomes a delay, not a lost order
上:B 挂了——请求失败。下:B 挂了——工作在 queue 里等待,B 恢复后继续。
ConceptResilience· fault tolerance(容错)
问题
用直接调用时,服务挂了意味着所有人都报错。
思路
broker 保管消息;故障变成延迟。
例子
邮件 worker 崩溃一小时——邮件在 queue 里等着,重启后发出。
代价
consumer 挂着的时候,工作会堆积并延迟完成。
关联
治愈 Failure Propagation;依赖 Broker 的可靠性。

Load Leveling——吸收流量高峰

山洪与小镇之间的一座水库:洪峰涌入水库,水库再以稳定、安全的速度向下游放水。

当一大波请求同时涌来——一场大促、一次病毒式传播——queue 让你可以削峰填谷(level the load):高峰填满 queue,consumer 按自己稳定的节奏把它排空。下游缓慢的系统从来看不到高峰,只看到可控、均匀的流量。处理积压所带来的压力叫做 backpressure(背压)。没有 queue,同样的高峰会直接冲击系统,很可能把它冲垮。

requests in: a spikeQueuebacklogbackpressureconsumers drain: steadythe downstream system never sees the spike
请求高峰填满 queue,consumer 平稳地把它排空。下游系统看不到高峰。
PatternQueue-Based Load Leveling· load leveling, backpressure(削峰、背压)
问题
流量高峰直接打到缓慢的系统上,把它冲垮。
思路
高峰填满 queue;consumer 按自己的节奏处理。
例子
一场大促:每分钟成千上万的订单,被当作平稳的流量来处理。
代价
积压增长,工作延迟完成(backpressure)——要盯着 queue 的长度。
关联

建立在 Temporal Decoupling 之上;Competing Consumers 负责清掉积压。

Independent Scaling——每一边各自扩展、各自演进

你可以多雇厨师、换厨房设备、再加一个工位——这一切都不用改变服务员点单的方式,因为他们只接触那排单子架。

解耦意味着每一边都可以独立地变化(independent scaling)。按需求增减 consumer;用另一种语言重写某个 consumer;新增一个对现有 event 做出反应的全新 consumer——全都不用碰 producer。queue 是系统各部分之间一道稳定的接缝,而稳定的接缝正是让大系统能一块一块地成长和变化、而不是一次性全部改动的原因。

ProducersuntouchedQueuestable seamScale workers 2 → 20Rewrite a worker in GoAdd a new consumer
queue 是一道稳定的接缝。在右边你可以扩展、重写、新增——左边什么都不用变。
ConceptIndependent Scaling· independent scaling and evolution(独立扩展与演进)
问题
改一个部分,就得动所有部分。
思路
queue 是稳定的接缝;每一边各自变化。
例子
一个 worker 被改用另一种语言重写——producer 根本没察觉。
代价
消息格式变成了一份契约——修改要谨慎。
关联
由 Competing Consumers 和 Event-driven Architecture 实现。

queue 换来韧性(工作经得起崩溃)、削峰(高峰被吸收)和独立扩展——这些都是直接调用根本给不了你的好处。

§ 06

异步消息解决了真实的问题,也制造了新的问题。没有一个是致命的,但假装它们不存在,正是基于 queue 的系统出现隐蔽、令人抓狂的 bug 的原因。

At-least-once 与 Idempotency——消息可能到达两次

为保险起见,邮局有时会再投递一份它不确定是否送到的信的副本——宁可两次也别没有,但现在你可能会对同一条指示执行两次。

大多数 broker 保证 at-least-once(至少一次)投递:它们宁可把消息发两次,也不冒丢失的风险,所以 consumer 偶尔会收到同一条消息不止一次。如果“向客户扣款”执行了两次,那就是真问题。解决办法是 idempotency(幂等):把工作设计成做两次与做一次效果相同(先检查“是否已经扣过款?”)。假定会有重复;让重复变得无害。

Brokerat-least-oncecharge order 42same msg againConsumeralready charged?charge ✓skipnoyesidempotent consumer: a duplicate does no harm
broker 把“给订单 42 扣款”发了两次。consumer 先检查“是否已扣款?”——于是直接跳过第二条。
PracticeIdempotency· at-least-once delivery(至少一次投递)
问题
At-least-once 投递:同一条消息可能到达不止一次。
思路
把工作设计成执行两次与执行一次效果相同。
例子
扣款前先检查“订单 42 是否已扣款?”——如果是,就跳过。
代价
你需要在某个地方记住哪些事已经做过。
关联

每个 Consumer 都必须具备;与 Dead-Letter Queue 一起,让重试变得安全。

Message Ordering——顺序没有保证

按先后顺序寄出的两封信,到达时可能是任意顺序——所以如果第二步比第一步先到,收信人就会一头雾水。

消息很多、consumer 又并行时,消息不一定按发送的顺序被处理。如果“更新地址”和“删除账号”乱序到达,结果就是一团乱。有些系统能以一定代价在某个类别内保持顺序;通常更省事的办法是把工作设计成不依赖严格顺序。不管怎样,除非你专门做了安排,否则绝不要假定有顺序。

sentprocessedupdate address1stdelete account2nddelete account1stupdate address2nd · nonsense✗ updating an account that no longer existsnever assume order unless you arranged it
先发了“更新地址”,再发了“删除账号”。结果被反过来处理——于是你在更新一个已经不存在的账号。
ConceptMessage Ordering· ordering guarantees(顺序保证)
问题
并行的 consumer 不会按发送的顺序处理消息。
思路
不要依赖顺序——或在某个类别内显式地安排顺序。
例子
“删除账号”先于“更新地址”被处理——结果一团乱。
代价
严格的顺序需要花功夫,还会妨碍并行。
关联
Competing Consumers 的代价;Eventual Consistency 的近邻。

Eventual Consistency——结果最终会一致

一则通知在办公室里传开:有那么一会儿,有些人知道、有些人不知道,直到消息传到每个人,大家又一致了。

因为反应是异步发生的,系统是最终一致的(eventually consistent):event 刚发生后,不同部分可能短暂地不一致——订单已存在,但分析系统还没把它统计进去。它很快就会追上,但“到处都瞬间一致”这件事没有了。你要围绕这个间隙来设计,给用户展示合理的中间状态,而不是假定所有部分在同一瞬间更新。

OrdersAnalyticsorder existsnot counted yetorder countedgap: they briefly disagreetimeeventcaught up
订单已经存在,但分析系统还没统计它。一个短暂的间隙——然后大家又一致了。
ConceptEventual Consistency· consistent in the end(最终一致)
问题
event 刚发生后,系统的各个部分会短暂地看到不同的东西。
思路
接受这个间隙:每个部分都会追上并达成一致——只是晚一点。
例子
订单已创建,但报表里还没有;片刻之后就有了。
代价
你得为用户设计中间状态(“处理中……”)。
关联
Asynchronous Processing 和 Event-driven Architecture 的结果。

Dead-Letter Queue——有毒的消息需要一个去处

一封没人能处理的信——字迹模糊、根本没法办——不能无休止地重试、堵住整条队伍。它会被放到一个特别的托盘里,等人来查看。

有些消息永远无法被成功处理——格式错误,或者对应的数据已经不存在。如果放任不管,这样的 poison message(毒消息)会被无休止地重试,并堵住 queue。标准的解决办法是 dead-letter queue(死信队列):失败几次之后,把消息挪到一个单独的 queue 里等待检查,这样它就不再毒害整个流程。要为会失败的消息做好打算,否则一条坏消息就会让它后面的所有消息都停滞。

QueueConsumerDoneok messagesDead-letter queuefor a humanretry ×3still failsthe poison message moves aside; the main queue keeps flowing
正常消息顺利通过。有毒的那条,尝试三次之后,被移入 dead-letter queue——等人来检查。
PatternDead-Letter Queue· DLQ
问题
poison message 每次都失败,被无休止地重试,并堵住 queue。
思路
失败几次之后,把消息挪到一个单独的 queue 里等待检查。
例子
一条关于已删除用户的消息,尝试三次后进入 DLQ。
代价
得有人盯着 DLQ,否则它会变成丢失工作的坟场。
关联

只有配合 Idempotency,重试才安全;让流程持续运转,从而保障 Resilience。

异步消息带来重复(用 idempotency 应对)、没有保证的顺序、最终一致,以及会失败的消息(用 dead-letter queue)。这四样都要提前规划。

§ 07

queue 是一件强力工具,不是默认选项。真正的功夫在于:解耦确实划算时才去用它,简单更值钱时就保留直接调用。

Queue vs Direct Call——等待本身是问题时,就用 queue

你领一个号,离开去等一项你稍后再来取的慢服务——但如果只是快速问一句是或不是,你就直接在柜台问,因为领号比等一下还要可笑。

当工作缓慢、有高峰或不紧急,当你想让调用方保持快速,或者当好几个服务都应该对同一件事做出反应时,就用 queue。当你需要立刻得到答复才能继续时——查价格、查权限——就保留直接调用,因为在那里,简单和即时的结果比解耦更值钱。选工具,要看等待是否真的是问题。

Need the answer to continue?Direct callprice lookup · permission checkQueueslow · spiky · non-urgent · fan-outyesnomatch the tool to whether waiting is the real problem
一个问题:你需要答案才能继续吗?需要——直接调用。不需要——queue。
PracticeQueue vs Direct Call· how to choose(如何选择)
问题
调用方需要答案才能继续吗?
Queue
工作缓慢、有高峰、不紧急,或者很多方都需要(fan-out)。
直接调用
你需要即时答复:查价格、查权限。
Queue 的代价
broker、重复、顺序、eventual consistency、更难追踪。
关联
在 Synchronous Call 与 Message Queue 之间做选择。

别条件反射般地用它

为了给隔壁桌的人递张字条,装上一整套邮件收发和分拣系统——这套机器的成本,已经超过了它要解决的问题。

queue 会带来实实在在的复杂度:要运行一个 broker,要处理重复和顺序,要围绕 eventual consistency 做设计,而且追踪一个现在要在消息之间跳转的请求也更困难。对于一个简单的系统、两部分之间只是一次快速调用,这些开销不值得。当某个具体的好处——韧性、规模、解耦——足以证明代价合理时,再加 queue,而不是因为事件驱动听起来很高级。

这些概念是相互关联的

一条传送带:从一个痛点到一剂解药——而这剂解药有它自己的好处,也有它自己的副作用。

整体画面如下。synchronous call 的痛点,由 message queue 来治,并由 broker 保管。command(给一个 consumer)和 event(给很多个,经由 pub/sub)从 queue 中流过。这换来了好处——韧性、削峰、扩展——也带来了难题:重复、顺序、eventual consistency 和 DLQ。

Synchronous callMessage queueBrokerCommandEventPub/Sub · fan-outPayoffsresilience · load leveling · scalingHard partsduplicates · order · consistency · DLQreplaced byruns itbuyscosts
本课概念之间的主要联系:从直接调用,到 queue、它的好处和它的代价。
添加 queue 之前
  • 等待是不是问题——这项工作是否足够缓慢、有高峰或不紧急,值得解耦?
  • command 还是 event——是一个 worker 做一项任务,还是许多服务对一个事实做出反应?
  • 工作是否幂等——既然消息可能到达两次,执行两次是否安全?
  • 它是否假定了顺序,我是安排好了顺序,还是在设计上消除了对顺序的依赖?
  • 系统能否容忍短暂的最终不一致?
  • 失败的消息去哪儿——有没有 dead-letter queue?
你用好 queue 的迹象
  • 你把缓慢、有高峰、不紧急的工作放进 queue,把直接调用留给即时答复。
  • 你的 consumer 是幂等的,所以重复消息不会造成伤害。
  • 除非明确安排过,否则你不假定有顺序。
  • 你为 eventual consistency 做设计,而不是期待瞬间一致。
  • 失败的消息落进 dead-letter queue,而不是堵住队伍。

速查表:全部 21 个概念

直接调用的问题

Synchronous Callrequest–response

调用并等待。简单,但在时间上把服务耦合在一起。

Failure Propagationcascading failure

一个服务出故障,会拖垮所有调用它的服务。

Background Workbackground jobs

用户不该等的工作:邮件、发票、照片。

queue

Message Queuequeue

丢下一条消息就继续走;接收方自己来取。

Temporal Decouplingbuffer in time

一个缓冲:双方不必在同一时刻运行。

Asynchronous Processingasync

不等结果;结果在之后发生。

角色

Producer / Consumerpublisher / worker

一边写消息,一边处理消息;它们只认识 queue。

Message Brokerbroker

可靠地存储和投递消息的软件:RabbitMQ、Kafka。

Competing Consumersworker pool

一个 queue 上的多个 worker 分担负载。

command 与 event

Command做这件事

“做这件事”——恰好一个 consumer 去做。

Event这件事发生了

“这件事发生了”——任何人都可以反应,也可以没人反应。

Publish/Subscribepub/sub, topic, fan-out

一个 event 进入 topic——每个订阅者都拿到自己的副本。

Event-driven ArchitectureEDA

功能作为新的监听者加入,而不是去修改发布者。

好处

Resiliencefault tolerance

consumer 崩溃只是延迟,不是丢失工作。

Queue-Based Load Levelingload leveling, backpressure

高峰填满 queue;consumer 平稳地把它排空。

Independent Scalingindependent evolution

每一边各自扩展、各自变化。

难题

Idempotencyat-least-once

重复会发生——让它们变得无害。

Message Orderingordering guarantees

除非你做安排,否则顺序没有保证。

Eventual Consistencyconsistent in the end

系统的各部分短暂地不一致,然后追上。

Dead-Letter QueueDLQ

一个专门存放反复失败的消息的 queue。

选择

Queue vs Direct Call如何选择

需要答案才能继续——直接调用;不需要——queue。

queue 是深思熟虑的选择:当在时间上解耦能换来韧性、规模或 fan-out 时再用它——而当即时答复比这一切都重要时,就保留直接调用。

速成课结束 · 7 章 · 21 个概念

接下来是实践:挑一件你的应用目前在请求里同步做的慢事——发邮件、生成报告——把它挪到 queue 后面,让用户得到即时响应,由 worker 在之后做慢的那部分。然后让这个 worker 可以安全地执行两次。当你亲眼看到响应变快而工作照样完成时,这个模式就豁然开朗了。但请把一个想法放在最前面:转变在于从调用并等待,变成留个消息然后继续。在时间上解耦工作,你的系统会变得更快、更结实、更容易成长——代价是要用事件而不是命令来思考。