Chapter 05

自测与辨析:把答案盖住

前四章给了模型、可靠性、内部机制、与 Kafka 的对标。这一章把它们搅在一起考——尤其最后的判别层,逼你在真实场景里选,而不是认。答案统一折叠在文末,先做完再展开。

三层梯度,约 17 题。概念层查词汇(对应 01),原理层查机制(对应 02、03),判别层查迁移——给场景、逼选型,跨越 01–04(这是本教程的 capstone)。

概念层 · 回忆 对应 01 · 5 题 原理层 · 理解 对应 02 · 03 · 7 题 判别层 · 迁移 综合 01–04 · 5 题 难度 ↑
图 5.1三层梯度,自下而上难度递增。注意:真正区分"懂了"和"读过"的是顶层判别题——它不问定义,只给场景逼你选 RabbitMQ 还是 Kafka、哪种 queue、哪种 exchange。下两层答得快不代表顶层答得对。

一概念层 · 对应 01

  1. 生产者 publish 一条消息时,需要指定队列名吗?它实际指定的是什么?(§1.2)
  2. 四种 exchange(direct/fanout/topic/headers)各自拿什么和 binding key 比?哪两种根本不看 routing key?(§1.3)
  3. topic 的 binding key *.error 能匹配 routing key auth.login.error 吗?要匹配它,binding key 该怎么写?(§1.3)
  4. 同一个 channel 能不能被多个线程同时收发?根因在协议的哪一层?(§1.1)
  5. 消息被 ack 之后,broker 对它做了什么?这与 Kafka 消费者提交 offset 的本质区别是什么?(§1.4)

二原理层 · 对应 02、03

  1. publisher confirms 与 consumer ack 各防住消息生命周期的哪一段丢失?少了其中一个会怎样?(§2.1、§2.2)
  2. durable、persistent、quorum 三者各保护什么?为什么"durable + persistent + 已确认"的消息在 classic 队列上仍可能丢?(§2.3)
  3. prefetch 设成无限大、设成 1,各会出什么问题?怎么估一个合理值?(§2.2)
  4. 一次 publish 在 AMQP 0-9-1 里由哪几种 frame 组成?channel 编号在哪里、起什么作用?(§3.1)
  5. "给 broker 加 CPU 核数能提升单个队列的吞吐"——对吗?根因是什么?(§3.2)
  6. 内存高水位告警触发时,谁被阻塞、谁不受影响?是阻塞还是丢弃消息?(§3.3)
  7. quorum 队列的 publisher confirm 在什么时刻返回?它靠什么机制取代了被移除的镜像队列?(§3.4)

三判别层 · 综合 01–04(capstone)

每题先写下你的选择和一句话依据,再展开答案。判别题没有"背得出",只有"想得通"。

  1. 事件分发 + 历史重跑:一份"用户注册"事件要让邮件、风控、数仓三个独立服务各消费一次,且数仓以后可能重跑全部历史。选 RabbitMQ 还是 Kafka?若坚持用 RabbitMQ,缺口在哪、用什么补?(综合 §1.3 路由 + §4.3 重放)
  2. 有序 + 高吞吐:订单状态机要求"同一订单的事件严格按序处理",同时整体吞吐要高。RabbitMQ 怎么做到?Kafka 怎么做到?各自的代价?(综合 §4.3 顺序 + §3.2 单队列瓶颈)
  3. 绝不丢:金融转账消息绝对不能丢,单节点宕机也不能丢。在 RabbitMQ 里你会怎么配(队列类型 + 发送端 + 消费端三处)?(综合 §2.3 + §3.4 quorum)
  4. RPC 低延迟:一个内部服务调用,请求-响应、要低延迟、量不大。RabbitMQ 还是 Kafka?为什么这种场景 RabbitMQ 反而延迟更低?(综合 §4.2 投递 + §4.4 延迟)
  5. 毒消息:某条消息每次消费都失败,导致 CPU 飙高、队列头部被堵、后面的消息也消费不动。怎么诊断、怎么修?(综合 §2.4 毒消息 + §1.4 生命周期)
亲手画一张图

合上教程,在纸上或 Excalidraw 里画出一条消息从 producer 到被删除的完整路径——只画 5 个元素:producer、exchange、binding、queue、consumer,外加 ack 箭头。画完回到 §1.4 和首页图 0.1 对照:你画的图里,"ack 之后消息被删除"这一步标出来了吗?exchange 和 queue 之间,你写上 binding 了吗?这两处是最容易在脑子里"想当然"、落到纸上才发现没想清的地方。

进阶挑战 · 刚好够不着

把整套教程压成一张选型决策表

不看 §4.5,自己列一张三列表:需求特征(如"复杂路由"/"重放"/"严格有序"/"超高吞吐"/"RPC"/"绝不丢")→ 选什么(RabbitMQ / Kafka / RabbitMQ Streams / quorum 队列)→ 一句话依据。列满 6 行。然后翻回 §4.5 的决策树对照,看你漏了哪条、判反了哪条。

提示(卡住再展开)

主线还是那句话:消息要不要留存可重放、路由要不要broker 端做、吞吐是不是极端高。这三个问题几乎能定位到所有选择。"绝不丢"是正交维度——它决定 queue 类型(quorum),不决定 RabbitMQ vs Kafka。

§全部答案(先做完再展开)

展开全部答案

概念层

  1. 不需要队列名。它指定的是 exchange 名 + routing key。消息进哪些队列由 binding 决定,生产者不感知队列——这是"发布/路由解耦"。
  2. direct 比"routing key 是否等于 binding key";topic 比"routing key 是否匹配 binding key 的通配模式"。fanout 和 headers 不看 routing key:fanout 广播给所有绑定队列,headers 改用消息头属性匹配。
  3. 不能。* 只匹配一个单词,auth.login.error 是三个单词。要匹配"任意前缀 + .error",用 #.error(# 匹配零或多个单词)。
  4. 不能共享。根因在帧层:每帧带 channel 编号、同一 channel 的帧严格有序串行处理;多线程并发写同一 channel 会让帧交错,broker 解析出错。一线程一 channel,连接可共享。
  5. broker 永久删除这条消息,不留记录。Kafka 提交 offset 只是移动读指针,消息仍在日志里、可被任意消费者按 offset 重读。RabbitMQ 没有 offset、没有重放(除非用 stream 队列)。

原理层

  1. publisher confirms 防"发送端→broker"这段:broker 确认已接管(已路由、持久化消息已落盘流程已启动)才回 confirm,否则发送方知道要重发。consumer ack 防"broker→消费端"这段:处理成功才 ack,broker 才删除。少了 confirms,消息没进 broker 就丢且发送方不知情;少了手动 ack(用了 auto-ack),消费者崩溃时消息已被删、直接丢失。
  2. durable 保护队列/exchange 定义在 broker 重启后还在(实体属性);persistent(delivery_mode=2)保护消息被写盘(消息属性);quorum 保护消息在节点宕机后还在(多副本)。classic 队列即便 durable+persistent,在发 confirm 之前不 fsync,崩溃落在写盘缓冲窗口内就丢——这正是 quorum(Raft 多数派提交后才 confirm)存在的理由。
  3. 无限大:一个消费者把整队列消息全抓到本地,内存爆掉、其它消费者闲着,崩溃时大批消息重投。设成 1:每处理一条要等一个完整往返,吞吐被 RTT 卡死(125ms RTT 下约 8 msg/s)。合理值 ≈ 往返时间 / 单条处理时间,常从 10–50 起调,消费者慢或多则调低。
  4. 一次 publish = 一个 method frame(Basic.Publish,RPC 意图)+ 一个 content-header frame(消息属性、body 大小)+ 一个或多个 content-body frame(消息体,按 frame_max 切分)。每个 frame 头部都带 2 字节 channel 编号,broker 靠它把交错的帧还原成各 channel 独立的会话——这就是多路复用的根。
  5. 不对。每个队列是一个 Erlang 进程,所有工作串行过它的信箱,被钉在约一个调度器/核上。单队列吞吐有天花板,加核数也抬不动。提升靠拆更多队列(consistent-hash exchange、分片),不是换更大的机器。
  6. 所有发布连接被阻塞(集群范围),消费连接不受影响。是阻塞(让发布方停下来),不是丢弃或换页——broker 宁可卡住生产者也不丢消息。一个发布者把内存顶到水位,会连累集群里所有发布者。
  7. quorum 队列的 confirm 在消息提交到多数派副本(N/2+1)之后才返回。每个 quorum 队列是一个独立的 Raft 组(一 leader 多 follower、复制 WAL),leader 挂了选新 leader、重新加入的节点从自己的日志位置续上。它用共识算法取代了镜像队列那套自研的、副本常不同步的链式复制——后者在分区时行为未定义、会真丢消息。

判别层

  1. Kafka 更顺手。三个独立消费者各读全量 + 数仓要重跑历史 = 重放与多消费者独立 offset,正是日志的主场。坚持 RabbitMQ 的话:经典/quorum 队列消息 ack 即删、无法重跑历史——缺口在重放。补法是改用 RabbitMQ Streams(非破坏性消费、按 offset 重读),但要接受它吞吐不及 Kafka,且没有 TTL/DLX/优先级那套队列语义。
  2. RabbitMQ:用 consistent-hash exchange 把"同一订单"路由到固定的一个队列,该队列配单活消费者(single active consumer)保证串行——代价是单订单串行、并行度受队列数限制(单队列吞吐天花板,§3.2)。Kafka:用订单 ID 作 partition key,同一订单进同一 partition、partition 内有序,并行度 = partition 数——代价是 partition 数定死了最大并行度、扩容要重分区。
  3. 三处一起配:① 队列类型用 quorum(Raft 多副本,多数派提交才确认);② 发送端开 publisher confirms、消息 persistent,confirm 没回来就重发(注意幂等);③ 消费端用手动 ack,处理成功再 ack。三者缺一:classic 队列会在崩溃窗口丢、无 confirms 发送段会丢、auto-ack 消费段会丢。
  4. RabbitMQ。请求-响应、低延迟、低量正是它的主场:broker 推送(push)省去消费者轮询的等待,低负载下 p99 可到亚毫秒。Kafka 是拉模型(pull)+ 为吞吐做的批量,单条消息要等 fetch/批处理,低量时延迟反而更高。Kafka 的强项是吞吐,不是单条延迟。RPC 还能用 RabbitMQ 的 reply-to + correlation-id 直接搭。
  5. 诊断:这是毒消息——某条消息每次消费失败、被 nack 后又 requeue 回队头,无限重投占满 CPU,并堵住队头让后面的消息也出不来。修复:给队列配 dead-letter exchange + 设重试上限(quorum 队列默认 delivery-limit=20),超限后消息进死信队列而不是继续 requeue;消费端对"必然失败"的消息用 basic.reject/nack(requeue=false) 直接打入死信,再离线排查。根因回到 §1.4:requeue 把消息送回队列、断连/ nack 触发重投。