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

7.3 KiB
Raw Blame History

tags, create time
tags create time
MQ
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),不是每条消息都建索引,而是每隔一定字节建一条。查找时先在索引中二分查找,再在文件中顺序扫描。

// 简化版 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 的核心流程:

  1. 序列化:将业务对象转为字节数组。
  2. 分区路由:根据 Key 的哈希值选择目标 Partition。
  3. 批量发送:攒一批消息一起发送,减少网络往返。
  4. 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

关联笔记