从零手撸一个 MQ,逐步遭遇所有经典难题,最终理解 Kafka 背后每一个设计决策
用户点击"下单",系统需要同步完成:扣库存 → 生成订单 → 发送短信 → 推送积分 → 更新用户行为日志 → 触发风控...
同步链路总耗时 = 每个步骤耗时相加。短信服务超时 2s?整个下单就要等 2s!任何一环故障,整条链路崩溃。
订单服务只负责核心步骤(扣库存+生成订单),其余非核心操作异步化,发一条消息就完事。
三大核心价值:① 解耦(生产者不关心谁在消费)② 削峰填谷(MQ 作为缓冲)③ 异步(非核心逻辑不阻塞主流程)
| 问题 | 没有MQ | 有了MQ |
|---|---|---|
| 系统耦合 | A调B,B调C,牵一发动全身 | 只依赖MQ,无需知道下游 |
| 响应时间 | 所有步骤串行,越来越慢 | 核心链路极短,异步处理剩余 |
| 流量突刺 | 瞬时高峰直接打爆下游 | MQ 蓄洪,下游按自己节奏消费 |
| 故障隔离 | 下游故障影响上游 | 下游挂了,消息在 MQ 中等待重试 |
| 扩展性 | 新增下游要改上游代码 | 新服务订阅 Topic 即可,零侵入 |
最朴素的思路:一个 BlockingQueue 放消息,一个线程生产,一个线程消费。
这就是一个"内存 MQ"。但立刻暴露第一个问题:进程一重启,消息全丢了。
一个 BlockingQueue,生产者投递,消费者取走。简单但不可靠,重启即丢失。
消息写入磁盘文件,启动时重放。但随机写磁盘极慢,需要改成 顺序追加写(Append-Only Log)。
多个消费者同时消费,需要记录每个消费者消费到哪了(Offset),否则同一条消息会被消费多次。
单机存不下,要分布式。消息需要分散到多台机器(分区 Partition),但顺序问题随之而来。
某台机器宕机,那台机器上的消息怎么办?需要副本(Replica)机制,数据多份存储。
以上所有问题的解决方案组合起来,就是 Kafka 的架构设计。每一个决策都有它存在的理由。
为什么要用 Append-Only Log?磁盘随机写很慢,顺序追加写极快,接近内存速度。
Kafka 官方实测:顺序磁盘写比随机内存读还快。这就是为什么 Kafka 不用内存缓存,直接写磁盘也能跑出 100万+/s 的吞吐量。
网络抖动、Broker 压力大、短暂超时都会导致发送失败。如果不重试,消息就丢了。
| acks 配置 | 含义 | 丢消息风险 | 性能 |
|---|---|---|---|
acks=0 | 不等Broker确认,发了就算 | 最高 | 最快 |
acks=1 | Leader 写入就确认 | 中等(Leader宕机会丢) | 中 |
acks=all | 所有副本都写入才确认 | 最低 | 最慢 |
消息只写了内存(PageCache),还没 flush 到磁盘,服务器断电,消息丢失。
Kafka 的默认刷盘策略是异步刷盘(靠 OS 调度),追求极致安全要开同步刷盘,但代价是性能下降。实际生产更推荐靠副本机制(多副本)来保可靠性,而非强制同步刷盘。
消费者取到消息,先提交 Ack/Offset,再处理业务逻辑。处理到一半崩溃了 → 消息已确认但业务没完成 → 消息丢失。
先处理后 Ack 可能导致重复消费(处理成功但 Ack 前崩溃)。这是一个经典的"消息丢失 vs 重复消费"的权衡,大多数系统选择容忍重复、保证不丢(At Least Once)。
看似简单,实则暗藏多个打乱顺序的节点:
线程A发消息1,线程B发消息2,但消息2可能先到 Broker(网络延迟不同)。
Kafka 默认按 Round Robin 分发消息,消息1在分区0,消息2在分区1,消费时无法保证跨分区顺序。
消费者取到消息后用线程池处理,消息1的处理可能比消息2更慢,导致消息2先完成。
消息1发送失败触发重试,消息2已经发出去了,消息1重试成功后排在了消息2后面。
Kafka 只保证同一个分区内的消息是有序的。跨分区不保证。
电商订单场景:同一个 orderId 的所有消息,永远路由到同一个分区 → 该订单的消息天然有序。
注意:如果某个 orderId 消息特别多(热点 Key),会导致该分区数据量远超其他分区,引发分区不均衡(数据倾斜)问题,需要特殊处理。
开启幂等性后,Kafka 给每条消息分配全局唯一 ProducerID + SequenceNumber,Broker 端去重并重排序,重试不再导致乱序。
| 语义 | 英文 | 含义 | 丢消息 | 重复消费 | 适用场景 |
|---|---|---|---|---|---|
| 至多一次 | At Most Once | 发完不管,Broker 不确认 | 可能 | 不会 | 日志、监控(允许丢) |
| 至少一次 | At Least Once | 失败重试,保证投递成功 | 不会 | 可能 | 大多数业务(推荐) |
| 恰好一次 | Exactly Once | 既不丢也不重 | 不会 | 不会 | 金融交易(代价高) |
以下是最常见的重复消费场景:
重启后从上次的 Offset 重新消费,之前已处理的消息被再次消费。
消费者 GC 暂停太久 → Kafka 认为它挂了 → 把分区分配给别人 → 同一批消息被两个消费者都消费了。
消息发送成功但网络超时没收到 ACK → 生产者以为失败 → 重发 → Broker 收到两条相同消息。
与其在 MQ 层面保证恰好一次(极难),不如让业务消费逻辑变成幂等的——多次执行和一次执行结果一样。
Kafka 0.11+ 引入事务 Producer,可以实现真正的 Exactly Once(跨分区原子写入)。
每个分区对应磁盘上的一个目录,目录下是若干 Segment(段)文件。Kafka 不在一个文件写到底,而是按大小/时间切割。
零拷贝(Zero-Copy)是 Kafka 消费吞吐高的关键。传统读文件:磁盘→内核缓存→用户态→Socket缓冲→网卡(4次拷贝)。零拷贝:磁盘→内核缓存→网卡(2次拷贝,Linux sendfile 系统调用)。
Kafka 不像传统 MQ 消费后就删消息,而是按策略定期清理。
| 策略 | 配置 | 说明 |
|---|---|---|
| 按时间保留 | log.retention.hours=168 |
默认保留7天,超期的 Segment 文件直接删除 |
| 按大小保留 | log.retention.bytes=1073741824 |
超过指定大小(如1GB)则删除最早的 Segment |
| 日志压缩 | cleanup.policy=compact |
只保留每个 Key 的最新消息,适合状态快照场景 |
分区数决定了并发消费上限。实践经验:
分区数设好之后不能减少,只能增加。增加分区后,同一个 Key 的消息可能路由到不同分区,破坏顺序性!生产环境要提前规划好。
同组内,一个分区只能被一个消费者消费(防止重复)
不同组之间互相独立,都能消费全量消息(广播模式)
消费者数 > 分区数时,多余的消费者空闲,浪费资源
当以下事件发生时,Kafka 会触发 Rebalance,重新分配分区给消费者:
Rebalance 期间,整个消费者组停止消费(Stop The World),直到重新分配完成。频繁 Rebalance 会导致消费延迟飙升!这是 Kafka 早期版本(<2.4)的重大性能痛点。
Kafka 的 Offset 存储经历了两个阶段:
| 时期 | 存储位置 | 问题 |
|---|---|---|
| Kafka < 0.9 | ZooKeeper(zk的 /consumers 节点) | ZK 不适合频繁写,高并发下 ZK 压力巨大 |
| Kafka >= 0.9 | 内部 Topic:__consumer_offsets | Kafka 自己存自己管,吞吐高,运维简单 |
ISR(In-Sync Replicas) = 与 Leader 保持同步的副本集合。
Kafka Controller(集群中选举出来的一个特殊 Broker)通过 ZooKeeper 或 KRaft 监听到 Leader 下线。
Controller 从当前 ISR 列表中选第一个(或按一定策略)作为新 Leader,保证新 Leader 的数据与旧 Leader 基本一致。
Controller 更新 ZooKeeper/KRaft 中的分区元数据,通知所有 Broker 和 Producer/Consumer 新的 Leader 位置。
Producer 和 Consumer 重新获取元数据,自动将请求路由到新 Leader,整个过程对业务透明。
Kafka 2.8+ 引入 KRaft 模式,Kafka 3.x 中 ZooKeeper 已被废弃。
| 维度 | Kafka | RabbitMQ | RocketMQ | Pulsar |
|---|---|---|---|---|
| 诞生背景 | LinkedIn 日志收集 | 金融系统消息 | 阿里电商 | Yahoo 云原生 |
| 吞吐量 | 极高 100万+/s | 中等 万级/s | 高 十万级/s | 极高 |
| 延迟 | 毫秒级(非最低) | 微秒级(最低) | 毫秒级 | 毫秒级 |
| 消息模型 | Pull(消费者主动拉) | Push(推送给消费者) | Push + Pull | Push + Pull |
| 消息顺序 | 分区内有序 | 队列内有序 | 分区内有序(支持全局) | 分区内有序 |
| 延迟消息 | 不原生支持 | 插件支持 | 原生支持 | 原生支持 |
| 事务消息 | 支持(0.11+) | 不支持 | 支持(强事务) | 支持 |
| 死信队列 | 需手动实现 | 原生支持 | 原生支持 | 原生支持 |
| 消息回溯 | 支持(按时间/Offset) | 消费即删除 | 支持 | 支持 |
| 存储 | 磁盘持久化 | 内存为主(可持久化) | 磁盘持久化 | BookKeeper(计算存储分离) |
| 生态 | 极丰富(大数据) | 丰富(企业集成) | 丰富(阿里系) | 新兴,快速增长 |
| 适用场景 | 日志、大数据流、事件溯源 | 任务队列、RPC、复杂路由 | 电商、金融事务消息 | 云原生、多租户 |
是 → 选 Kafka。与 Flink/Spark/Hadoop 生态无缝集成,吞吐量无敌。
是 → 选 RabbitMQ。Exchange + Binding 路由灵活,AMQP 协议标准化。
是 → 选 RocketMQ。阿里双十一实战打磨,电商场景完美契合。
是 → 考虑 Pulsar。计算与存储完全分离,Kubernetes 友好。