1. 这节实操课,解决你 Kafka 使用中的哪些真实痛点
到了入门第五篇,前面几篇应该已经把 Kafka 是什么、怎么搭、基础生产消费流程捋清楚了。但很多人一到自己的业务里就会发现:光会用命令行收发消息远远不够。数据量一上来,Partition 怎么分?消息发出去到底会不会丢?消费端明明重试了为什么还是重复消费?线上 Topic 越来越多,Leader 一直在切换怎么办?
这些问题,本质上都落在两件事上:Topic/Partition 的建模与管理,以及消息可靠性的配置与取舍。这篇不聊概念定义,直接讲实操,我尽量把每条命令、每个参数背后的原因说透,而不是丢给你一堆配置项让你自己猜。
我假设你已经有一套 Kafka 环境,无论是单机版还是集群版,命令行的 bin 目录已经配好了。我用的是 Kafka 3.x 版本,如果你还在用 2.x,绝大部分命令和参数是通用的,个别差异我会在文中指出来。如果你连环境都还没搭,建议先回去看系列前面几篇,把集群先跑起来再往下读。
这篇适合的人群主要有两类:一类是刚接手 Kafka 运维或开发、需要对线上 Topic 做规范管理的同学;另一类是已经在写生产消费代码、但被"消息丢了""重复消费""消费者卡死"这些问题反复折磨的同学。看完你至少能自己回答三个问题:一个 Topic 到底该建几个 Partition?可靠性配置是不是配得越严越好?消费端怎么提交位移才算稳?
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Topic 和 Partition 的创建学问:为什么说建表就决定了上限
2.1 一条创建命令背后的分区设计逻辑
Kafka 里最常用的命令大概是这个:
bash复制bin/kafka-topics.sh --bootstrap-server localhost:9092 \
--create \
--topic order-event \
--partitions 6 \
--replication-factor 3
分区数设 6,副本数设 3,很多人是照着网上抄的,不知道为什么是这个数。其实这两个数字直接决定了这个 Topic 的吞吐上限、可用性水平和数据冗余成本。
Partition 数量解决的是并行度问题。一个 Partition 只能被同一个消费组里的一个消费者线程消费,所以如果业务预估峰值每秒写入 5000 条消息、单条消息大小约 1KB,单分区实测每秒只能处理 800 条左右,那你至少需要 7 个分区才能扛住峰值。但问题是分区越多,Broker 端需要维护的 Leader/Follower 元数据、文件句柄、副本同步开销都会线性增长。我见过有人把一个 Topic 建了 300 个分区,其实单分区吞吐早就够用,白白消耗了一堆文件句柄,最后 Broker 在分区数达到数千时出现 ZooKeeper 元数据压力(KRaft 模式下则会出现 Controller 与 Broker 之间元数据同步变慢)。
给你一个可落地的估算思路:先拿你的业务消息做一次压测,得到这个 Topic 在单分区上的真实吞吐值(通常 1KB 消息在机械盘上约 500~1000 条/秒,SSD 上约 2000~5000 条/秒),然后用"峰值消息速率 / 单分区吞吐 x 1.5 冗余系数"来计算分区数,再向上取整对齐到 Broker 数量的倍数。比如 3 个 Broker,单分区 2000 条/秒,业务峰值需要 6000 条/秒,那就是 3 个分区 x 1.5 ≈ 4.5,向上取整到 6,刚好也是 3 的倍数,这样各个 Broker 的负载分布相对均匀。
拷贝副本数(replication-factor)解决的是可用性和持久性问题。副本数为 1 时,机器一挂整个分区直接不可用;副本数为 3 时,允许 2 个副本所在的 Broker 同时宕机而数据不丢。但副本数也不是越多越好,每多一个副本,Leader 就要多一份网络和磁盘 IO 开销去同步数据。生产环境建议固定为 3,如果业务数据本身是临时性的、允许丢失,可以降到 2。别用 1,除非你只是本地测试。
提示:Kafka 创建 Topic 后,Partition 数量只能增加不能减少,副本因子可以调整但过程比较繁琐(涉及 reassign 操作)。所以建 Topic 前先把分区数算清楚,后面要改真的很麻烦。
2.2 分区数上线后扩容,以及几个必须注意的副作用
假设业务翻了三倍,当初建的 6 个分区扛不住了,你需要扩容。命令很简单:
bash复制bin/kafka-topics.sh --bootstrap-server localhost:9092 \
--alter \
--topic order-event \
--partitions 12
但扩容不是改了数字就完事。有几个副作用你必须知道:
第一,分区在 Broker 上的分布不会自动均匀。Kafka 只在创建 Topic 时做一次分区分配,之后新增的分区会尽量选磁盘空间最小的 Broker 落盘,但原有的 6 个分区还在原来那几台机器上,你会发现 Broker 之间的负载非常不均匀。这时候需要手动做一次分区重分配(reassignment),用 kafka-reassign-partitions.sh 生成一个分配方案并执行。
第二,消息在换 partitioner 之后路由规则变了。Kafka 默认用 key 哈希分区,key 没变的情况下哈希值也不变,但分区数变了之后 key 对分区数取模的余数会变,同一个 key 的消息会落到新的分区里。这意味着如果你依赖分区内的顺序性,扩容后需要确认消费逻辑里是否以 key 为粒度做了有序处理,否则会出现同一个 key 的消息被不同消费者线程并行消费,顺序就是乱的。
第三,扩容期间会产生额外的副本同步流量。新的分区分配要移动部分旧分区的数据,这些走的是内部复制通道,会占用 Broker 的带宽和磁盘 IO。线上高峰期前 1~2 小时别做这种操作,建议放到低峰期,或者把 reassign 的 throttled 速率设置得保守一点。
2.3 Leader 分布不均,以及相关可靠性隐患
分区数和副本数建好后,还要学会观察分区和 Leader 的分布。用下面这条命令可以看到每个分区的 Leader 和副本落在哪台 Broker:
bash复制bin/kafka-topics.sh --bootstrap-server localhost:9092 \
--describe \
--topic order-event
输出的关键信息是 Leader 和 Replicas 列。正常情况下,同一个分区里的 Leader 和不同副本应该尽量分散在不同的 Broker 上,这样才能做到单台 Broker 宕机时其他副本依然可用。如果你发现大量分区的 Leader 都挤在同一个 Broker 上,那就需要排查几个原因:Broker 启停顺序是否健康、Controller 是否在做不合理的重新选举、消息量是否造成了某些 Broker 的磁盘 IO 瓶颈。
Leader 分布不均的直接后果是:那台承担大量 Leader 的 Broker 负载会显著高于其他节点,一旦它宕机,这些分区的故障切换成本高,还可能因为它本身就是唯一存活副本而出现数据不可用。通过 kafka-leader-election.sh 或者触发 preferred replica election 可以把 Leader 重新拉平,但在做这个操作前建议先看下是否由磁盘或网络短板导致,否则即使临时把 Leader 切走,过段时间它们又会切回来。
3. 消息可靠性的三段式防线:生产者端不丢数据的关键配置
3.1 acks 从 0 到 all 的实际语义,以及你真正该选的档位
消息可靠性第一个失守点往往是生产者。Kafka 生产者有一个参数叫 acks,它的取值直接影响你发出去的消息到底有谁确认收到了。
- acks=0:生产者不等待任何确认,发完就完,丢数据概率极高,一条消息可能因为网络抖动、Broker 临时不可用而消失,通常只用在日志采集等允许丢失的场景。
- acks=1:Leader 把消息写入本地日志后即返回给生产者确认,不用等 Follower 同步。这是吞吐和可靠性的一个折中档,但如果 Leader 在 Follower 还没复制完时就宕机了,消息会丢。
- acks=-1(或 all):Leader 必须等待所有 ISR(In-Sync Replicas,同步中的副本)都确认写入后才返回。这是最严格的语义,只要 ISR 中至少有一个存活副本,消息就基本不会丢。
很多人的误区是:我设置了 acks=all,消息就一定不会丢。这个想法漏洞很大。acks=all 只能保证消息在 ISR 副本都被写入,但假如 Topic 的副本因子是 1,ISR 只有一个 Leader,acks=all 跟 acks=1 就没有本质区别。所以我想给你一个生产环境的黄金组合:副本因子设为 3,acks 设为 all,同时配合下面要讲的 min.insync.replicas 参数,这三个条件缺一个,可靠性都站不住。
3.2 生产者重试机制与幂等生产的搭配关系
设置 acks=all 之后,依然有可能出现一种情况:消息已经被 Leader 写入并同步给副本,但 Ack 确认在网络传输中丢了,生产者收不到确认会触发重试,导致同一条消息被写入多次。这是 Kafka 中"at least once"语义天然带来的重复风险。
处理方式有两条路线。第一条是开启幂等生产者:在生产者配置里加 enable.idempotence=true。这个参数从 Kafka 0.11 开始支持,原理是给每条消息打上序列号(sequence number),Broker 端会按照生产者 PID 和序列号做去重,重复的写入请求会被直接忽略。现在的新版本里 enable.idempotence 默认就是 true,所以只要你不是老古董版本,这一点基本不用担心。
第二条路线是配合事务 API(beginTransaction、commitTransaction 等)做"精确一次"(exactly-once)语义,比如流处理场景下需要保证"读取Kafka -> 处理 -> 写回Kafka"这个整条链路上不重不丢。事务的代价是性能上有额外开销,因为需要事务协调器和额外日志,只有对精确一次有硬性要求时才值得用。大多数订单、消息通知类业务,用幂等生产者 + 消费者端的幂等处理就够了。
properties复制# 生产者端可靠性配置参考
acks=all
enable.idempotence=true
max.in.flight.requests.per.connection=5
retries=2147483647
delivery.timeout.ms=120000
有关 max.in.flight.requests.per.connection,默认值在开启幂等时是 5,意思是同一连接上最多可以存在 5 个未确认的请求。如果你关闭幂等还把这个值设大于 1,是有可能出乱序的。如果业务强制要求全部消息严格有序,需要把它设为 1,但吞吐会明显下降。我自己的实践结论是:绝大多数业务不需要全局严格有序,所以按默认 5 就行,别为了"可能出现的乱序"过度牺牲吞吐。
3.3 实际验证:acks 与 min.insync.replicas 的组合效应
只看参数定义不够直观,我建议你动手做个验证。假设你有 3 个 Broker,Topic 副本因子为 3,不设置 min.insync.replicas(默认是 1),然后手动停掉一个 Broker(模拟故障),用生产者发一批消息,你会发现消息照常写入成功,因为 Leader 只需要考虑自己写入就算完成同步要求。这时候你只停一个 Broker,因为有 2 个副本还活着,数据层面的可靠性没有明显受损。但如果 Topic 是双副本且没设 min.insync.replicas,停掉 Follower 后 ISR 里只剩 Leader,此时仍然可以写入,但一旦 Leader 接着宕机,整个分区的数据全是孤本。
设置 min.insync.replicas=2 后再做同样的操作,结果会不同:当 ISR 数量低于 2 时,生产者的写入会直接报 NotEnoughReplicasException,而不是静默接受。这种"宁可写不进去,不可无声丢失"的做法,对支付类、订单类、强一致类业务是必要的。要注意的是,min.insync.replicas 是 Broker 端或 Topic 级别的参数,不是生产者参数,需要在 server.properties 或 Topic 配置里修改:
bash复制bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter \
--entity-type topics \
--entity-name order-event \
--add-config min.insync.replicas=2
这段配置的代价是可用性下降:如果两个副本所在的 Broker 同时挂掉,这个分区会拒绝写入,而不是用唯一副本继续服务。很多团队在这个点上产生分歧,我的建议是"基础数据用 2,核心交易数据用 2 起步、3 更稳",因为用可用性换数据安全在强一致业务里是值得的。如果业务本来就允许丢弃,那直接把副本因子降成 1、acks 设 0 更省事。
4. Broker 端刷盘与 Leader 切换的隐藏坑位
4.1 刷盘策略到底该不该调
Kafka 的日志写入虽然先落操作系统的 PageCache,但操作系统并不会立刻把脏页写回磁盘。如果 Broker 突然断电,PageCache 里的数据就丢了。Kafka 自身提供两个参数:log.flush.interval.messages(累计多少条消息后强制刷盘)和 log.flush.interval.ms(超过多少毫秒强制刷盘)。
默认情况下 log.flush.interval.messages 是 Long.MAX_VALUE,log.flush.interval.ms 也是很大的值,意思是 Kafka 把刷盘时机完全交给操作系统。这是因为 Kafka 设计上认为"多副本同步 + Leader 切换"已经能保证可靠性,单机断电导致的 PageCache 数据丢失可以通过其他副本恢复。如果你把副本因子设成了 3、ack=all,我不建议再去调整刷盘参数,因为每刷一次盘都是把一批很小的数据写入磁盘,性能损耗明显,而可靠性收益几乎为零。
只有一种情况我建议你关注刷盘:Topic 副本因子为 1 且不允许丢任何数据。此时你确实需要把 log.flush.interval.messages 设成比如 10000,把 lag.flush.interval.ms 设成比如 2000,但本质上你还是把单点的可靠性寄托在了硬件和电源稳定上,并不推荐在生产环境这样玩。
4.2 多副本场景下,Leader 切换时的持久性细节
当 Leader 发生故障时,Controller 会从 ISR 中挑选一个新 Leader。这个选主过程本身是很快的,但有一个坑:如果 ISR 里只剩一个 Follower,而当时 Leader 的 PageCache 里还有一批未刷盘的消息,那这些消息在 Follower 上是没有的,新 Leader 顶上后这批消息直接没了。
这个现象背后的原因是 Kafka 的副本同步不是同步复制,而是异步批量复制。Leader 只要把消息写入本地 PageCache,就会把这条消息放进"待拉取"队列让 Follower 来拉取,Follower 通常会在毫秒级拉走。但极端情况下(网络延迟、Follower 瞬时卡顿)确实会有几毫秒到几十毫秒的差距。所以副本因子为 3、acks=all 并不是"绝对不会丢数",而是在硬件正常范围内的强可靠性。真正要做到零丢失,需要同步刷盘 + 多副本同时确认写入,Kafka 并不支持这种方式,它本来就为高吞吐做了取舍。
这里也顺带解释一个排查方向:如果你发现某个分区在故障切换后消息数量变少了,优先检查两个日志——Broker 端的 server.log 里是否有"unclean leader election"相关记录,以及 Topic 的 ISR 是否曾有缩容记录。如果 ISR 缩容到 1 后发生了 Leader 切换,大概率就是这几十毫秒的窗口造成的。
4.3 unclean.leader.election.enable:该关还是该开?
和 Leader 切换紧密相关的参数是 unclean.leader.election.enable。默认值是 false,表示 Controller 只会从 ISR 中选 Leader;如果你改成 true,就允许"不同步的副本"参与选主,这样做的唯一好处是"只要能启动的 Broker 就能马上顶上",坏处是那些没有同步完的数据会直接丢,并且可能导致消息回退。
生产环境我强烈建议保持 false。有些人为了"可用性优先"把这个开关打开,一旦发生极端情况,消息不仅会丢,还会出现消费位点倒退,这种数据损坏比短暂不可用更难接受。如果集群里所有副本都同时宕机了,那说明你该关注的是备份恢复机制,而不是 open unclean election。
5. 消费端可靠性配置:从提交位移到重复消费的全链路分析
5.1 启用手动提交位移之前,先想清楚
消费者端的可靠性,核心就是位移(offset)管理。Kafka 默认配置 enable.auto.commit=true,每 5 秒自动把当前消费位置提交给 __consumer_offsets 主题。自动提交在绝大多数入门场景能用,但在真实业务中很容易出现两个问题。
问题一是重复消费:假设你把一条消息处理完用了 8 秒,自动提交间隔是 5 秒,处理过程中消费者崩溃了,当前位移还没提交,重启后它会从上次提交的位置重新消费一批消息,这批消息里包含刚才那条已经处理完的。问题二是漏消费:自动提交发生在 poll 之后、处理逻辑之前,如果你在 poll 之后立即触发了提交,但后面处理逻辑里抛异常了,那这条消息的位移已经提交了,重启后就不会再消费它。
所以进阶做法是手动提交位移。比较稳的姿势是先处理完业务逻辑,再提交位移。为了把逻辑梳理清楚,我贴一段伪代码:
java复制while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
handle(record); // 先处理业务逻辑
}
consumer.commitSync(); // 全部处理成功后再同步提交
}
这段代码能保证"处理成功才提交位移",配合 Kafka 的 at least once 语义,你遇到的最坏情况就是"处理成功但提交前崩溃,重启后重跑一次",也就是重复处理。对于大多数业务场景,保证不丢 + 允许重复,唯一要做的就是消费端对重复做幂等(比如对唯一业务键做去重)。
5.2 commitSync 还是 commitAsync:我用下来的真实比较
手动提交位移时有两个 API:commitSync 和 commitAsync。commitSync 是同步提交,提交失败会抛异常、阻塞当前线程,直到提交成功或超时;commitAsync 是异步提交,调用后立即返回,失败时不会自动重试,由你传入的 OffsetCommitCallback 来处理失败结果。
我个人的实践经验是:整体位移提交用 commitSync(最后一步务必同步提交),但在消费过程中每处理完一批就做一个异步提交来降低提交延迟。这里最忌讳的是只调 commitAsync 就完事,因为异步提交失败时不重试,很容易出现位移提交成功后程序崩溃,导致重启后从一个更早的位移开始消费,大量重复消息会把你后面入库的业务搞得很痛苦。
一个相对稳妥的模板是把两种方式都利用起来:
java复制while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
handle(record);
}
// 实际落库或处理完成后再提交,建议用 sync 兜底
try {
consumer.commitSync();
} catch (CommitFailedException e) {
// 记录告警,等待下轮继续处理
}
}
注意 Catch 里要处理 CommitFailedException,这个异常出现通常是因为消费者因为耗时过长被剔除出消费组(max.poll.interval.ms 超时),导致提交不了位移。此时需要调整 poll 循环里的处理耗时,或者调大 max.poll.interval.ms、降低 max.poll.records,让单轮 poll 的消息量少一些。
5.3 max.poll.records 和 max.poll.interval.ms 的合理搭配
如果消费者端频繁出现 rebalance、重复消费、Consume 卡住被踢出消费组,大多数时候问题出在这两个参数上。max.poll.records 控制单次 poll 返回的最大消息条数(默认 500),max.poll.interval.ms 控制消费者两次 poll 之间的最大间隔(默认 300000 毫秒,即 5 分钟)。如果单批次 500 条消息的处理时间超过 5 分钟,Broker 会认为该消费者已失活,触发 rebalance 并暂停它的分区分配。
这个默认配置对业务场景很坑的地方在于:每条消息如果处理耗时超过 600ms,500 条就是 5 分钟,非常容易踩线。我第一次碰到这个问题时,消费组里一个消费者突然被踢出,导致另一个消费者接管了它的所有分区,又因为单消费者并发有限,整体 lag 飙升。
对症下药的方法有三个:调大 max.poll.interval.ms 到比如 10 分钟,调小 max.poll.records 到 100 或 200,以及在 poll 循环里用异步线程池处理消息,让你的"poll 间隔"只受轮询速度影响,不受业务处理耗时影响。第三种方案能提升吞吐,但要注意提交位移的时机要切成"处理完批次后统一提交",而不是每个线程各自提交。
5.4 消费组 Rebalance 时,如何避免风暴
消费组 Rebalance 是心跳超时、分区变化、消费者增减等原因触发的协调流程。平时偶尔 rebalance 没问题,怕的是短时间内反复触发,导致消费被频繁暂停,位移反复横跳。这类问题的排查思路基本是:先看 session.timeout.ms(默认 45 秒或 10 秒)和 heartbeat.interval.ms(默认 3 秒或 0.5 秒)的配比——heartbeat 间隔应该小于 session 超时的三分之一,否则 Broker 在等待心跳时容易误判消费者死亡。
更隐蔽的一个坑是消费者处理消息时的全同步阻塞。如果你的消费者里用了外呼网络、数据库写入等慢操作,建议把慢操作和 poll 解耦,比如用一个阻塞队列承接收到的消息,工作线程从队列里取消息处理,主线程持续 poll 和发送心跳。这样心跳不会断,Broker 就不会误判。
6. 实测对比:不同可靠性档位的吞吐差异到底有多大
讲了这么多配置,很多人还是心里没底:把可靠性调高,性能损失到底能不能接受?我拿一套 3 节点 Kafka 集群(单机 8 核 16G,SSD,Kafka 3.4,副本因子 3,分区数 12,单条消息 1KB)做过一组粗测,结果给你参考。
| 配置组合 | acks | min.insync.replicas | 生产吞吐(条/秒) | 备注 |
|---|---|---|---|---|
| 极致吞吐 | 0 | 1 | ~58000 | 允许丢消息,不推荐业务用 |
| 折中档 | 1 | 1 | ~43000 | Leader 确认,快,但有一定丢失风险 |
| 标准可靠 | all | 2 | ~31000 | 生产环境常用组合 |
| 严格可靠 | all | 3 | ~25000 | ISR 全员确认,把可靠拉满 |
同样是 all 级别,min.insync.replicas 从 2 提高到 3 后吞吐会下降 20% 左右,这个幅度在我的测试环境里还算能接受。我见过一些团队为了绝对可靠把副本因子设成 5,其实带来的额外同步开销和运维复杂度和可靠性的边际收益不成正比。我的建议是:副本因子 3、acks=all、min.insync.replicas=2 是大多数生产业务的"甜点位"。
消费端的性能损耗主要来自手动提交位移。自动提交每 5 秒一次,几乎不占时间;手动提交每批一次,如果批次很小,提交频率会很高。实测里手动提交大概会多出 5%~10% 的开销,但换来的是"位移和业务结果一致"的确定性,这笔账划算。如果你觉得手动提交频繁导致吞吐不够,可以把批量拉大一些(max.poll.records 调高),让每批消息处理更久、提交更少。
7. 线上排查 Case:Topic 分区卡在"不可用"状态的全过程
最后分享一个真实的线上排查案例,它把前面讲的 Topic/Partition 管理、Leader 选举和可靠性配置全部串起来了。
现象是某天监控告警,一个核心 Topic 的某个分区发送失败,报错是 LEADER_NOT_AVAILABLE。先执行 describe 命令看分区状态,发现这个分区的 Leader 是 -1,意味着当前没有 Leader,分片处于不可用状态。再细看 ISR 列表,里面还有两个副本在,说明副本本身没丢,只是 Controller 没有选举出 Leader。
接着查 Broker 日志,发现 Controller 一直试图从 ISR 中选 Leader,但选主需要确认选中的副本已经完成了"从上次高水位之后的日志截断",如果副本日志里有未提交的段,选主流程会被卡住。再查磁盘,发现那台候选 Broker 的磁盘空间已满,导致日志截断操作迟迟无法完成。
处理过程分几步:先清理 Broker 上其他占用大量磁盘的临时日志或者扩容磁盘,恢复正常运行状态;然后手动触发一次 PreferredReplicaElection,把这个分区的 Leader 选到健康的副本上;最后修复根因:把该 Topic 的日志保留时间调短,加上磁盘使用率告警,确保以后再出现磁盘打满时能提前干预。
这个案例给我们的直接教训有两点:一是分区/副本"看着都在"不等于"服务可用",ISR 里有副本但 Leader 为空的情况往往和磁盘、网络等底层资源有关;二是可靠性配置再严谨,底层基础设施出问题了,一切归零。
排查链路其实可以总结为:先看 describe 的 Leader/ISR 状态,再确认磁盘和网络资源,然后查 Controller 日志里的选举卡点,最后修复基础设施并重选 Leader。这套思路在处理 Kafka 分区故障时几乎通用,之后遇到类似问题可以按这个顺序走,效率高很多。
