Kafka从消息队列到分布式日志与实时数据平台:原理、部署与高频问题排查
跟不少做后端的朋友聊过 Kafka,发现一个挺普遍的现象:大家一说 Kafka 就是“消息队列”,面试背消息队列,真到生产环境出了问题,又只会重启。其实 Kafka 能火这么多年,绝不只是因为它是个消息队列。它本质上是一个分布式日志系统,后来顺势演进成了实时数据平台。这个认知一旦扭转,很多之前想不明白的问题——比如为什么它吞吐量这么高、为什么消费过的数据还可以再读、为什么能当存储用——都会豁然开朗。
这篇文章我不打算给你列一堆官网概念,我想从一个从业者的角度,把 Kafka 从消息队列、分布式日志到实时数据平台这条演进路径串起来讲,顺便把安装部署、生产消费命令、重复消费、消息延迟高、OOM 这些网上高频问题一次性梳理清楚。内容适合刚入门 Kafka 的新手,也适合被线上问题折磨过、想系统补一遍原理的运维和开发同学。
1. Kafka 到底是什么:先摘掉“消息队列”这顶帽子
1.1 从 LinkedIn 的日志痛点说起
Kafka 是 LinkedIn 在 2011 年左右开源的项目,当时这家公司的规模已经很大,每天有大量行为日志、系统指标、业务事件需要从各种服务里收集起来再分发出去。用现在的话说,这就是一个典型的海量数据管道场景。
一开始他们也是用现成的消息队列,比如 ActiveMQ。但用着用着就发现问题了:吞吐上不去,消息堆积之后消费者跟不上,而且队列里消息被消费完就删,想重新拉一份历史数据做分析非常麻烦。于是几个工程师冒出一个想法:干脆不把它做成传统队列,而是做一个分布式的提交日志,就像数据库里的 binlog、MySQL 的 redo log 那样,所有数据往里追加写,谁想看数据就从某个位置自己读。
这个设计思路极其简单,却又极其反直觉。传统消息队列的核心是“投递”,消息发给消费者之后队列里就没有了;Kafka 的核心是“存储”,消息写进日志里就一直在那里,消费者爱读几遍读几遍,唯一要记录的是“我读到哪了”。就是这一层“日志”思维,让 Kafka 在后续十年里越走越宽。
1.2 用分布式日志的视角重新理解 Kafka
很多人学 Kafka 时被 Topic、Partition、Consumer Group 这些概念绕晕。但如果回到“分布式日志”这个本质,一切都好解释了。
日志在计算机里的语义是什么?是一条按时间顺序追加的记录序列。Kafka 把每个 Topic 看成一份日志,一份日志太大就切成多个 Partition,每个 Partition 内部仍然保持写入顺序。生产者把消息追加到 Partition 尾部,消费者从某个 offset(偏移量)往后读。这个过程和人们写文件、读文件几乎没有区别。
最有意思的是,Kafka 给每条日志设了一个保留期,默认是 7 天。也就是说,一条消息被消费者读走之后并不会被物理删除,它还会在磁盘上躺 7 天。这意味着你可以让一个新的消费者从头开始重新消费一遍这 7 天里的数据,也可以让多个消费者各自从不同的位置并行消费。传统消息队列做不到这一点,它们通常在消息被确认后就删除了。
所以我在跟人解释 Kafka 的时候经常说:你可以把它理解成一个跨系统的、分布式的 binlog。MySQL 的 binlog 能支撑主从复制,Kafka 则能把几十个系统里的数据变更全部汇集起来,再分发到几十个下游系统。
1.3 它和 RabbitMQ、RocketMQ 的根本区别
既然 Kafka 是日志,那它和我们常说的 RabbitMQ、RocketMQ 到底差在哪?直接看一张对比表:
| 维度 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 核心模型 | 分布式日志(append-only) | 队列/交换机路由 | 队列/主题模型,偏业务消息 |
| 消息是否删除 | 按保留期保存,可重放 | 消费确认后删除 | 默认保留,但面向业务存储 |
| 吞吐能力 | 非常高,百万级 TPS | 一般,几万级 | 高,十万级到百万级 |
| 消息顺序 | 分区内有序 | 单队列有序 | 队列内有序 |
| 典型场景 | 日志管道、事件流、数据同步 | 业务解耦、异步通知 | 业务消息、事务消息 |
| 消费方式 | Consumer Group 分段拉取 | push/pull 混合 | push/pull 混合 |
选型时千万不要只看“消息队列”四个字就拍板。如果业务里需要复杂路由、灵活确认、消息 TTL、死信队列,RabbitMQ 很合适;如果要做分布式事务消息、顺序消息且对延迟敏感,RocketMQ 很合适;但如果你要处理的是海量日志、用户行为埋点、系统间数据同步,或者你需要把一段时间的数据重放给多个下游,Kafka 基本是最优解。认清这点,面试时被问到“为什么选 Kafka 而不是 RabbitMQ”也能答到点子上。
2. 核心架构拆解:Topic、Partition、副本机制一次讲透
2.1 Producer 写入链路:从消息到分区的路由规则
先看数据怎么进 Kafka。生产者(Producer)发送消息时,会计算这条消息应该写到哪个分区的哪个副本上。分区路由规则很简单:
- 如果消息指定了 key,就对这个 key 做哈希,相同的 key 一定进同一个分区。这保证了同一用户、同一订单的所有事件在分区内有序。
- 如果没指定 key,轮询写各分区,实现负载均衡。
- 你也可以实现自定义分区器,完全控制路由逻辑。
真正影响写入性能的是三个参数:acks、batch.size、linger.ms。acks 表示需要多少个副本确认写入成功。设 0 不等待任何确认,吞吐最高但可能丢消息;设 1 是默认值,Leader 写成功就算成功,兼顾性能与可靠性;设 -1/all 要等所有 ISR 副本都写入成功,最可靠但延迟变高。
很多新手以为 Kafka 是每发一条消息就写一次磁盘,这是误区。Producer 端有一个缓冲区,消息会先在内存里攒一批,达到 batch.size(默认 16KB)或者等待 linger.ms(默认 0 毫秒)后一次性发出去。这就像寄一堆明信片时不会跑一趟邮局寄一张,而是攒够一摞再去,批量操作大幅减少了网络往返次数。所以你会发现,适当把 linger.ms 调到 5 到 10 毫秒,吞吐能涨不少,而延迟几乎没受什么影响。
2.2 Consumer 与 Consumer Group:并行消费背后的“坑”
消费端是 Kafka 架构里最容易踩坑的地方。Kafka 引入了消费者组(Consumer Group)这个概念来实现“队列”语义:同一个组内的消费者,被分配到的分区互不重叠;而不同组之间则相互独立,各自维护自己的 offset。
举个例子:一个 Topic 有 6 个分区,你开了 3 个消费者在同一个组里,每个消费者会分到 2 个分区,这样三条线程并行消费,效率比单线程高得多。但如果你开了 6 个以上的消费者,就会有人分不到分区,一直空闲;反过来,分区数太少、消费者太多时,某些消费者会分到多个分区,出现局部积压。
这里有一个核心规则:一个分区最多只能被同一个消费者组里的一个消费者消费。所以消费者并行度上限就是分区数。需要提高并发时,不是盲目加消费者,而是先把分区数规划够。
消费者拉取消息后,需要提交 offset 记录自己的消费进度。offset 存在 Kafka 内部一个名为 __consumer_offsets 的主题里。提交方式有自动提交和手动提交两种。自动提交默认开启,每 5 秒提交一次,但这是重复消费和消息丢失的根源之一——你正在处理一条消息还没处理完,offset 已经提交了,这时如果进程挂了,消息恢复后不会重新消费;反过来,如果处理完消息还没来得及提交就挂了,重启后会从上次提交的 offset 开始拉,又会重复消费一遍。线上场景要求高的话,一定要根据业务情况手动管理 offset。
2.3 Broker、ISR 与副本机制:高可用是怎么保证的
Kafka 的每个分区都有多个副本,分布在不同的 Broker 上。副本分为 Leader 和 Follower,生产者和消费者只跟 Leader 交互,Follower 负责从 Leader 同步数据,一旦 Leader 挂了,Follower 会竞选成为新 Leader。
这里有个关键集合叫 ISR(In-Sync Replicas),也就是“和 Leader 保持同步的副本列表”。只有 ISR 里的副本才有资格竞选 Leader。如果某个 Follower 落后 Leader 太多,或者心跳超时,它会被踢出 ISR。所以你会发现 ISR 是动态变化的,它是 Kafka 保证“不丢已提交消息”的基础。
与 ISR 相关的还有两个概念:HW(High Watermark)和 LEO(Log End Offset)。LEO 是日志最后一条消息的位置,HW 是消费者能看到的最新位置。只有被 ISR 中所有副本都写入的消息,才会推进 HW;消费者只能消费到 HW 之前的数据。这是为了确保你读到的东西不会因为 Leader 切换而丢失。
实操中最需要注意的参数是 min.insync.replicas。假设副本数设了 3,但 min.insync.replicas 是 1,那么理论上哪怕只有一个副本活着,也会继续接受写入,这就有丢数据的风险。生产环境我建议副本数至少 2 或 3,min.insync.replicas 设置为 2,再配合 acks=all,才能比较稳妥地保证“写入即持久”。
3. 为什么 Kafka 能扛住百万级 TPS:性能设计的底层逻辑
3.1 顺序写盘与页缓存:把磁盘用成内存
Kafka 最让人惊讶的就是吞吐,单机百万条消息很常见。它并不是用了什么黑科技,靠的是几个非常本质的操作系统原理。
第一个是顺序写。机械硬盘的随机写能力只有几百 IOPS,但顺序写能跑到每秒几百 MB;SSD 的顺序写同样远优于随机写。Kafka 把每个分区的数据变成一个只允许追加的日志文件,所有写入都是顺序追加,天然利用了磁盘最强的性能区间。
第二个是页缓存(Page Cache)。Kafka 写入数据时,数据其实是先写到操作系统的页缓存里,再由操作系统异步刷盘。读数据时,如果消费者读的是刚写入不久的消息,大概率直接命中页缓存,连磁盘都没碰。这就解释了为什么 Kafka 能在不太依赖 JVM 堆内存的情况下跑出高吞吐——它把缓存工作交给了操作系统。
很多人看到服务器内存占用高就紧张,以为 Kafka 内存泄漏了。其实 Kafka 的日志数据大量缓存在页缓存中,这是设计故意为之。只要堆内存没有频繁 Full GC,物理内存占用高往往说明缓存命中率高,反而是好事。
3.2 零拷贝与批量传输:减少内核态用户态切换
第二个关键设计是零拷贝。传统的文件读取到网络发送,需要把文件从磁盘读到内核缓冲区,再复制到用户态应用缓冲区,应用处理完再复制回内核态 Socket 缓冲区,最后网卡发送。来回倒腾,CPU 都在做数据搬运。
Kafka 利用操作系统的 sendfile 系统调用,支持数据直接从内核缓冲区发送到网卡,中间不经用户态。数据路径变短,CPU 开销大幅下降。这就是消费者拉取数据时,即使吞吐很大 CPU 占用也不高的原因之一。
配合零拷贝的还有批量传输。Producer 端批量发送,Consumer 端也是批量拉取,默认一次可以拉取最多 500 条或 50MB 数据。网络包变大,传输次数变少,吞吐自然上去了。
3.3 日志分段存储与稀疏索引:日常运维必须懂
Kafka 在磁盘上并不是把一个分区存成一个巨大的文件,而是切成很多段(Segment)。每个 Segment 包含一个 .log 数据文件和一个 .index 偏移索引文件,索引是稀疏的,每隔一定字节记录一条 offset 与实际物理位置的映射。
分段存储带来三个直接好处:第一,过期消息删除时直接删掉整个 Segment 文件,而不是在文件中间打洞,避免磁盘碎片;第二,消息查找时可以先用稀疏索引定位到某一段,再在段内二分查找,不必全文件扫描;第三,单个文件不会无限增大,方便 Kafka 做日志的管理和备份。
日常排查磁盘占用时,如果发现某个 Topic 占了很多空间,你可以在日志目录下看到大量类似 000000000000012345.log 的文件,文件名代表该 Segment 的起始 offset。想删除某段时间数据,不是手动 rm(绝对不要在生产环境手动删文件),而是通过调整 log.retention.hours 或 log.retention.bytes 参数触发 Kafka 内部的清理任务。
4. 从日志平台到实时数据中枢:Kafka 在大型系统中的位置
4.1 典型架构:ELK 链路怎么配合 Kafka
Kafka 在日志领域最经典的用法,就是配合 ELK(Elasticsearch、Logstash、Kibana)搭日志平台。很多人会问,Filebeat 直接往 Elasticsearch 写不就行了,为什么中间非要插一个 Kafka?
原因有三。第一是削峰填谷:业务高峰期日志量暴增,而 Elasticsearch 的写入能力有限,直接把 Logstash 或 ES 压死会导致数据丢失。Kafka 夹在中间,日志先堆在 Kafka 里,Logstash 按自己的能力慢慢消费,全链路不会因为流量尖峰而雪崩。第二是多方订阅:日志不仅给 ELK 看,可能还要给审计系统、指标监控系统、推荐系统各来一份。Kafka 允许不同消费者组各自消费同一份数据,这是直接写 ES 做不到的。第三是数据重放:排查问题时你可能会说“我想看看昨天某段时间的原始日志”,Kafka 的保留机制让这件事成为可能。
完整链路一般是这样:应用服务器的日志通过 Filebeat 采集,发到 Kafka,Logstash 从 Kafka 消费后做清洗、结构化、解析,再写入 Elasticsearch,最后由 Kibana 展示和检索。只要你理解了 Kafka 在里面的定位,整个架构图一下子就活了。
4.2 Kafka Connect 与 Kafka Streams:流处理能力补充
Kafka 能从一个日志管道变成实时数据平台,离不开它周边的生态组件。Kafka Connect 可以让你用配置文件就能把数据库、文件、消息中间件的数据接入 Kafka,或者从 Kafka 导出到下游系统,不需要写一堆重复的消费者代码。比如同步 MySQL binlog 到 Kafka,可以用 Debezium 这个 Connector 实现。
Kafka Streams 则是内置在客户端库里的流处理引擎。它允许你用普通的 Java 程序读取 Kafka 里的数据,做分组、聚合、窗口计算、多流 join,再写回 Kafka。相比 Flink、Spark Streaming 这种重量级框架,Kafka Streams 的优势是部署简单,不需要独立集群,你的应用本身就是处理引擎。
举一个实际案例:一个电商系统每天产生大量用户行为埋点数据,原始日志进入 Kafka;Kafka Streams 实时统计每个商品最近 5 分钟的点击量,写入一个实时指标 Topic;同一份原始数据又通过 Kafka Connect 同步到数仓做离线分析;同时 ELK 平台的 Logstash 还在消费它做日志检索。一份数据,多个场景复用,这就是“实时数据平台”的含义。
4.3 不同场景的配置权衡
Kafka 的默认配置能满足大多数场景,但生产环境需要根据自己的使用目的做调整。
| 场景 | 保留时间 | 副本数 | acks | 消费者提交方式 | 重点关注 |
|---|---|---|---|---|---|
| 日志管道 | 3~7 天 | 2 | 1 | 自动提交 | 吞吐优先,可接受少量重复 |
| 业务事件消息 | 24h~7 天 | 3 | all | 手动提交 | 不丢不重,端到端可靠 |
| 实时流计算 | 1~24h | 2 | 1 | 手动+幂等 | 延迟与吞吐平衡 |
| 数据库同步 | 7 天以上 | 3 | all | 精确一次 | 数据完整性最重要 |
做配置调整时,记住一个原则:没有万金油配置,只有适合场景的配置。你为了追求低延迟把 linger.ms 设成 0,吞吐就会掉;你把 acks 设成 all,系统瓶颈可能就从网络变成副本同步。每一项调整都是一笔交易,新人最容易犯的错误是想全都要,最后全都没做好。
5. 安装部署与基础命令实战:从单机到集群
5.1 基于 Docker 快速部署单机 Kafka(Windows/Mac/Linux 通用)
在本地机器上跑一个 Kafka 其实非常简单,用 Docker 是最省心的方式。Windows 和 Mac 用户可以装 Docker Desktop,Linux 用户装 docker 和 docker-compose 即可。下面是一个可以直接用的 docker-compose.yml:
执行 docker-compose up -d 后,Kafka 就起来了。注意这里用的是 KRaft 模式,也就是 Kafka 自带的元数据管理能力,不再依赖 ZooKeeper。3.0 之前 Kafka 必须配 ZooKeeper,3.0 开始逐渐过渡到 KRaft,新项目直接用这个即可,不用再额外维护 ZK 集群。
下载和安装遇到问题时,优先检查两件事:一是端口 9092 有没有被占用,二是 ADVERTISED_LISTENERS 里的地址是不是客户端能访问到的地址。如果只是本地调试,填 localhost 没问题;如果在 Docker 里做跨容器通信,这里要填宿主机 IP 或对应服务名,很多 Windows 下启动失败都是卡在这一点。
5.2 集群模式部署要点:Controller、Broker 数量与分区副本规划
生产环境不可能只跑单机,集群部署时几个关键点需要提前规划。
第一是节点数。KRaft 模式下,通常奇数台节点组成集群,推荐至少 3 台。Controller 用于管理元数据和分区分配,生产环境至少 3 个或者和 Broker 混合部署,避免单点。如果你已有 ZooKeeper 环境且不想迁移,3.0 之前的老版本也可以沿用 ZK 模式,但新项目不建议再从 ZK 起步。
第二是磁盘。Kafka 的数据目录 log.dirs 可以配置多个磁盘路径,比如 /data1/kafka,/data2/kafka。多块盘时 Kafka 会按“分区数少的目录优先放置”策略选盘,但依然建议多块数据盘做 RAID 或让 Kafka 自己跨盘分布,否则单盘故障会导致副本丢失。
第三是分区与副本规划。一个分区在集群里的副本数不宜过多,常见设置是 2 或 3。分区数则要考虑峰值吞吐量、消费者并发数、单分区顺序性要求。我的经验是:先把目标吞吐量换算出来,然后用“目标吞吐量 / 单分区预期吞吐(比如 5MB/s)”估算分区数,再稍微多留一些余量。分区数设置得太大也会带来副作用,比如文件句柄过多、rebalance 变慢,所以也不要拍脑袋设个千把个分区。
5.3 生产消费命令:启动一次会一直运行吗
网上有个提问频率特别高的问题:kafka 的生产消费命令启动一次会一直运行吗?这个问题的答案要分两条命令来看。
执行 kafka-console-producer.sh --broker-list localhost:9092 --topic test 启动生产者命令后,进程会一直运行,等待你在终端里一行一行输入消息内容。它会阻塞在标准输入上,不会自己退出。你想退出就按 Ctrl+C 结束进程。
执行 kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning 启动消费者命令后,进程同样会一直运行,持续阻塞等待新消息。这其实就是 Kafka 消费者客户端的设计:消费者会一直循环向 Broker 拉取数据,没有新消息就空转等待。
所以结论很明确:默认情况下两条命令都会一直运行,因为它们都是常驻进程。如果希望消费者在读完已有消息后自动退出,可以加 --timeout-ms 参数,比如:
意思是 5 秒内没有新消息就自动退出。还可以用 --max-messages 100 限定只消费 100 条消息后退出。这两个参数在实际调试时非常有用,能省去手动盯终端 Ctrl+C 的麻烦。
5.4 查看 Topic 与消息中的常用命令
日常维护最常用的是 kafka-topics.sh。想看集群里有哪些主题,用:
想看某个主题的分区数、副本分布、ISR 情况,用:
想直接看某个 Topic 里的数据内容,最直接的方式就是起一个临时消费者,用一个新的消费者组去消费:
注意一个坑:如果直接复用已有消费者组,消费者会从该组上次提交的 offset 开始消费,而不是从头读。加 --from-beginning 且使用新 group id,才能保证看到全量数据。调试完记得把临时 group 删掉,比如 kafka-consumer-groups.sh --bootstrap-server localhost:9092 --delete --group temp-view-group,否则 __consumer_offsets 里会积累一堆无用元数据。
除了命令行,也可以使用可视化工具。比较流行的开源工具有 Kafka UI、Offset Explorer、Kafdrop 等,它们可以直观地看 Topic 列表、分区数、消费者组和 lag,适合不想敲命令的时候管理集群。
6. 生产环境高频问题排查实录
6.1 重复消费问题:根本原因与解决方式
重复消费是 Kafka 使用中最常见的问题,几乎每个团队都会踩到。它的根因不在于 Kafka 本身“多发了消息”,而在于 offset 的提交时机和消费者的存活状态之间出现了间隙。
最常见的场景是:消费者拉取了一批消息开始处理,每条消息平均耗时较长,超过了 max.poll.interval.ms 的默认值 300 秒,Broker 判断这个消费者已经失联,把它踢出消费者组,触发 rebalance。rebalance 之后,这个消费者之前处理完的消息因为还没来得及提交 offset,被重新分配给另一个消费者,然后重新消费了一遍。
解决重复消费要分两条路走。一条是从机制上避免:调大 max.poll.interval.ms 和 max.poll.records,让消费者每次拉取数量少一点,处理快一点,不至于在两次 poll 之间超时。另一条是从业务上兜底:消费逻辑做好幂等,比如消息里有全局唯一订单号,消费时查一下数据库,如果已经处理过就直接跳过。
对于严格要求“不重不丢”的场景,Kafka 还提供了精确一次语义(EOS),需要生产者开启幂等 enable.idempotence=true,消费者端配合事务或自己去重。但精确一次有性能代价,不是所有场景都值得。我个人经验是先看业务能不能接受少量重复,如果能接受,用幂等键去重就够了;如果不能接受,再考虑事务和读已提交级别的消费端配置。
6.2 消息延迟偏高:别再只怪 Broker
消息延迟高,也就是消息积压,排查时第一反应容易去盯 Broker 的 CPU 和磁盘。但实际上大部分延迟问题出在消费端。
判断思路很直接:先查消费者的 lag,也就是消费进度落后生产进度多少。执行命令:
输出里会看到 LAG 列,表示该分区还有多少条消息没被消费。如果 lag 持续上涨,说明消费能力小于生产能力,问题就在消费端。常见原因有:
- 消费者数量小于分区数,并发不够。
- 单条消息处理逻辑太重,比如每条消息都调一次数据库事务。
max.poll.records设置太大,一次拉取 1000 条后处理时间过长,反而拖慢整体消费进度。- 消费端 CPU 出现瓶颈,或者频繁 Full GC。
生产端导致延迟的情况也存在,比如 acks=all 但某个 Follower 网络不稳定,ISR 抖动导致写入变慢。但这种问题往往伴随 Broker 端异常日志和监控指标波动,不至于让 lag 无限积压。所以遇到延迟高,先看 lag 趋势、再看生产速率,最后才动 Broker 配置。一上来就调 Broker 参数,大概率白忙活。
6.3 Kafka OOM 排查:从堆外内存到 Page Cache
Kafka OOM 也是一个高频问题,但很多人对 OOM 的理解停留在“内存不够了”。Kafka 作为 JVM 应用,确实可能堆内存溢出,但更隐蔽的是堆外内存问题。
先看堆内存。Kafka 启动脚本里可以通过 KAFKA_HEAP_OPTS 设置堆大小,比如 -Xmx6G -Xms6G。如果堆设置过小,高并发写入时会导致 Full GC 频繁甚至 OOM。排查时可以观察 JVM 的 GC 日志,用 jstat 或者直接接上监控平台看堆内存曲线。
再看堆外内存。Kafka 网络层使用 Java NIO 的 DirectByteBuffer,这部分内存不受 JVM 堆限制,但受 -XX:MaxDirectMemorySize 限制。当大量连接并发读写时,Direct Memory 可能被打满,抛出 OutOfMemoryError: Direct buffer memory。解决方式是适当调大 MaxDirectMemorySize,同时排查是否有消费者或生产者连接数异常。
另外一种常见情况是页缓存占用高导致看起来内存不够。Kafka 写数据会大量使用操作系统页缓存,页缓存占用的物理内存在 free 命令里会被算成 used,但这个 used 在系统内存压力大时可以被内核自动回收,不能和 JVM 堆内存混为一谈。如果发现 Kafka 进程 RSS 很高,但 GC 正常、堆内存也没涨,大概率是页缓存的功劳,不需要处理。
我做线上排查时一般按三步走:先看 JVM 堆和 GC,再用 jcmd 查看 Direct Memory 使用,最后用 dmesg 检查是否有操作系统层面的 OOM Kill。绝大多数 Kafka OOM 都出在前面两步。
6.4 数据重复与数据丢失的一线经验
数据重复和数据丢失是镜像问题,一个是因为太可靠导致重复,一个是因为不可靠导致丢失。先说丢失的场景。
丢数据最常见的原因有三个:生产者 acks=0,发送完根本不管结果,Broker 挂了就丢了;复制因子为 1,分区只有一个副本,磁盘坏了就真的没了;unclean.leader.election.enable=true,允许非 ISR 副本竞选 Leader,这个副本可能落后很多,它一旦上位,旧 Leader 上那些新消息就丢了。
数据重复的典型场景则是生产者重试。生产者发送消息后因为网络超时没有收到确认,于是重发了一次,但Broker 实际上已经把第一条消息写入成功了,结果就是同一条消息被写入了两次。Kafka 从 0.11 版本开始支持幂等生产者,开启 enable.idempotence=true 后,Broker 会通过序列号机制识别并丢弃重复写入。注意幂等只保证单分区内不重复,跨分区需要配合事务 API。
给新人的建议就一句话:消息丢失靠配置,消息重复靠业务幂等。Kafka 能帮你把丢失概率降到非常低,但绝对不丢在分布式系统里只是理论上的承诺,业务端一定要有兜底方案。
7. 面试高频考点速查:像讲人话一样讲原理
7.1 基础概念类问答
面试官问“Kafka 为什么快”,你要能一层一层往外讲:顺序写盘利用磁盘最优性能,页缓存降低读延迟,零拷贝减少 CPU 拷贝,批次收发减少网络往返,分区并行提升吞吐。抽掉任何一层,Kafka 都不会有今天的名声。
问“消息不丢失怎么保证”,要从三个端分别回答。生产者端:acks=all 加重试机制;Broker 端:复制因子大于 1,min.insync.replicas 不小于 2,关闭 unclean 选举;消费者端:关闭自动提交,处理成功后手动提交 offset。
问“消息顺序怎么保证”,核心就一句话:Kafka 只在分区内保证顺序。要全局顺序就只用一个分区,但那样吞吐上不去;实际做法是按业务 key 路由到分区,保证同一个订单或同一个用户的消息在同一个分区内有序。
7.2 进阶类问答
问“重复消费如何解决”,先说根因(offset 提交滞后 + rebalance),再说两种解法(机制上调整 max.poll.records 和 max.poll.interval.ms,业务上做幂等),最后补一句“严格还要上事务和精确一次语义,但生产环境要看代价”。
问“Kafka 和 Pulsar/RabbitMQ 的区别”,核心差异是存储模型。Kafka 是日志存储、分段追加、消费者从任意 offset 读;Pulsar 是存算分离、分层存储;RabbitMQ 是传统队列模型、消费后删除。理解这几类模型差异,比死记特性列表有用得多。
问“一个 Topic 应该设置多少分区”,需要给出思路而不是标准答案:看目标吞吐量、消费者并发、单分区顺序性要求。分区太少会限流,分区太多会带来文件句柄和 rebalance 成本。回答时能说出这个权衡过程,基本就过关了。
我自己带团队这些年最大的体会是,Kafka 相关的很多问题,根子都在你对“分布式日志”这个本质的理解上。有一次线上丢消息,好几个人围着 Broker 调参数,最后发现是生产者端没开重试,acks 又设了 0——这不是 Broker 的锅,而是对写入链路不够熟。所以如果你刚开始接触 Kafka,别急着背命令和架构图,先把日志模型吃透,后面遇到问题基本都能自己推出来。这篇文章里涉及的安装、命令、排查手段,大家都可以在本地环境照着试一遍,踩过坑之后理解会深很多。最后再分享一个小技巧:排查消息积压问题时,不要只看集群监控面板,先跑一下 kafka-consumer-groups.sh 看 lag 变化趋势,很多时候问题一眼就出来了。