7.3 KiB
7.3 KiB
tags, create time
| tags | 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?三个原因:
- 顺序写磁盘比随机写快 1000 倍:即使是 SSD,顺序写的吞吐也远高于随机写。
- 天然支持发布订阅:Consumer 只需要记住读到哪个 Offset,每次从 Offset 位置顺序读即可。
- 实现简单:不需要复杂的 B+Tree 索引,追加写 + 文件分段就够了。
分段存储(Segment):单个日志文件会无限增长,需要按大小或时间分段。每个 Segment 对应一个数据文件和一个索引文件。
索引文件:存储消息 Offset 到文件物理位置的映射。索引采用稀疏索引(Sparse Index),不是每条消息都建索引,而是每隔一定字节建一条。查找时先在索引中二分查找,再在文件中顺序扫描。
// 简化版 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 模式是最佳选择:
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]
// 简单的协议编解码
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 的核心流程:
- 序列化:将业务对象转为字节数组。
- 分区路由:根据 Key 的哈希值选择目标 Partition。
- 批量发送:攒一批消息一起发送,减少网络往返。
- ACK 等待:根据配置等待 Broker 确认(acks=0/1/all)。
// 批量发送
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 副本确认的消息才对外可见。
整体架构
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