--- 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-客户端连接管理]]