Files
cs-note/hhs/MQ/12-架构与实战/50-MQ-设计与实现.md
T
2026-05-24 20:51:06 +08:00

238 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-客户端连接管理]]