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 理解:

拿 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 的通用定义,属于「官方规定 + 推理链」,不是本机实测:

五、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 个就没多数派;奇数更合适

生产边界

动手

  1. 用 x-queue-type=quorum 建 lab.quorum.q,rabbitmqctl list_queues name type 确认 type=quorum。
  2. 对同一个队列跑 rabbitmq-queues quorum_status -p /lab lab.quorum.q,逐个字段解释 Raft State / Membership / Term。
  3. 对 lab.classic.q 跑同一个命令,记录报错文本。
  4. 尝试给 quorum 队列声明 exclusive=True,记录 broker 的拒绝信息,和 classic 队列做对比。
  5. (需要另起节点)起第二个节点组成 2 节点集群,把队列的 quorum 组成员设成 2,kill 一个节点,观察 quorum_status 与可写性;再想清楚为什么 2 节点比 3 节点更不安全。

自测

  1. classic 非复制队列挂在 A 节点上,A 宕机后这个队列还能用吗?quorum 队列有什么不同?
  2. quorum_status 的 Membership=voter 和 Raft State=leader 分别说明什么?Term 频繁变化意味着什么?
  3. 为什么 2 节点的 quorum 组比 3 节点更不安全?多数派是怎么算的?
  4. RabbitMQ 4.0 移除了什么?现在需要复制的队列用哪两种类型?
  5. quorum 队列不能配非持久化、不能用 exclusive,这两条限制背后想保证的是什么?

↓ 下一步:06 章 · 流控与运维

进入 keel 阅读