Files
cs-note/hhs/MQ/12-架构与实战/50-MQ-设计与实现.md
T

238 lines
7.3 KiB
Markdown
Raw Normal View History

2026-05-24 20:51:06 +08:00
---
tags:
- MQ
create time: 2026-05-24 19:52
---
# MQ 设计与实现
## 概述
理解 MQ 的最好方式是自己设计一个。本文串联前面所有知识,从零设计一个简化版 MQ,涵盖存储层、网络层、生产者、消费者、高可用等核心模块,帮助读者建立对 MQ 内部机制的完整认知。
## 正文
### 设计目标
在动手之前,先明确设计目标。一个实用的 MQ 需要平衡四个指标:
| 指标 | 目标 | 说明 |
|------|------|------|
| 高吞吐 | 10 万+ msg/s | 单节点能力,通过分区横向扩展 |
| 低延迟 | p99 < 10ms | 端到端的生产-消费延迟 |
| 消息不丢失 | At-Least-Once | 通过副本复制和 ACK 机制保证 |
| 发布订阅 | 支持多消费组 | 每个消费组独立消费全量消息 |
> [!question]
> 如果让你从零设计一个 MQ,你会优先保证哪个指标:吞吐、延迟,还是可靠性?为什么?
### 存储层设计
存储是 MQ 的基石。现代 MQ 几乎都采用 **Append-Only Log** 作为核心存储结构——消息只能追加写入,不能修改和删除(删除通过过期清理实现)。
为什么用 Log?三个原因:
1. **顺序写磁盘比随机写快 1000 倍**:即使是 SSD,顺序写的吞吐也远高于随机写。
2. **天然支持发布订阅**:Consumer 只需要记住读到哪个 Offset,每次从 Offset 位置顺序读即可。
3. **实现简单**:不需要复杂的 B+Tree 索引,追加写 + 文件分段就够了。
**分段存储(Segment)**:单个日志文件会无限增长,需要按大小或时间分段。每个 Segment 对应一个数据文件和一个索引文件。
**索引文件**:存储消息 Offset 到文件物理位置的映射。索引采用稀疏索引(Sparse Index),不是每条消息都建索引,而是每隔一定字节建一条。查找时先在索引中二分查找,再在文件中顺序扫描。
```go
// 简化版 Log 存储
type Segment struct {
baseOffset uint64
dataFile *os.File
indexFile *os.File
currentSize int64
maxBytes int64
}
// 追加写入消息
func (s *Segment) Append(offset uint64, data []byte) error {
// 写数据文件:[length][data]
buf := make([]byte, 4+len(data))
binary.BigEndian.PutUint32(buf[:4], uint32(len(data)))
copy(buf[4:], data)
pos, _ := s.dataFile.Seek(0, io.SeekCurrent)
s.dataFile.Write(buf)
// 写索引:[offset][position](稀疏索引,每 4KB 写一条)
if s.currentSize%4096 == 0 {
idxBuf := make([]byte, 16)
binary.BigEndian.PutUint64(idxBuf[:8], offset)
binary.BigEndian.PutUint64(idxBuf[8:], uint64(pos))
s.indexFile.Write(idxBuf)
}
s.currentSize += int64(len(buf))
return nil
}
// 按 Offset 读取消息
func (s *Segment) Read(offset uint64) ([]byte, error) {
// 1. 在索引文件中二分查找
position := s.findIndexPosition(offset)
// 2. 在数据文件中从 position 开始顺序读
s.dataFile.Seek(int64(position), io.SeekStart)
// 3. 读取 length + data
lenBuf := make([]byte, 4)
s.dataFile.Read(lenBuf)
length := binary.BigEndian.Uint32(lenBuf)
data := make([]byte, length)
s.dataFile.Read(data)
return data, nil
}
```
### 网络层设计
MQ 的网络层需要处理大量并发连接,**Reactor 模式**是最佳选择:
```mermaid
graph TB
subgraph "Reactor 网络模型"
A["Acceptor 线程"] -->|"接受连接"| EP["EventPoller - epoll/kqueue"]
EP -->|"可读事件"| W1["Worker 线程 1"]
EP -->|"可读事件"| W2["Worker 线程 2"]
EP -->|"可读事件"| W3["Worker 线程 3"]
W1 -->|"解析协议"| R["请求路由器"]
W2 -->|"解析协议"| R
W3 -->|"解析协议"| R
R -->|"Produce请求"| PH["ProduceHandler"]
R -->|"Fetch请求"| FH["FetchHandler"]
end
```
**协议编解码**:MQ 需要自定义二进制协议。一个简单的协议格式:
```
[4字节长度] [2字节请求类型] [4字节CorrelationID] [变长Body]
```
```go
// 简单的协议编解码
type Request struct {
RequestType uint16
CorrelationID uint32
Body []byte
}
func DecodeRequest(conn net.Conn) (*Request, error) {
// 读取长度
lenBuf := make([]byte, 4)
io.ReadFull(conn, lenBuf)
length := binary.BigEndian.Uint32(lenBuf)
// 读取完整请求
payload := make([]byte, length)
io.ReadFull(conn, payload)
return &Request{
RequestType: binary.BigEndian.Uint16(payload[:2]),
CorrelationID: binary.BigEndian.Uint32(payload[2:6]),
Body: payload[6:],
}, nil
}
```
### 生产者设计
Producer 的核心流程:
1. **序列化**:将业务对象转为字节数组。
2. **分区路由**:根据 Key 的哈希值选择目标 Partition。
3. **批量发送**:攒一批消息一起发送,减少网络往返。
4. **ACK 等待**:根据配置等待 Broker 确认(acks=0/1/all)。
```go
// 批量发送
type Producer struct {
buffer map[string][]*Message // key: topic-partition
batchSize int
linger time.Duration
mu sync.Mutex
}
func (p *Producer) Send(msg *Message) {
p.mu.Lock()
partition := hashKey(msg.Key) % p.partitionCount
key := fmt.Sprintf("%s-%d", msg.Topic, partition)
p.buffer[key] = append(p.buffer[key], msg)
if len(p.buffer[key]) >= p.batchSize {
msgs := p.buffer[key]
p.buffer[key] = nil
p.mu.Unlock()
p.flush(msgs) // 攒够一批,发送
return
}
p.mu.Unlock()
}
```
### 消费者设计
消费者的核心是 **Pull 模式 + Offset 管理**:
- **Pull 模式**:消费者主动从 Broker 拉取消息,而非 Broker 推送。这样消费者可以按自己的速率消费,天然支持背压。
- **Offset 管理**:每个消费组在每个 Partition 上维护一个 Offset,记录消费到的位置。
- **Consumer Group 协调**:同一组内的多个消费者通过 Rebalance 机制分配 Partition。
### 高可用设计
单节点 MQ 不够可靠,需要副本复制:
- **Leader-Follower**:每个 Partition 有一个 Leader 和多个 Follower。Leader 处理读写,Follower 同步数据。
- **Leader 选举**:Leader 宕机后,从 ISR 中选出新 Leader。基于 Epoch(任期)避免脑裂。
- **数据一致性**:通过 HW(High Watermark)机制,只有被所有 ISR 副本确认的消息才对外可见。
### 整体架构
```mermaid
graph TB
subgraph "Producer 集群"
P1["Producer 1"]
P2["Producer 2"]
end
subgraph "Broker 集群"
subgraph "Broker 1"
L1["Partition 0 Leader"]
F1["Partition 1 Follower"]
end
subgraph "Broker 2"
L2["Partition 1 Leader"]
F2["Partition 0 Follower"]
end
subgraph "存储层"
S1["Segment + Index"]
S2["Segment + Index"]
end
end
subgraph "Consumer Group A"
C1["Consumer 1 - P0"]
C2["Consumer 2 - P1"]
end
subgraph "Consumer Group B"
C3["Consumer 3 - P0+P1"]
end
P1 --> L1
P2 --> L2
L1 -->|"同步复制"| F2
L2 -->|"同步复制"| F1
L1 --> S1
L2 --> S2
L1 --> C1
L2 --> C2
L1 --> C3
L2 --> C3
```
## 关联笔记
- [[44-MQ-高可用架构]]
- [[48-MQ-客户端-SDK-最佳实践]]
- [[49-MQ-客户端连接管理]]