
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/sSSD 上可以到 100MB/s 以上。假设你的目标写入吞吐是 200MB/s单分区实测能跑 50MB/s那至少需要 4 个分区。同时如果你计划最多用 8 个消费者实例并行消费那分区数不能少于 8否则多出来的消费者会闲置。我自己的习惯是先按吞吐量算一个下限再按未来半年的消费者并行度留一点余量但绝不超过必要值的两倍。比如算下来需要 6 个那就定 8 个而不是 16 个。2.2 分区策略消息到底进了哪个分区生产者发送消息时如果没有指定 partitionKafka 会根据分区器决定消息落到哪个分区。默认的分区器逻辑经历过几次变化现在主流版本的行为是如果消息有 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的意思是生产者写入时至少要有这么多个副本确认收到才算成功。它和acksall配合使用。假设副本因子是 3min.insync.replicas2那么只要有两个副本包括 leader确认写入就成功。如果此时一个 follower 挂了ISR 只剩两个仍然可以写入但如果再挂一个ISR 只剩一个小于 2生产者就会收到NotEnoughReplicasException写入被拒绝。这套组合的意义在于在保证一定冗余的前提下尽量维持可用性。如果设成min.insync.replicas3那任何一个副本挂掉都会导致写入不可用对大多数业务来说太严苛了。3. 生产者可靠性acks 与重试的取舍3.1 acks 三个值的真实含义acks参数控制生产者在什么条件下认为一条消息发送成功。它有三个取值acks 值含义可靠性吞吐0生产者发出消息就认为成功不等 broker 确认最低broker 挂了就丢最高1leader 写入本地日志就确认不等 follower中等leader 挂了可能丢中等all (-1)ISR 中所有副本都确认才成功最高最低很多人以为acksall就万无一失了其实不然。它只保证“ISR 里的副本都收到了”但如果 ISR 只剩 leader 一个那acksall就退化成acks1。所以acksall必须和min.insync.replicas一起用才有意义。我自己的配置习惯是核心业务用acksallmin.insync.replicas2 副本因子 3日志类、监控类数据用acks1追求吞吐极少数允许丢数据的场景才用acks0。3.2 重试与幂等重复消息从哪来生产者发送失败后会重试由retries参数控制默认是Integer.MAX_VALUE。重试本身是好事但它会带来重复消息的问题。设想一个场景生产者发送消息broker 写入成功但确认响应在网络中丢失了。生产者以为失败触发重试broker 又写入一条。结果就是同一条消息在 Kafka 里出现了两次。解决这个问题的标准方案是开启幂等生产者配置enable.idempotencetrue。它的原理是给每个生产者分配一个 PID并为每条消息附加一个序列号。broker 会缓存每个 PID 最近几个批次的序列号如果收到重复序列号就直接丢弃。开启幂等后有几个参数会被自动调整acks强制为allretries大于 0max.in.flight.requests.per.connection不超过 5。最后这个参数很关键它控制生产者在收到确认之前最多能发多少个请求。如果设成 1吞吐会受影响设成大于 5幂等性可能被破坏。所以 Kafka 自动把它限制在 5 以内。实操心得幂等生产者只能保证单个生产者会话内不重复。如果生产者重启PID 会变之前的序列号缓存失效跨会话的重复仍然可能发生。要做到端到端精确一次还需要事务或者消费端去重。3.3 批次与延迟的平衡生产者不是来一条消息就发一条而是攒成批次再发。两个参数决定批次行为batch.size一个批次的最大字节数默认 16KB。linger.ms批次等待时间默认 0。linger.ms0意味着生产者会尽可能快地发送哪怕批次里只有一条消息。这样延迟最低但吞吐也最低。把linger.ms调到 5 到 100 毫秒之间可以让更多消息攒进一个批次显著提升吞吐代价是增加了一点延迟。我做过一组对比测试单分区、消息大小 1KB、目标吞吐 50MB/slinger.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.committrue消费者每隔auto.commit.interval.ms默认 5 秒自动提交一次当前位移。手动提交enable.auto.commitfalse由业务代码在合适的时候调用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.strategyCooperativeStickyAssignor。注意从RangeAssignor切换到CooperativeStickyAssignor时需要先滚动重启所有消费者否则会报错。这个坑我踩过当时没看文档直接改配置结果整个组起不来。5. 消息顺序与重复那些绕不开的取舍5.1 分区内有序全局无序Kafka 只保证单个分区内消息有序不保证 topic 全局有序。这个设计是刻意的因为全局有序意味着只能有一个分区并行能力直接归零。如果你的业务需要全局有序比如某些状态变更事件那只有两个选择要么单分区牺牲吞吐要么在消费端做排序但需要额外的缓冲和超时机制。我一般会先问真的需要全局有序吗很多场景其实只需要“同一个实体的事件有序”那用实体 ID 做 key让同一实体的消息进同一个分区就够了。5.2 顺序与重试的冲突即使在同一分区内顺序也可能被重试破坏。设想生产者发了消息 A 和 BA 发送失败触发重试B 发送成功。如果max.in.flight.requests.per.connection大于 1B 可能在 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 频繁收缩acksall的写入会经常超时。我一般会把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.idempotencetrueacksallretries保持默认。linger.ms根据业务定实时类 0 到 5吞吐类 20 到 50。batch.size跟着linger.ms走一般不单独调。compression.type用lz4压缩率和 CPU 开销比较平衡。消费者端enable.auto.commitfalse手动提交。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 分区数和副本因子都是默认值很可能不符合业务需求。我见过一个线上事故就是因为业务代码自动创建了一个单副本的 topicbroker 一重启数据全丢了。Kafka 的基础入门难点从来不是 API 怎么调而是这些参数背后的取舍和边界条件。把分区、副本、acks、位移、重平衡这几个概念吃透再上手生产环境心里就有底了。后面如果要做 exactly-once 或者跨集群同步那又是另一个层面的问题了有机会再聊。