消息队列架构设计要点
你知道为什么 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/s | SSD 接近内存速度 |
| 顺序读磁盘 | 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 风暴?
面试追问
面试官可能会问:
- 「Kafka 的高可用是怎么保证的?」—— 副本 + ISR + Controller 选举
- 「为什么 Kafka 用顺序写就能这么快?」—— 对比随机写,解释 Page Cache 和零拷贝
- 「分区数和消费者数的关系是什么?」—— 超过消费者数的分区会空闲
- 「Rebalance 是什么?什么时候会触发?」—— 消费者加入/离开/心跳超时
理解这些设计原理,比背诵配置参数重要得多。因为参数会忘,原理不会。
