vault backup: 2026-05-24 20:51:06
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
---
|
||||
tags: [MQ, RabbitMQ, 存储引擎, Erlang]
|
||||
create time: 2026-05-24 19:52
|
||||
---
|
||||
|
||||
# RabbitMQ 消息存储
|
||||
|
||||
## 概述
|
||||
|
||||
RabbitMQ 基于 Erlang/OTP 平台构建,消息存储依赖 Erlang 进程模型和 Mnesia 分布式数据库。本文深入分析 RabbitMQ 的消息持久化机制、队列类型演进(经典队列、仲裁队列、流式队列)、内存管理策略,以及消息从写入到删除的完整生命周期。
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. 存储架构:Erlang 进程 + Mnesia
|
||||
|
||||
RabbitMQ 的每个 Queue 本质上是一个 Erlang 进程(gen_server),拥有独立的 mailbox 和状态。这种设计天然隔离了不同队列的故障,但也意味着每个队列的资源消耗(内存、调度时间)受到 Erlang 虚拟机(BEAM)的约束。
|
||||
|
||||
持久化元数据(Exchange 定义、Binding 关系、用户权限等)存储在 **Mnesia** 分布式数据库中。Mnesia 是 Erlang 原生的数据库,支持事务和分布式复制,但它的设计目标是存储小量元数据,而非大规模消息数据。实际的消息体存储由各队列进程自行管理。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Producer["Producer"] -->|"AMQP 发布"| Exchange["Exchange"]
|
||||
Exchange -->|"路由规则"| Binding["Binding"]
|
||||
Binding --> Queue1["Queue 进程 1"]
|
||||
Binding --> Queue2["Queue 进程 2"]
|
||||
Queue1 -->|"持久化写入"| MsgStore["消息存储"]
|
||||
Queue1 -->|"内存缓存"| Memory["进程内存"]
|
||||
MsgStore --> Disk["磁盘文件"]
|
||||
Queue1 --> Consumer["Consumer"]
|
||||
|
||||
style Exchange fill:#4A90D9,color:#fff
|
||||
style Queue1 fill:#F5A623,color:#fff
|
||||
style Queue2 fill:#F5A623,color:#fff
|
||||
style MsgStore fill:#6EC1E0,color:#fff
|
||||
```
|
||||
|
||||
### 2. 消息持久化机制
|
||||
|
||||
RabbitMQ 中消息要真正持久化,需要同时满足两个条件:
|
||||
|
||||
- **Queue 声明为 durable**:队列的元数据在 Broker 重启后保留。
|
||||
- **Message 的 delivery mode 设为 2(persistent)**:消息体写入磁盘。
|
||||
|
||||
> [!question] durable 和 persistent 有什么区别?
|
||||
>
|
||||
> `durable` 是队列的属性,控制的是"队列本身在重启后是否还存在";`persistent` 是消息的属性,控制的是"这条消息是否需要写入磁盘"。两者必须同时设置才能实现真正的持久化。如果只有 durable 没有 persistent,队列重启后还在,但里面的消息会丢失;反过来则队列重启后直接消失,消息也就无从谈起了。
|
||||
|
||||
消息写入磁盘的时机并非立即的。RabbitMQ 使用一种"惰性"写入策略:消息先写入内存,当满足一定条件(如内存压力达到阈值、或显式 flush)时才批量刷入磁盘。这意味着在极端情况下(如 Broker 突然崩溃),少量消息可能丢失。
|
||||
|
||||
### 3. Queue 类型演进
|
||||
|
||||
RabbitMQ 3.8+ 引入了三种队列类型,各自面向不同的可靠性与性能需求:
|
||||
|
||||
**经典队列(Classic Queue)**是 RabbitMQ 最早的队列实现。消息存储在 Erlang Mnesia 表中(实际上从 3.x 开始使用自定义的文件存储引擎),支持可选持久化。单 Master 架构,不支持复制。在消息堆积时性能急剧下降,因为队列进程需要维护一个按 offset 索引的磁盘文件结构,大量消息会导致 GC 压力和 I/O 放大。
|
||||
|
||||
**仲裁队列(Quorum Queue)**基于 Raft 共识协议,每个队列在多个节点上维护副本(奇数个,默认 3 个)。消息写入需要多数节点确认,天然保证了数据安全。底层使用 WAL(Write-Ahead Log)顺序写盘,性能在大量消息堆积时保持稳定。
|
||||
|
||||
**流式队列(Stream)**是 3.9 引入的新类型,借鉴了 Kafka 的设计思想。消息以 append-only log 形式存储,支持多消费者独立读取(非竞争消费),消息不会被消费后删除,支持回溯重放。适合事件驱动、审计日志等场景。
|
||||
|
||||
| 特性 | Classic Queue | Quorum Queue | Stream |
|
||||
|------|:---:|:---:|:---:|
|
||||
| 复制 | 不支持 | Raft 多副本 | 可选 |
|
||||
| 持久化 | 可选 | 必须 | 必须 |
|
||||
| 消费后删除 | 是 | 是 | 否 |
|
||||
| 消息回溯 | 不支持 | 不支持 | 支持 |
|
||||
| 适合场景 | 简单队列 | 高可靠 | 事件流 |
|
||||
|
||||
> [!question] 经典队列在大量消息堆积时性能为什么会急剧下降?
|
||||
>
|
||||
> 经典队列的存储引擎在消息堆积时面临两个瓶颈:一是 Erlang 进程的 mailbox 膨胀导致调度延迟增大,GC 暂停时间变长;二是磁盘存储使用了按消息 ID 索引的文件结构,大量随机 I/O 使得写入性能断崖式下跌。仲裁队列通过 WAL 顺序写解决了 I/O 问题,通过 Raft 副本机制分散了单节点压力,从而在堆积场景下保持稳定。
|
||||
|
||||
### 4. Lazy Queue:磁盘优先策略
|
||||
|
||||
Lazy Queue 是经典队列的一种特殊模式,将消息尽可能早地写入磁盘,只在消费者需要时才加载到内存。适合消息堆积量大但消费速度跟不上的场景。
|
||||
|
||||
配置方式:声明队列时设置 `x-queue-mode: lazy`。在 Quorum Queue 中,由于 WAL 本身就是顺序写磁盘的,不需要额外的 Lazy 模式。
|
||||
|
||||
### 5. 内存管理:告警、流控与信用机制
|
||||
|
||||
RabbitMQ 的内存管理是一个三层防护体系:
|
||||
|
||||
**内存告警(Memory Alarm)**:当节点内存使用超过阈值(默认物理内存的 40%),Broker 触发内存告警,阻塞所有生产者的消息发布,直到内存回落到安全水位。
|
||||
|
||||
**流控(Flow Control)**:当某个 Erlang 进程(如队列进程)的消息积压超过一定阈值,会主动向 TCP 连接进程发送 `pause` 信号,让生产者的 TCP 接收窗口降为 0,从而在协议层面实现背压。
|
||||
|
||||
**信用机制(Credit Flow)**:这是 Erlang 进程间的流控机制。每个进程维护一个 credit 计数器,消息传递消耗 credit,消费端处理完毕后归还 credit。当 credit 归零时,发送端阻塞,防止过快地往下游进程灌消息。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Producer["Producer"] -->|"发送消息"| Conn["连接进程"]
|
||||
Conn -->|"credit 检查"| Queue["队列进程"]
|
||||
Queue -->|"内存检测"| MemCheck{"内存超阈值?"}
|
||||
MemCheck -->|"是"| Alarm["触发内存告警"]
|
||||
Alarm -->|"阻塞生产"| Producer
|
||||
MemCheck -->|"否"| Process["正常处理"]
|
||||
Process --> Consumer["Consumer"]
|
||||
|
||||
style Alarm fill:#D0021B,color:#fff
|
||||
style Queue fill:#F5A623,color:#fff
|
||||
```
|
||||
|
||||
### 6. 消息的生命周期:写入与删除
|
||||
|
||||
消息从进入 RabbitMQ 到最终消失,经历以下阶段:
|
||||
|
||||
1. **写入内存**:消息到达队列进程后,先存入进程内存(Erlang ETS 表或进程状态)。
|
||||
2. **写入磁盘**(如果 persistent):当内存压力达到一定阈值,或消息被标记为 persistent 时,消息体被写入磁盘文件。
|
||||
3. **投递消费者**:消息从内存中读取并发送给消费者。
|
||||
4. **等待 ACK**:消息在未收到 ACK 前不会被删除。
|
||||
5. **收到 ACK 后删除**:消息从内存和磁盘中移除。对于持久化消息,磁盘文件中的标记被清除,空间在文件 compaction 时回收。
|
||||
|
||||
### 7. Go 代码示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
conn, _ := amqp.Dial("amqp://guest:guest@localhost:5672/")
|
||||
defer conn.Close()
|
||||
|
||||
ch, _ := conn.Channel()
|
||||
defer ch.Close()
|
||||
|
||||
// 声明持久化队列:durable = true, autoDelete = false, exclusive = false
|
||||
ch.QueueDeclare("order_events", true, false, false, false, amqp.Table{
|
||||
"x-queue-type": "quorum", // 使用仲裁队列,替代默认的经典队列
|
||||
})
|
||||
|
||||
// 发布持久化消息:DeliveryMode = Persistent
|
||||
ch.Publish("", "order_events", false, false, amqp.Publishing{
|
||||
ContentType: "application/json",
|
||||
DeliveryMode: amqp.Persistent, // 消息持久化
|
||||
Body: []byte(`{"order_id":"12345","status":"created"}`),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
上面的代码展示了两个关键点:一是 `QueueDeclare` 设置 `durable: true` 并指定 `x-queue-type: quorum` 使用仲裁队列;二是 `Publishing` 设置 `DeliveryMode: amqp.Persistent` 确保消息持久化。两者缺一不可。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[07-主流MQ对比/23-RabbitMQ|RabbitMQ]]
|
||||
- [[04-存储引擎/8-MQ-存储引擎设计|MQ 存储引擎设计]]
|
||||
- [[05-可靠性保障/12-MQ-消息确认与持久化|MQ 消息确认与持久化]]
|
||||
- [[03-协议与标准/5-AMQP-协议|AMQP 协议]]
|
||||
Reference in New Issue
Block a user