238 lines
7.3 KiB
Markdown
238 lines
7.3 KiB
Markdown
|
|
---
|
|||
|
|
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-客户端连接管理]]
|