KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
05 · 集群与队列类型:classic、quorum 与 Raft 语义 — keel 龙骨
「节点挂了一台,队列会怎样」这个问题没有统一答案,答案取决于队列类型。classic 队列(非复制)整个只活在一个节点上,那个节点挂了队列就不可用;quorum 队列用 Raft 把消息复制到多个节点,少数节点挂掉还能继续。这一章把两者的差异对着 listqueues 的 type 字段和 quorumstatus 的 Raft 字段讲清楚,并且明确标出哪些结论本机单节点跑不出来、只能引用官方规定。
「节点挂了一台,队列会怎样」这个问题没有统一答案,答案取决于队列类型。classic 队列(非复制)整个只活在一个节点上,那个节点挂了队列就不可用;quorum 队列用 Raft 把消息复制到多个节点,少数节点挂掉还能继续。这一章把两者的差异对着
list_queues的type字段和quorum_status的 Raft 字段讲清楚,并且明确标出哪些结论本机单节点跑不出来、只能引用官方规定。
一、现场:节点挂掉,队列跟着消失
一个 3 节点的 RabbitMQ 集群,某天 A 节点宕机。运维发现挂在 A 上的几个队列整个不可用了——list_queues 里查不到、发布报错、消费者掉线。而另外几个队列却毫发无损。差别就在队列类型:前者是 classic 非复制队列,后者是 quorum 队列。
这是初学者最常见的误判:以为「集群」天然意味着「每个队列都有多份」。实际上 RabbitMQ 里交换机和绑定是所有节点都有的,队列不是——classic 队列默认只存在于声明它的那个节点上。
二、集群与复制拓扑
flowchart TD
subgraph C["RabbitMQ cluster(3 节点,本机只能起 1 个)"]
direction LR
N1["rabbit@node1<br/>(本机唯一节点)"]
N2["rabbit@node2<br/>本机未起"]
N3["rabbit@node3<br/>本机未起"]
end
subgraph RQ["quorum 队列 lab.quorum.q(Raft 复制组)"]
L["leader(node1 上)"]
F1["follower(node2)"]
F2["follower(node3)"]
end
subgraph CQ["classic 非复制队列 lab.classic.q"]
ONLY["只存在于 node1<br/>node1 挂 = 队列不可用"]
end
N1 --> L
N2 --> F1
N3 --> F2
L -.->|"复制日志"| F1
L -.->|"复制日志"| F2
N1 --> ONLY
ONLY -.->|"节点故障无副本可切"| DEAD["队列不可用"]
L -.->|"少数派无法形成多数<br/>拒绝写入,避免脑裂"| MINOR["处于少数派的一侧停止接受写入"]
style C fill:#e3f2fd,color:#0d3b66
style N1 fill:#e3f2fd,color:#0d3b66
style N2 fill:#eef4fb,color:#4a6a8a
style N3 fill:#eef4fb,color:#4a6a8a
style RQ fill:#e8f5e9,color:#1b5e20
style L fill:#c8e6c9,color:#1b5e20
style F1 fill:#e8f5e9,color:#1b5e20
style F2 fill:#e8f5e9,color:#1b5e20
style CQ fill:#fff8e1,color:#8a4b00
style ONLY fill:#ffe0b2,color:#8a4b00
style DEAD fill:#ffebee,color:#b71c1c
style MINOR fill:#ffebee,color:#b71c1c
三、对着输出区分两种队列
同名两种队列各建一个,用 x-queue-type 声明 quorum 类型,投同样的消息后查类型字段(07-quorum-vs-classic.txt):
name type messages
lab.classic.q classic 10
lab.quorum.q quorum 10
type 这一列是唯一权威的判据——不是看名字,也不是看有没有配 policy。list_queues 里不显式列 type 时看不到这列,排查队列行为异常时第一时间应该加上它。
quorum 队列的 Raft 成员与状态,用 rabbitmq-queues quorum_status 看(本机只有 1 个节点):
Status of queue lab.quorum.q on node rabbit@node1 ...
Node Name Raft State Membership Last Log Index Commit Index Term Machine Version
rabbit@node1 leader voter 169 169 1 8
把这几个字段对照 Raft 理解:
Raft State=leader:这个节点是当前 quorum 的 leader。Raft 里所有写请求都先经过 leader。Membership=voter:这个节点有投票权。Raft 组的成员分 voter 和 learner,只有 voter 参与多数派计数。Last Log Index=169、Commit Index=169:日志已提交到第 169 条。两个值相等说明没有未提交的日志。Term=1:选举任期。每次选举成功任期加一;单节点一直是 1。
拿 classic 队列去问同一个命令,会明确报错:
Status of queue lab.classic.q on node rabbit@node1 ...
Error:
{:unsupported, :rabbit_classic_queue}
{:unsupported, :rabbit_classic_queue} 说明 classic 队列根本不在 Raft 的管辖范围内——它不是复制队列。
四、本机跑不出来的部分
必须说清楚:本机实验台只有 1 个节点,无法演示 leader 选举、多数派丢失、脑裂恢复。 上面 quorum_status 里 Membership=voter 只有一个成员、Term=1 从没变过,就是单节点的直接证据——Raft 组的规模是 1,谈不上「多数派」。以下几条来自 RabbitMQ 官方文档(Quorum Queues / Queues,检索于 2026-10-05)和 Raft 的通用定义,属于「官方规定 + 推理链」,不是本机实测:
- 多数派语义。quorum 的「多数」是
(N/2)+1。3 个节点可以容忍 1 个挂掉,2 个节点的组反而比 3 个更差(2 节点的多数派还是 2,挂 1 个就没了,所以官方建议奇数个节点、通常 3 或 5)。本机只有 1 个成员,等价于「挂了就全没」,所以看不到任何容错表现。 - 少数派拒绝写入。网络分区时,处于少数派的一侧无法形成多数派,会停止接受写入,从而避免两个分区各自接受写入造成数据分叉(脑裂)。这是 Raft 相对旧版 classic 镜像的设计目的:镜像队列在某些故障下会过早确认导致丢数据,官方文档明确指出 classic 镜像在这些场景下的语义难以保证,并在 RabbitMQ 4.0 中完全移除了 classic 镜像队列(
classic_queue_mirroring于 2021-08 标记弃用,4.0 移除,检索于 2026-10-05)。现在需要复制的队列类型只有 quorum queues 和 streams。 - 故障恢复。leader 挂掉后,剩余多数派会选新 leader;挂掉的节点回来后会补上缺失的日志。本机没有第二个节点,无法验证这一步。
五、classic 与 quorum 的取舍
| 维度 | classic 队列 | quorum 队列 |
|---|---|---|
| 复制 | 无(4.x 已移除镜像) | Raft 复制到多个节点 |
| 持久化 | 队列可非持久化;消息按 delivery_mode |
队列恒持久化,消息恒持久化 |
| 独占性 | 支持 exclusive | 不支持 exclusive |
| 消息 TTL | 支持 | 支持(3.10 起) |
| 消息优先级 | 支持 | 不支持 |
| 单节点吞吐 | 通常更高、延迟更低 | 每次写要过多数派,吞吐与延迟受复制影响 |
| 典型用途 | 单节点、可承受重建的队列 | 需要高可用与数据安全的队列 |
这张表里几条来自官方文档的硬约束值得单独点出:quorum 队列没有非持久化这一说,也没有 exclusive(这两点本机也能间接感受到——第 00 章里我们给 classic 队列加 exclusive=True 才声明成功,quorum 队列则根本不接受 exclusive)。因此第 02 章那套「非持久化消息省一次 fsync」的优化,在 quorum 队列上完全不存在。
取舍可以收成一句话:如果这个队列挂了可以接受「等节点恢复」或「从别处重建」,classic 更省资源;如果这个队列不能停、数据不能丢,用 quorum,并接受它更高的写入成本和更多限制。本机单节点环境两种都能声明、都能收发,但高可用这个卖点在本机验证不了。
六、常见误判
| 常见误解 | 对着哪条输出核对 | 结论 |
|---|---|---|
| 集群里每个队列都有副本 | quorum_status 只有 1 个成员;classic 队列 {:unsupported, :rabbit_classic_queue} |
只有 quorum/stream 是复制的;classic 队列只在一个节点上 |
| 看队列名能猜到类型 | list_queues name type 里 type 才权威 |
名字带「quorum」不代表它是 quorum 队列,必须查 type |
| quorum 队列也能配成非持久化省 IO | quorum 恒持久化,本机声明也不接受 exclusive | quorum 的设计前提就是数据安全,不给「不落盘」这个选项 |
| 3 个节点比 2 个节点更容易脑裂 | Raft 的多数派是 (N/2)+1 |
3 节点容忍 1 个故障,2 节点挂 1 个就没多数派;奇数更合适 |
生产边界
- 教学替身 vs 真实依赖:本机是单节点,本章关于选举、多数派、脑裂、恢复的结论全部来自官方文档而非实测。生产验证必须真的起一个 3 节点集群,并做节点 kill、网络分区(用
iptables或tc断连)的演练。 - 上线要盯的指标:
quorum_status的Raft State是否长期是 leader、Term是否频繁增长(频繁选举说明网络或节点不稳定)、Last Log Index与Commit Index的差值(差得大说明复制跟不上)。 - 失败策略:quorum 队列建议至少 3 个节点(
x-quorum-initial-group-size),跨机架/可用区部署副本;对延迟敏感、可重建的队列继续用 classic;不要在同一集群里把 key 队列都设成 quorum 而不留容量余量——复制会放大内存和磁盘占用。
动手
- 用
x-queue-type=quorum建lab.quorum.q,rabbitmqctl list_queues name type确认type=quorum。 - 对同一个队列跑
rabbitmq-queues quorum_status -p /lab lab.quorum.q,逐个字段解释Raft State/Membership/Term。 - 对
lab.classic.q跑同一个命令,记录报错文本。 - 尝试给 quorum 队列声明
exclusive=True,记录 broker 的拒绝信息,和 classic 队列做对比。 - (需要另起节点)起第二个节点组成 2 节点集群,把队列的 quorum 组成员设成 2,kill 一个节点,观察
quorum_status与可写性;再想清楚为什么 2 节点比 3 节点更不安全。
自测
- classic 非复制队列挂在 A 节点上,A 宕机后这个队列还能用吗?quorum 队列有什么不同?
quorum_status的Membership=voter和Raft State=leader分别说明什么?Term频繁变化意味着什么?- 为什么 2 节点的 quorum 组比 3 节点更不安全?多数派是怎么算的?
- RabbitMQ 4.0 移除了什么?现在需要复制的队列用哪两种类型?
- quorum 队列不能配非持久化、不能用 exclusive,这两条限制背后想保证的是什么?
↓ 下一步:06 章 · 流控与运维