Skip to content

消息队列架构设计要点

你知道为什么 Kafka 能支持每秒百万级消息写入吗?

不是因为它用了什么黑科技,而是架构设计上的克制与权衡

今天我们不聊具体的 MQ 实现,来聊聊消息队列架构设计中,那些决定「能用」还是「好用」的关键设计。


一、高可用设计:让系统「不挂」

单点故障的代价

想象一个没有副本的 MQ:

Producer ──发送──► Broker(单点)───消费──► Consumer

如果 Broker 挂了:
• 生产者发不出消息
• 消费者收不到消息
• 整个系统瘫痪

单点故障 = 系统不可用。这对于核心链路来说,是不能接受的。

多副本 + 主从切换

主流消息队列都采用副本机制来保证高可用:

┌─────────────────────────────────────────────────────────────┐
│                    Topic: order-topic                        │
├─────────────────────────────────────────────────────────────┤
│  Partition 0                                               │
│  ┌───────────┐    ┌───────────┐    ┌───────────┐          │
│  │ Broker 1  │    │ Broker 2  │    │ Broker 3  │          │
│  │  Leader   │◄──►│ Follower  │◄──►│ Follower  │          │
│  │  (读写)    │    │  (热备)    │    │  (热备)    │          │
│  └───────────┘    └───────────┘    └───────────┘          │
│                                                             │
│  写入:只写入 Leader                                          │
│  读取:可以从 Leader 或 Follower 读取(Kafka 支持)             │
└─────────────────────────────────────────────────────────────┘

Leader 选举机制

当 Leader 挂掉时,需要从 Follower 中选出一个新的 Leader:

选举方式实现特点
ZK 帮助选举Kafka Controller所有分区 Leader 选举由 Controller 统一处理
多数派选举Raft 协议需要 N/2+1 个节点同意,RocketMQ DLedger
无选举协商确定固定 ID 最小的是 Leader(不推荐,故障转移慢)

ISR:同步副本集合

不是所有 Follower 都有资格当选 Leader。Kafka 引入 ISR(In-Sync Replicas)概念:

java
// ISR 包含哪些副本?
// 满足以下条件的 Follower 才会进入 ISR:
// 1. 与 Leader 的数据同步延迟 < replica.lag.time.max.ms
// 2. 最近一次 fetch 请求在 10 秒内(默认配置)
// 3. 仍在存活

// 写入条件 acks=all 的含义:
// 必须等 ISR 中所有副本都确认,才算写入成功
producer.send(topic, message, acks: "all");

脑裂问题与过半机制

分布式系统中,网络分区可能导致「脑裂」——两个节点都认为自己是 Leader。

正常情况:
Broker A ──网络正常──► Broker B ──网络正常──► Broker C

网络分区:
Broker A ──与──► Broker B(断开)
Broker B ──与──► Broker C(断开)
Broker A 与 Broker C 都能通信(正常)

可能出现的脑裂:
A 以为自己是 Leader
C 以为自己是 Leader
→ 两个 Leader 同时服务 → 数据不一致!

解决方案:过半机制

3 节点集群:写入需要 2 个节点确认(过半)
5 节点集群:写入需要 3 个节点确认(过半)

这样即使出现网络分区,也只有一个分区能拿到过半票数
→ 只有一个能成为 Leader → 不会脑裂

二、高性能设计:让系统「飞快」

顺序写盘:极致的磁盘利用

你可能觉得「磁盘」一定很慢。但顺序写入和随机写入,差距是数量级的:

操作类型速度说明
顺序写磁盘500 MB/sSSD 接近内存速度
顺序读磁盘400 MB/s批量读取
随机写磁盘0.1 MB/s寻道时间成为瓶颈
顺序写内存10 GB/s理想情况

Kafka 正是利用了顺序写的特性:

传统数据库(随机写):
写入位置1 ──► 写入位置2 ──► 写入位置3 ──► 写入位置4
    │             │             │             │
    ▼             ▼             ▼             ▼
   磁盘          磁盘          磁盘          磁盘
  (寻道)        (寻道)        (寻道)        (寻道)

Kafka(顺序写):
写入 ──► 写入 ──► 写入 ──► 写入 ──► 追加到文件末尾


           磁盘(一直是顺序追加)

Page Cache:内存的神助攻

操作系统会把磁盘内容缓存到内存中,这就是 Page Cache:

应用写入 ──► Page Cache(内存) ──► 异步刷盘 ──► 磁盘

读取时:
• 数据在 Page Cache → 直接返回(内存速度)
• 数据不在 Page Cache → 读取磁盘并缓存

Kafka 利用这一点:
• 生产者写入 → 先到 Page Cache
• 消费者读取 → 先从 Page Cache 读
• 大部分情况下,磁盘 IO 根本不会发生

零拷贝:减少数据搬运

传统的数据发送过程需要 4 次拷贝:

1. 磁盘 ──读取──► 内核缓冲区
2. 内核缓冲区 ──拷贝──► 用户缓冲区
3. 用户缓冲区 ──拷贝──► Socket 缓冲区
4. Socket 缓冲区 ──发送──► 网卡

Kafka 使用零拷贝(Zero Copy)技术,只需要 2 次拷贝:

1. 磁盘 ──读取──► 内核缓冲区(Page Cache)
2. 内核缓冲区 ──直接发送──► 网卡(sendfile 系统调用)

这就是为什么 Kafka 能用普通 SATA 盘达到百万级 QPS。


三、可扩展性设计:让系统「能长」

水平扩展:加机器就能提升性能

Kafka 的分区机制天然支持水平扩展:

初始状态:3 台 Broker,6 个分区
┌─────────┐  ┌─────────┐  ┌─────────┐
│ Broker1 │  │ Broker2 │  │ Broker3 │
│ P0, P1  │  │ P2, P3  │  │ P4, P5  │
└─────────┘  └─────────┘  └─────────┘

扩展到 5 台 Broker,重新分配分区
┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐
│ Broker1 │  │ Broker2 │  │ Broker3 │  │ Broker4 │  │ Broker5 │
│ P0      │  │ P1      │  │ P2, P5  │  │ P3      │  │ P4      │
└─────────┘  └─────────┘  └─────────┘  └─────────┘  └─────────┘

分区重新分配过程中:

  • 生产者会自动感知新 Broker
  • 消费者会重新 Rebalance
  • 对业务无感知

分区数与吞吐量的关系

分区数 = 1:
• 单线程处理
• 吞吐量受限

分区数 = N:
• N 个并行处理
• 理论上吞吐量提升 N 倍
• 实际受限于网络、磁盘 IO

瓶颈分析:
• 1 Gbps 网卡 → 理论上限约 700 MB/s
• 3 台 Broker 每台 6 个分区 → 每台 233 MB/s

所以:
• 分区数不是越多越好
• 超过一定数量后,开销反而变大

分区分配策略

当消费者加入或离开时,需要重新分配分区:

java
// Kafka 的分区分配策略

// 1. Range(默认):按 Topic 分,每个消费者分配连续分区
// Topic A 有 10 个分区,3 个消费者
// C0: P0-P3, C1: P4-P6, C2: P7-P9
// 问题:可能导致不均衡

// 2. RoundRobin:跨 Topic 轮询分配
// 所有 Topic 的分区放在一起,轮询分配
// 更均衡,但同一 Topic 的分区可能被不同消费者处理

// 3. StickyAssignor:保持现有分配的前提下尽量均衡
// 尽量减少 Rebalance 时的分区变动

四、架构设计检查清单

设计一个消息队列架构时,需要考虑这些问题:

可用性

  • [ ] Broker 挂了怎么办?有没有副本机制?
  • [ ] Leader 挂了怎么选举?选举期间服务是否可用?
  • [ ] 网络分区了会不会脑裂?
  • [ ] 数据会不会丢?

性能

  • [ ] 消息写入是顺序写还是随机写?
  • [ ] 有没有利用 Page Cache?
  • [ ] 数据传输有没有用零拷贝?
  • [ ] 分区数是否合理?

扩展性

  • [ ] 分区数可以动态增加吗?
  • [ ] Broker 可以动态扩容吗?
  • [ ] 消费者增加后如何 Rebalance?
  • [ ] 如何避免 Rebalance 风暴?

面试追问

面试官可能会问:

  1. 「Kafka 的高可用是怎么保证的?」—— 副本 + ISR + Controller 选举
  2. 「为什么 Kafka 用顺序写就能这么快?」—— 对比随机写,解释 Page Cache 和零拷贝
  3. 「分区数和消费者数的关系是什么?」—— 超过消费者数的分区会空闲
  4. 「Rebalance 是什么?什么时候会触发?」—— 消费者加入/离开/心跳超时

理解这些设计原理,比背诵配置参数重要得多。因为参数会忘,原理不会

基于 VitePress 构建