1. 从“能跑起来”到“跑得明白”:Kafka 基础入门的第二道坎很多人第一次接触 Kafka,都是照着官方 Quickstart 敲一遍命令行,看到生产者发消息、消费者收到消息,就觉得“我会了”。但真到了项目里,面对一堆server.properties参数、acks配置、分区策略、消费者组重平衡,立刻又懵了。这个“入门(二)”要解决的,恰恰是从“能跑起来”到“跑得明白”之间的那道坎。我自己带过不少新人,也踩过不少坑。最典型的一个场景:某次压测,生产者端日志显示消息发送成功,消费者端却死活收不到数据。排查了大半天,最后发现是消费者组里有两个实例用了同一个group.id,但订阅的 topic 不一样,导致分区分配错乱。这种问题,光看 Quickstart 是永远遇不到的。所以这篇内容,我打算把 Kafka 入门阶段最容易含糊的几个核心概念——主题与分区、生产者可靠性、消费者组与位移管理、消息顺序与重复——掰开揉碎讲清楚。每个点都会配上我实际用过的配置、参数计算过程,以及踩坑之后的经验总结。适合已经跑通过 Hello World、但一上生产就心里没底的开发者,也适合想系统梳理 Kafka 基础知识的同学。2. 主题与分区:Kafka 并行处理的地基2.1 为什么分区数不是拍脑袋定的Kafka 的 topic 只是一个逻辑概念,真正干活的是 partition。每个 partition 是一个有序的、不可变的消息序列,物理上对应一个目录下的 segment 文件。分区带来的最大好处是并行:生产端可以同时往多个分区写,消费端也可以多个实例同时读。但分区数定多少,很多人是随手写个 3 或者 6。我见过一个团队,业务量不大,却给每个 topic 开了 30 个分区,结果 broker 上文件句柄暴涨,集群时不时抖动。分区数不是越多越好,它直接关联到几个成本:文件句柄:每个 partition 在 broker 上至少对应一个活跃 segment 文件和一个索引文件,分区越多,打开的文件越多。端到端延迟:Kafka 的副本同步是按分区进行的,分区多了,leader 需要维护的 follower 拉取请求也多,极端情况下会影响延迟。重平衡时间:消费者组重平衡时,分区分配是基本单位,分区越多,重平衡耗时越长。那怎么算?一个常用的经验公式是:分区数 = max(目标吞吐量 / 单分区吞吐量, 消费者并行度)单分区吞吐量在普通机械盘上大约 10MB/s 到 50MB/s,SSD 上可以到 100MB/s 以上。假设你的目标写入吞吐是 200MB/s,单分区实测能跑 50MB/s,那至少需要 4 个分区。同时,如果你计划最多用 8 个消费者实例并行消费,那分区数不能少于 8,否则多出来的消费者会闲置。我自己的习惯是:先按吞吐量算一个下限,再按未来半年的消费者并行度留一点余量,但绝不超过必要值的两倍。比如算下来需要 6 个,那就定 8 个,而不是 16 个。2.2 分区策略:消息到底进了哪个分区生产者发送消息时,如果没有指定 partition,Kafka 会根据分区器决定消息落到哪个分区。默认的分区器逻辑经历过几次变化,现在主流版本的行为是:如果消息有 key,对 key 做 murmur2 哈希,然后对分区数取模。如果消息没有 key,在 2.4 版本之前是轮询,之后改成了粘性分区策略。粘性分区的意思是:在一条批次填满或者 linger.ms 到期之前,生产者会一直往同一个分区发,填满后再换下一个。这样做的好处是减少小批次,提升吞吐。但这也带来一个副作用:没有 key 的消息,在短时间内会集中在一个分区。如果你依赖“无 key 消息均匀分布”来做负载均衡,可能会失望。我实测过一个场景:单生产者、无 key、每秒 1000 条消息,linger.ms 设为 5ms。结果在 5ms 窗口内,所有消息都进了同一个分区,下一个窗口才换。从宏观上看是均匀的,但微观上是有倾斜的。如果你的消费端处理逻辑对分区倾斜敏感,要么给消息加上合理的 key,要么调整 linger.ms 和 batch.size 让批次更小更频繁。注意:给消息加 key 时,一定要选基数足够大的字段。我见过用“性别”做 key 的,结果只有两个分区有数据,另外几个分区完全空闲,消费端并行度直接废掉。2.3 副本与 ISR:数据不丢的底层保障每个 partition 可以有多个副本,其中一个是 leader,其余是 follower。生产者和消费者只跟 leader 交互,follower 只负责从 leader 拉数据做备份。ISR 是“in-sync replica”的缩写,意思是跟 leader 保持同步的副本集合。这里有个关键点:只有 ISR 里的副本才有资格被选为新 leader。如果一个 follower 落后太多,它会被踢出 ISR。落后多少算太多?由replica.lag.time.max.ms控制,默认 30 秒。也就是说,如果一个 follower 超过 30 秒没有追上 leader 的进度,就会被移出 ISR。这个参数直接关系到可用性和一致性。调大了,ISR 里可能包含很落后的副本,leader 切换时容易丢数据;调小了,网络抖动一下副本就被踢出去,ISR 频繁收缩,影响写入稳定性。我一般会在内网环境把它设成 10 秒左右,同时配合min.insync.replicas使用。min.insync.replicas的意思是:生产者写入时,至少要有这么多个副本确认收到,才算成功。它和acks=all配合使用。假设副本因子是 3,min.insync.replicas=2,那么只要有两个副本(包括 leader)确认,写入就成功。如果此时一个 follower 挂了,ISR 只剩两个,仍然可以写入;但如果再挂一个,ISR 只剩一个,小于 2,生产者就会收到NotEnoughReplicasException,写入被拒绝。这套组合的意义在于:在保证一定冗余的前提下,尽量维持可用性。如果设成min.insync.replicas=3,那任何一个副本挂掉都会导致写入不可用,对大多数业务来说太严苛了。3. 生产者可靠性:acks 与重试的取舍3.1 acks 三个值的真实含义acks参数控制生产者在什么条件下认为一条消息发送成功。它有三个取值:acks 值含义可靠性吞吐0生产者发出消息就认为成功,不等 broker 确认最低,broker 挂了就丢最高1leader 写入本地日志就确认,不等 follower中等,leader 挂了可能丢中等all (-1)ISR 中所有副本都确认才成功最高最低很多人以为acks=all就万无一失了,其实不然。它只保证“ISR 里的副本都收到了”,但如果 ISR 只剩 leader 一个,那acks=all就退化成acks=1。所以acks=all必须和min.insync.replicas一起用才有意义。我自己的配置习惯是:核心业务用acks=all+min.insync.replicas=2+ 副本因子 3;日志类、监控类数据用acks=1,追求吞吐;极少数允许丢数据的场景才用acks=0。3.2 重试与幂等:重复消息从哪来生产者发送失败后会重试,由retries参数控制,默认是Integer.MAX_VALUE。重试本身是好事,但它会带来重复消息的问题。设想一个场景:生产者发送消息,broker 写入成功,但确认响应在网络中丢失了。生产者以为失败,触发重试,broker 又写入一条。结果就是同一条消息在 Kafka 里出现了两次。解决这个问题的标准方案是开启幂等生产者,配置enable.idempotence=true。它的原理是给每个生产者分配一个 PID,并为每条消息附加一个序列号。broker 会缓存每个 PID 最近几个批次的序列号,如果收到重复序列号,就直接丢弃。开启幂等后,有几个参数会被自动调整:acks强制为all,retries大于 0,max.in.flight.requests.per.connection不超过 5。最后这个参数很关键,它控制生产者在收到确认之前最多能发多少个请求。如果设成 1,吞吐会受影响;设成大于 5,幂等性可能被破坏。所以 Kafka 自动把它限制在 5 以内。实操心得:幂等生产者只能保证单个生产者会话内不重复。如果生产者重启,PID 会变,之前的序列号缓存失效,跨会话的重复仍然可能发生。要做到端到端精确一次,还需要事务或者消费端去重。3.3 批次与延迟的平衡生产者不是来一条消息就发一条,而是攒成批次再发。两个参数决定批次行为:batch.size:一个批次的最大字节数,默认 16KB。linger.ms:批次等待时间,默认 0。linger.ms=0意味着生产者会尽可能快地发送,哪怕批次里只有一条消息。这样延迟最低,但吞吐也最低。把linger.ms调到 5 到 100 毫秒之间,可以让更多消息攒进一个批次,显著提升吞吐,代价是增加了一点延迟。我做过一组对比测试,单分区、消息大小 1KB、目标吞吐 50MB/s:linger.msbatch.size实际吞吐平均延迟016KB12MB/s2ms532KB38MB/s8ms2064KB52MB/s25ms50128KB58MB/s60ms可以看到,linger.ms从 0 调到 5,吞吐翻了 3 倍,延迟只增加了 6ms。这个 trade-off 对大多数业务都是划算的。但如果你做的是实时风控、在线交易这类延迟敏感场景,那linger.ms还是保持 0 或者 1 比较稳妥。另外,batch.size不是越大越好。批次太大会占用更多内存,而且如果消息产生速率低,批次迟迟填不满,反而增加延迟。我一般会把batch.size和linger.ms配合调:高吞吐场景用 64KB + 20ms,低延迟场景用 16KB + 0ms。4. 消费者组与位移管理:并行消费的核心机制4.1 消费者组到底解决了什么问题消费者组是 Kafka 实现单播与广播统一的关键设计。同一个组内的消费者,共同消费一个 topic 的所有分区,每个分区只被组内一个消费者消费;不同组的消费者互不影响,可以各自消费全量数据。这个模型解决了一个很实际的问题:消费能力水平扩展。当单个消费者处理不过来时,直接往同一个组里加实例就行,Kafka 会自动把分区重新分配。但这里有个硬约束:组内消费者数量不能超过分区数,多出来的消费者会处于空闲状态,白白占用资源。我见过一个团队,消费者实例开了 20 个,但 topic 只有 8 个分区,结果 12 个实例全程摸鱼。他们还以为是消费逻辑慢,查了半天才发现是分区数不够。所以前面讲分区数计算时,一定要把消费者并行度考虑进去。4.2 位移提交:自动还是手动消费者消费到哪了,靠的是位移(offset)。位移提交有两种方式:自动提交:enable.auto.commit=true,消费者每隔auto.commit.interval.ms(默认 5 秒)自动提交一次当前位移。手动提交:enable.auto.commit=false,由业务代码在合适的时候调用commitSync()或commitAsync()。自动提交看起来省事,但有个致命问题:它提交的是“拉取到的位移”,而不是“处理完的位移”。假设消费者拉了一批消息,自动提交把位移推进了,但业务处理到一半崩溃了,重启后从新位移开始消费,中间那批没处理完的消息就丢了。所以只要业务对数据完整性有要求,就必须用手动提交。手动提交又分同步和异步:commitSync():阻塞直到提交成功或失败,可靠但影响吞吐。commitAsync():不阻塞,但失败不会自动重试,可能在重平衡时提交旧位移。我的做法是:正常处理时用commitAsync()提升吞吐,在消费者关闭或重平衡回调里用commitSync()做最终提交。这样既保证了日常性能,又避免了重平衡时的位移回退。4.3 重平衡:消费者组的“集体暂停”重平衡是消费者组最让人头疼的机制。触发条件有三个:组内成员数量变化、订阅的 topic 分区数变化、订阅的 topic 列表变化。一旦触发,所有消费者都会暂停消费,等待协调者重新分配分区。重平衡期间,整个消费者组是不可用的。如果重平衡频繁发生,消费延迟会急剧上升。常见的重平衡诱因包括:消费者处理消息太慢,超过max.poll.interval.ms(默认 5 分钟),被协调者认为已死亡,踢出组。消费者心跳线程正常,但处理线程卡住,导致 poll 间隔超时。网络抖动导致心跳丢失。针对第一个问题,最直接的解决办法是调大max.poll.interval.ms,或者减少每次 poll 返回的消息数量(调小max.poll.records,默认 500)。我一般会把max.poll.records调到 100 到 200,让每次处理的数据量可控,避免单次 poll 处理时间过长。另外,Kafka 2.4 引入了增量协作重平衡协议,相比之前的“全部暂停再分配”,它可以让未受影响的分区继续消费,大幅减少重平衡的影响范围。如果你的版本支持,建议开启partition.assignment.strategy=CooperativeStickyAssignor。注意:从RangeAssignor切换到CooperativeStickyAssignor时,需要先滚动重启所有消费者,否则会报错。这个坑我踩过,当时没看文档直接改配置,结果整个组起不来。5. 消息顺序与重复:那些绕不开的取舍5.1 分区内有序,全局无序Kafka 只保证单个分区内消息有序,不保证 topic 全局有序。这个设计是刻意的,因为全局有序意味着只能有一个分区,并行能力直接归零。如果你的业务需要全局有序,比如某些状态变更事件,那只有两个选择:要么单分区,牺牲吞吐;要么在消费端做排序,但需要额外的缓冲和超时机制。我一般会先问:真的需要全局有序吗?很多场景其实只需要“同一个实体的事件有序”,那用实体 ID 做 key,让同一实体的消息进同一个分区就够了。5.2 顺序与重试的冲突即使在同一分区内,顺序也可能被重试破坏。设想生产者发了消息 A 和 B,A 发送失败触发重试,B 发送成功。如果max.in.flight.requests.per.connection大于 1,B 可能在 A 重试成功之前就写入了,导致顺序变成 B、A。要严格保证顺序,有两个办法:把max.in.flight.requests.per.connection设为 1,但吞吐会下降。开启幂等生产者,Kafka 会保证同一生产者的消息按发送顺序写入,即使有重试。所以幂等生产者不仅解决重复问题,也顺带解决了顺序问题。这也是我建议默认开启幂等的原因之一。5.3 消费端重复的常见来源消费端重复消费的原因主要有三个:位移提交滞后于处理:处理完了但还没提交位移,消费者崩溃,重启后从上次提交的位移重新消费。重平衡导致位移回退:重平衡时,如果位移提交没跟上,新接手的消费者可能从更早的位移开始。生产者重复:前面提到的生产者重试导致的重复。对于消费端重复,最实用的方案是业务幂等。比如用数据库唯一索引、Redis 去重、或者状态机判断。不要指望 Kafka 帮你做到端到端精确一次,那需要事务支持,而且性能开销不小。我自己的经验是:先保证不丢,再考虑不重。因为丢数据往往是不可接受的,而重复数据只要业务能去重,影响就有限。所以位移提交宁可滞后一点,也不要提前提交。6. 常见问题与排查技巧实录6.1 消费者收不到消息的排查清单现象可能原因排查方法消费者启动后无数据订阅的 topic 不对检查subscribe()的 topic 名消费者启动后无数据消费者组位移已到末尾用kafka-consumer-groups.sh查看 LAG消费者启动后无数据分区分配失败查看消费者日志有无Rebalance异常部分分区无数据分区倾斜检查生产者 key 分布消费延迟高处理逻辑慢查看max.poll.interval.ms是否超时我遇到最多的情况是位移已到末尾。尤其是新消费者组第一次启动,如果auto.offset.reset设成了latest,那它只会消费启动之后产生的消息,之前的全部跳过。调试阶段建议设成earliest,上线后再根据业务决定。6.2 生产者发送超时的排查思路生产者报TimeoutException,通常不是网络问题,而是 broker 端处理不过来。排查顺序:看 broker 的request.handler.avg.idle.percent,如果很低,说明请求处理线程忙不过来。看network.threads和io.threads配置,默认分别是 3 和 8,高吞吐场景需要调大。看log.flush.interval.messages,如果设成了 1,每条消息都刷盘,性能会极差。看replica.lag.time.max.ms,如果 ISR 频繁收缩,acks=all的写入会经常超时。我一般会把num.io.threads调到 CPU 核数,num.network.threads调到 CPU 核数的一半。刷盘策略用默认的“由操作系统决定”,不要手动强制刷盘。6.3 重平衡频繁触发的解决步骤重平衡频繁,先看消费者日志里的Rebalance原因。如果是Member consumer-xxx has left the group,说明消费者主动离开,通常是max.poll.interval.ms超时。解决步骤:调大max.poll.interval.ms,比如从 5 分钟调到 10 分钟。调小max.poll.records,比如从 500 调到 100。检查处理逻辑有没有阻塞操作,比如同步 HTTP 调用、大事务。如果用了CooperativeStickyAssignor,确认所有消费者版本一致。我踩过的一个坑是:消费者里调了一个外部接口,那个接口偶尔会卡 30 秒,导致 poll 间隔直接超时。后来把外部调用改成了异步,问题就消失了。所以消费者处理逻辑里千万不要有不可控的阻塞。7. 我个人的一些配置习惯与体会说了这么多原理和排查,最后分享几个我平时配 Kafka 的习惯,都是踩坑之后固化下来的。生产者端:默认开启enable.idempotence=true,acks=all,retries保持默认。linger.ms根据业务定,实时类 0 到 5,吞吐类 20 到 50。batch.size跟着linger.ms走,一般不单独调。compression.type用lz4,压缩率和 CPU 开销比较平衡。消费者端:enable.auto.commit=false,手动提交。max.poll.records设 100 到 200。max.poll.interval.ms根据单批处理时间乘以 3 来定。session.timeout.ms保持默认 10 秒,不要调太小,否则网络抖动就触发重平衡。partition.assignment.strategy用CooperativeStickyAssignor。Broker 端:num.partitions默认值改成 6,避免创建 topic 时忘记指定。default.replication.factor设 3。min.insync.replicas设 2。log.retention.hours根据磁盘容量算,一般 72 到 168 小时。log.segment.bytes保持默认 1GB,不要调太小,否则 segment 文件太多。还有一个容易被忽略的点:topic 的创建最好走运维流程,不要让业务代码自动创建。自动创建出来的 topic 分区数和副本因子都是默认值,很可能不符合业务需求。我见过一个线上事故,就是因为业务代码自动创建了一个单副本的 topic,broker 一重启数据全丢了。Kafka 的基础入门,难点从来不是 API 怎么调,而是这些参数背后的取舍和边界条件。把分区、副本、acks、位移、重平衡这几个概念吃透,再上手生产环境,心里就有底了。后面如果要做 exactly-once 或者跨集群同步,那又是另一个层面的问题了,有机会再聊。
1. 从“能跑起来”到“跑得明白”:Kafka 基础入门的第二道坎很多人第一次接触 Kafka,都是照着官方 Quickstart 敲一遍命令行,看到生产者发消息、消费者收到消息,就觉得“我会了”。但真到了项目里,面对一堆server.properties参数、acks配置、分区策略、消费者组重平衡,立刻又懵了。这个“入门(二)”要解决的,恰恰是从“能跑起来”到“跑得明白”之间的那道坎。我自己带过不少新人,也踩过不少坑。最典型的一个场景:某次压测,生产者端日志显示消息发送成功,消费者端却死活收不到数据。排查了大半天,最后发现是消费者组里有两个实例用了同一个group.id,但订阅的 topic 不一样,导致分区分配错乱。这种问题,光看 Quickstart 是永远遇不到的。所以这篇内容,我打算把 Kafka 入门阶段最容易含糊的几个核心概念——主题与分区、生产者可靠性、消费者组与位移管理、消息顺序与重复——掰开揉碎讲清楚。每个点都会配上我实际用过的配置、参数计算过程,以及踩坑之后的经验总结。适合已经跑通过 Hello World、但一上生产就心里没底的开发者,也适合想系统梳理 Kafka 基础知识的同学。2. 主题与分区:Kafka 并行处理的地基2.1 为什么分区数不是拍脑袋定的Kafka 的 topic 只是一个逻辑概念,真正干活的是 partition。每个 partition 是一个有序的、不可变的消息序列,物理上对应一个目录下的 segment 文件。分区带来的最大好处是并行:生产端可以同时往多个分区写,消费端也可以多个实例同时读。但分区数定多少,很多人是随手写个 3 或者 6。我见过一个团队,业务量不大,却给每个 topic 开了 30 个分区,结果 broker 上文件句柄暴涨,集群时不时抖动。分区数不是越多越好,它直接关联到几个成本:文件句柄:每个 partition 在 broker 上至少对应一个活跃 segment 文件和一个索引文件,分区越多,打开的文件越多。端到端延迟:Kafka 的副本同步是按分区进行的,分区多了,leader 需要维护的 follower 拉取请求也多,极端情况下会影响延迟。重平衡时间:消费者组重平衡时,分区分配是基本单位,分区越多,重平衡耗时越长。那怎么算?一个常用的经验公式是:分区数 = max(目标吞吐量 / 单分区吞吐量, 消费者并行度)单分区吞吐量在普通机械盘上大约 10MB/s 到 50MB/s,SSD 上可以到 100MB/s 以上。假设你的目标写入吞吐是 200MB/s,单分区实测能跑 50MB/s,那至少需要 4 个分区。同时,如果你计划最多用 8 个消费者实例并行消费,那分区数不能少于 8,否则多出来的消费者会闲置。我自己的习惯是:先按吞吐量算一个下限,再按未来半年的消费者并行度留一点余量,但绝不超过必要值的两倍。比如算下来需要 6 个,那就定 8 个,而不是 16 个。2.2 分区策略:消息到底进了哪个分区生产者发送消息时,如果没有指定 partition,Kafka 会根据分区器决定消息落到哪个分区。默认的分区器逻辑经历过几次变化,现在主流版本的行为是:如果消息有 key,对 key 做 murmur2 哈希,然后对分区数取模。如果消息没有 key,在 2.4 版本之前是轮询,之后改成了粘性分区策略。粘性分区的意思是:在一条批次填满或者 linger.ms 到期之前,生产者会一直往同一个分区发,填满后再换下一个。这样做的好处是减少小批次,提升吞吐。但这也带来一个副作用:没有 key 的消息,在短时间内会集中在一个分区。如果你依赖“无 key 消息均匀分布”来做负载均衡,可能会失望。我实测过一个场景:单生产者、无 key、每秒 1000 条消息,linger.ms 设为 5ms。结果在 5ms 窗口内,所有消息都进了同一个分区,下一个窗口才换。从宏观上看是均匀的,但微观上是有倾斜的。如果你的消费端处理逻辑对分区倾斜敏感,要么给消息加上合理的 key,要么调整 linger.ms 和 batch.size 让批次更小更频繁。注意:给消息加 key 时,一定要选基数足够大的字段。我见过用“性别”做 key 的,结果只有两个分区有数据,另外几个分区完全空闲,消费端并行度直接废掉。2.3 副本与 ISR:数据不丢的底层保障每个 partition 可以有多个副本,其中一个是 leader,其余是 follower。生产者和消费者只跟 leader 交互,follower 只负责从 leader 拉数据做备份。ISR 是“in-sync replica”的缩写,意思是跟 leader 保持同步的副本集合。这里有个关键点:只有 ISR 里的副本才有资格被选为新 leader。如果一个 follower 落后太多,它会被踢出 ISR。落后多少算太多?由replica.lag.time.max.ms控制,默认 30 秒。也就是说,如果一个 follower 超过 30 秒没有追上 leader 的进度,就会被移出 ISR。这个参数直接关系到可用性和一致性。调大了,ISR 里可能包含很落后的副本,leader 切换时容易丢数据;调小了,网络抖动一下副本就被踢出去,ISR 频繁收缩,影响写入稳定性。我一般会在内网环境把它设成 10 秒左右,同时配合min.insync.replicas使用。min.insync.replicas的意思是:生产者写入时,至少要有这么多个副本确认收到,才算成功。它和acks=all配合使用。假设副本因子是 3,min.insync.replicas=2,那么只要有两个副本(包括 leader)确认,写入就成功。如果此时一个 follower 挂了,ISR 只剩两个,仍然可以写入;但如果再挂一个,ISR 只剩一个,小于 2,生产者就会收到NotEnoughReplicasException,写入被拒绝。这套组合的意义在于:在保证一定冗余的前提下,尽量维持可用性。如果设成min.insync.replicas=3,那任何一个副本挂掉都会导致写入不可用,对大多数业务来说太严苛了。3. 生产者可靠性:acks 与重试的取舍3.1 acks 三个值的真实含义acks参数控制生产者在什么条件下认为一条消息发送成功。它有三个取值:acks 值含义可靠性吞吐0生产者发出消息就认为成功,不等 broker 确认最低,broker 挂了就丢最高1leader 写入本地日志就确认,不等 follower中等,leader 挂了可能丢中等all (-1)ISR 中所有副本都确认才成功最高最低很多人以为acks=all就万无一失了,其实不然。它只保证“ISR 里的副本都收到了”,但如果 ISR 只剩 leader 一个,那acks=all就退化成acks=1。所以acks=all必须和min.insync.replicas一起用才有意义。我自己的配置习惯是:核心业务用acks=all+min.insync.replicas=2+ 副本因子 3;日志类、监控类数据用acks=1,追求吞吐;极少数允许丢数据的场景才用acks=0。3.2 重试与幂等:重复消息从哪来生产者发送失败后会重试,由retries参数控制,默认是Integer.MAX_VALUE。重试本身是好事,但它会带来重复消息的问题。设想一个场景:生产者发送消息,broker 写入成功,但确认响应在网络中丢失了。生产者以为失败,触发重试,broker 又写入一条。结果就是同一条消息在 Kafka 里出现了两次。解决这个问题的标准方案是开启幂等生产者,配置enable.idempotence=true。它的原理是给每个生产者分配一个 PID,并为每条消息附加一个序列号。broker 会缓存每个 PID 最近几个批次的序列号,如果收到重复序列号,就直接丢弃。开启幂等后,有几个参数会被自动调整:acks强制为all,retries大于 0,max.in.flight.requests.per.connection不超过 5。最后这个参数很关键,它控制生产者在收到确认之前最多能发多少个请求。如果设成 1,吞吐会受影响;设成大于 5,幂等性可能被破坏。所以 Kafka 自动把它限制在 5 以内。实操心得:幂等生产者只能保证单个生产者会话内不重复。如果生产者重启,PID 会变,之前的序列号缓存失效,跨会话的重复仍然可能发生。要做到端到端精确一次,还需要事务或者消费端去重。3.3 批次与延迟的平衡生产者不是来一条消息就发一条,而是攒成批次再发。两个参数决定批次行为:batch.size:一个批次的最大字节数,默认 16KB。linger.ms:批次等待时间,默认 0。linger.ms=0意味着生产者会尽可能快地发送,哪怕批次里只有一条消息。这样延迟最低,但吞吐也最低。把linger.ms调到 5 到 100 毫秒之间,可以让更多消息攒进一个批次,显著提升吞吐,代价是增加了一点延迟。我做过一组对比测试,单分区、消息大小 1KB、目标吞吐 50MB/s:linger.msbatch.size实际吞吐平均延迟016KB12MB/s2ms532KB38MB/s8ms2064KB52MB/s25ms50128KB58MB/s60ms可以看到,linger.ms从 0 调到 5,吞吐翻了 3 倍,延迟只增加了 6ms。这个 trade-off 对大多数业务都是划算的。但如果你做的是实时风控、在线交易这类延迟敏感场景,那linger.ms还是保持 0 或者 1 比较稳妥。另外,batch.size不是越大越好。批次太大会占用更多内存,而且如果消息产生速率低,批次迟迟填不满,反而增加延迟。我一般会把batch.size和linger.ms配合调:高吞吐场景用 64KB + 20ms,低延迟场景用 16KB + 0ms。4. 消费者组与位移管理:并行消费的核心机制4.1 消费者组到底解决了什么问题消费者组是 Kafka 实现单播与广播统一的关键设计。同一个组内的消费者,共同消费一个 topic 的所有分区,每个分区只被组内一个消费者消费;不同组的消费者互不影响,可以各自消费全量数据。这个模型解决了一个很实际的问题:消费能力水平扩展。当单个消费者处理不过来时,直接往同一个组里加实例就行,Kafka 会自动把分区重新分配。但这里有个硬约束:组内消费者数量不能超过分区数,多出来的消费者会处于空闲状态,白白占用资源。我见过一个团队,消费者实例开了 20 个,但 topic 只有 8 个分区,结果 12 个实例全程摸鱼。他们还以为是消费逻辑慢,查了半天才发现是分区数不够。所以前面讲分区数计算时,一定要把消费者并行度考虑进去。4.2 位移提交:自动还是手动消费者消费到哪了,靠的是位移(offset)。位移提交有两种方式:自动提交:enable.auto.commit=true,消费者每隔auto.commit.interval.ms(默认 5 秒)自动提交一次当前位移。手动提交:enable.auto.commit=false,由业务代码在合适的时候调用commitSync()或commitAsync()。自动提交看起来省事,但有个致命问题:它提交的是“拉取到的位移”,而不是“处理完的位移”。假设消费者拉了一批消息,自动提交把位移推进了,但业务处理到一半崩溃了,重启后从新位移开始消费,中间那批没处理完的消息就丢了。所以只要业务对数据完整性有要求,就必须用手动提交。手动提交又分同步和异步:commitSync():阻塞直到提交成功或失败,可靠但影响吞吐。commitAsync():不阻塞,但失败不会自动重试,可能在重平衡时提交旧位移。我的做法是:正常处理时用commitAsync()提升吞吐,在消费者关闭或重平衡回调里用commitSync()做最终提交。这样既保证了日常性能,又避免了重平衡时的位移回退。4.3 重平衡:消费者组的“集体暂停”重平衡是消费者组最让人头疼的机制。触发条件有三个:组内成员数量变化、订阅的 topic 分区数变化、订阅的 topic 列表变化。一旦触发,所有消费者都会暂停消费,等待协调者重新分配分区。重平衡期间,整个消费者组是不可用的。如果重平衡频繁发生,消费延迟会急剧上升。常见的重平衡诱因包括:消费者处理消息太慢,超过max.poll.interval.ms(默认 5 分钟),被协调者认为已死亡,踢出组。消费者心跳线程正常,但处理线程卡住,导致 poll 间隔超时。网络抖动导致心跳丢失。针对第一个问题,最直接的解决办法是调大max.poll.interval.ms,或者减少每次 poll 返回的消息数量(调小max.poll.records,默认 500)。我一般会把max.poll.records调到 100 到 200,让每次处理的数据量可控,避免单次 poll 处理时间过长。另外,Kafka 2.4 引入了增量协作重平衡协议,相比之前的“全部暂停再分配”,它可以让未受影响的分区继续消费,大幅减少重平衡的影响范围。如果你的版本支持,建议开启partition.assignment.strategy=CooperativeStickyAssignor。注意:从RangeAssignor切换到CooperativeStickyAssignor时,需要先滚动重启所有消费者,否则会报错。这个坑我踩过,当时没看文档直接改配置,结果整个组起不来。5. 消息顺序与重复:那些绕不开的取舍5.1 分区内有序,全局无序Kafka 只保证单个分区内消息有序,不保证 topic 全局有序。这个设计是刻意的,因为全局有序意味着只能有一个分区,并行能力直接归零。如果你的业务需要全局有序,比如某些状态变更事件,那只有两个选择:要么单分区,牺牲吞吐;要么在消费端做排序,但需要额外的缓冲和超时机制。我一般会先问:真的需要全局有序吗?很多场景其实只需要“同一个实体的事件有序”,那用实体 ID 做 key,让同一实体的消息进同一个分区就够了。5.2 顺序与重试的冲突即使在同一分区内,顺序也可能被重试破坏。设想生产者发了消息 A 和 B,A 发送失败触发重试,B 发送成功。如果max.in.flight.requests.per.connection大于 1,B 可能在 A 重试成功之前就写入了,导致顺序变成 B、A。要严格保证顺序,有两个办法:把max.in.flight.requests.per.connection设为 1,但吞吐会下降。开启幂等生产者,Kafka 会保证同一生产者的消息按发送顺序写入,即使有重试。所以幂等生产者不仅解决重复问题,也顺带解决了顺序问题。这也是我建议默认开启幂等的原因之一。5.3 消费端重复的常见来源消费端重复消费的原因主要有三个:位移提交滞后于处理:处理完了但还没提交位移,消费者崩溃,重启后从上次提交的位移重新消费。重平衡导致位移回退:重平衡时,如果位移提交没跟上,新接手的消费者可能从更早的位移开始。生产者重复:前面提到的生产者重试导致的重复。对于消费端重复,最实用的方案是业务幂等。比如用数据库唯一索引、Redis 去重、或者状态机判断。不要指望 Kafka 帮你做到端到端精确一次,那需要事务支持,而且性能开销不小。我自己的经验是:先保证不丢,再考虑不重。因为丢数据往往是不可接受的,而重复数据只要业务能去重,影响就有限。所以位移提交宁可滞后一点,也不要提前提交。6. 常见问题与排查技巧实录6.1 消费者收不到消息的排查清单现象可能原因排查方法消费者启动后无数据订阅的 topic 不对检查subscribe()的 topic 名消费者启动后无数据消费者组位移已到末尾用kafka-consumer-groups.sh查看 LAG消费者启动后无数据分区分配失败查看消费者日志有无Rebalance异常部分分区无数据分区倾斜检查生产者 key 分布消费延迟高处理逻辑慢查看max.poll.interval.ms是否超时我遇到最多的情况是位移已到末尾。尤其是新消费者组第一次启动,如果auto.offset.reset设成了latest,那它只会消费启动之后产生的消息,之前的全部跳过。调试阶段建议设成earliest,上线后再根据业务决定。6.2 生产者发送超时的排查思路生产者报TimeoutException,通常不是网络问题,而是 broker 端处理不过来。排查顺序:看 broker 的request.handler.avg.idle.percent,如果很低,说明请求处理线程忙不过来。看network.threads和io.threads配置,默认分别是 3 和 8,高吞吐场景需要调大。看log.flush.interval.messages,如果设成了 1,每条消息都刷盘,性能会极差。看replica.lag.time.max.ms,如果 ISR 频繁收缩,acks=all的写入会经常超时。我一般会把num.io.threads调到 CPU 核数,num.network.threads调到 CPU 核数的一半。刷盘策略用默认的“由操作系统决定”,不要手动强制刷盘。6.3 重平衡频繁触发的解决步骤重平衡频繁,先看消费者日志里的Rebalance原因。如果是Member consumer-xxx has left the group,说明消费者主动离开,通常是max.poll.interval.ms超时。解决步骤:调大max.poll.interval.ms,比如从 5 分钟调到 10 分钟。调小max.poll.records,比如从 500 调到 100。检查处理逻辑有没有阻塞操作,比如同步 HTTP 调用、大事务。如果用了CooperativeStickyAssignor,确认所有消费者版本一致。我踩过的一个坑是:消费者里调了一个外部接口,那个接口偶尔会卡 30 秒,导致 poll 间隔直接超时。后来把外部调用改成了异步,问题就消失了。所以消费者处理逻辑里千万不要有不可控的阻塞。7. 我个人的一些配置习惯与体会说了这么多原理和排查,最后分享几个我平时配 Kafka 的习惯,都是踩坑之后固化下来的。生产者端:默认开启enable.idempotence=true,acks=all,retries保持默认。linger.ms根据业务定,实时类 0 到 5,吞吐类 20 到 50。batch.size跟着linger.ms走,一般不单独调。compression.type用lz4,压缩率和 CPU 开销比较平衡。消费者端:enable.auto.commit=false,手动提交。max.poll.records设 100 到 200。max.poll.interval.ms根据单批处理时间乘以 3 来定。session.timeout.ms保持默认 10 秒,不要调太小,否则网络抖动就触发重平衡。partition.assignment.strategy用CooperativeStickyAssignor。Broker 端:num.partitions默认值改成 6,避免创建 topic 时忘记指定。default.replication.factor设 3。min.insync.replicas设 2。log.retention.hours根据磁盘容量算,一般 72 到 168 小时。log.segment.bytes保持默认 1GB,不要调太小,否则 segment 文件太多。还有一个容易被忽略的点:topic 的创建最好走运维流程,不要让业务代码自动创建。自动创建出来的 topic 分区数和副本因子都是默认值,很可能不符合业务需求。我见过一个线上事故,就是因为业务代码自动创建了一个单副本的 topic,broker 一重启数据全丢了。Kafka 的基础入门,难点从来不是 API 怎么调,而是这些参数背后的取舍和边界条件。把分区、副本、acks、位移、重平衡这几个概念吃透,再上手生产环境,心里就有底了。后面如果要做 exactly-once 或者跨集群同步,那又是另一个层面的问题了,有机会再聊。