跳到正文

生产故障处置 ​

一句话:线上故障大多落在三条线上——副本跟不上、消费者分不到或处理不完、磁盘和位点越界;处置顺序是先取证再动手。

适用版本:Kafka 3.9 / 4.0。命令在 bin/ 下执行,假设已有 orders Topic 与 order-worker 组,localhost:9092 换成实际地址。

为什么需要 ​

线上故障的现场只有一次:重启会清掉日志和线程栈,盲目重置位点会直接丢数据。先取证、再判断、后处置,比「先恢复再说」更重要。

核心机制 ​

故障只落在三条线:副本线看 Isr/UnderReplicatedPartitions,消费线看 LAG/成员状态,存储线看 kafka-log-dirs.sh/df。每条线的入口命令见下方场景。

最小上手 ​

bash
bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group order-worker
bin/kafka-log-dirs.sh --bootstrap-server localhost:9092 --describe --topic-list orders

生产动作 ​

场景一:ISR 收缩 ​

现象:kafka-topics.sh --describe 里 Isr 少于 Replicas;JMX UnderReplicatedPartitions 持续大于 0;acks=all 的生产者出现延迟或 NotEnoughReplicasException。

bash
bin/kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic orders | grep -E 'Partition|Isr'

部分版本还支持 --under-replicated-partitions,以 kafka-topics.sh --help 为准。JMX 看 kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions 和 kafka.server:type=ReplicaFetcherManager,name=MaxLag,clientId=Replica;Broker 日志搜 Shrinking ISR、Expanding ISR。

判断:

  • 单个 Follower 掉队:查该 Broker 的磁盘 IO、GC、网络。
  • 所有 Follower 同时掉队:查 Leader 的写入压力、replica.fetch.max.bytes(默认 1 MiB)是否小于单条消息。
  • ISR 缩到小于 min.insync.replicas:该分区已经拒写,这是保护,不是故障本身。
  • Follower 一直在 fetch 但 LEO 追不上:不是「没发请求」,而是复制速度不够;是否掉出 ISR 看 replica.lag.time.max.ms(默认 30s)。

处置:先恢复掉队 Broker 的磁盘/GC/网络,不要先调大 replica.lag.time.max.ms 掩盖问题。单条大消息导致 Follower 永远追不上时,调大 replica.fetch.max.bytes 和 message.max.bytes,再按滚动窗口重启 Follower。Broker 确认无法恢复时,用 kafka-reassign-partitions.sh 把副本迁走再下线。unclean.leader.election.enable=true 只在明确接受丢数据时使用;默认 false 时 ISR 为空分区不可用,这是有意的取舍。

场景二:rebalance 风暴 ​

现象:消费者反复暂停,日志频繁出现 PreparingRebalance / CompletingRebalance;同一批消息被重复消费;--describe 里成员 ID 不断变化。

bash
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group order-worker --members --verbose
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --state --group order-worker

看 MEMBER-ID、CLIENT-ID、#PARTITIONS 是否稳定;消费者日志搜 member has left、Attempt to heartbeat failed、Rebalance。

判断:

  • 滚动发布期间反复重平衡:没有配 group.instance.id,每次实例重启都被当成新成员。
  • 单次 poll() 处理超过 max.poll.interval.ms(默认 300000):成员被踢出组。心跳线程正常也救不了,判据是 poll 间隔。
  • session.timeout.ms(默认 45000)过小,叠加 GC/网络抖动:误判离线。
  • eager 与 cooperative 分配策略混用:会反复重平衡;同一 group.instance.id 被两个实例使用:互相 fencing。

处置:固定实例名/StatefulSet 配 group.instance.id,滚动重启在 session.timeout.ms 内完成。保证 max.poll.records × 单条处理耗时 < max.poll.interval.ms;先调小 max.poll.records,或把处理移到异步线程、poll 线程持续调用 poll()。新项目用 CooperativeStickyAssignor;从 eager 迁移时按官方步骤滚动切换,不要在线混跑。不要靠反复重启消费者「恢复」,重启本身就是一次成员变更。

场景三:消费 lag 持续增长 ​

现象:LAG 越来越大,端到端延迟上升,下游数据变旧。

bash
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group order-worker

# 看成员与分区归属
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group order-worker --members --verbose

列含义:CURRENT-OFFSET(已提交位点)、LOG-END-OFFSET(分区 LEO)、LAG = LOG-END-OFFSET - CURRENT-OFFSET、CONSUMER-ID/HOST/CLIENT-ID(当前成员)。消费者 JMX 看 records-lag-max、fetch-rate、commit-rate;Broker JMX 看 BytesInPerSec。

判断:

  • lag 稳定但不降:消费速率约等于生产速率;持续上升:消费者不足、处理慢、下游阻塞,或消费者已经掉线。
  • 只有少数分区 lag 高:key 倾斜;所有分区都高:整体容量不足。
  • CONSUMER-ID 为空:该分区没有活跃消费者,组可能空转或正在重平衡。
  • 消费者数已经等于分区数:再扩无效,只能扩分区或提升单消费者吞吐。

处置:先把消费者扩到分区数(不超过),再观察 lag 斜率。仍不够时扩分区;扩分区会改变 key 的落点,依赖同 key 有序的业务要先评估。优化处理:批量写下游、异步化、减少每条消息的同步调用;同时确认下游没有限流/慢查询。不要用 --reset-offsets --to-latest 消 lag,那会直接丢未消费数据。

场景四:磁盘写满 ​

现象:Broker 日志出现 No space left on device / IOException;分区变为 offline;生产失败;kafka-log-dirs.sh 显示某目录接近容量上限。

bash
bin/kafka-log-dirs.sh --bootstrap-server localhost:9092 \
  --describe --topic-list orders
df -h /var/lib/kafka/data

# 看实际生效的保留配置(--all 才包含 Broker 默认值)
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
  --describe --all --entity-type topics --entity-name orders

看 JSON 里的 size、offsetLag、isFuture;isFuture=true 说明分区正在重分配,会额外占空间。Broker JMX 看 kafka.log:type=LogFlushStats,name=LogFlushRateAndTimeMs 和磁盘使用率。

判断:

  • 所有分区均匀增长:流量增长或 retention.ms/retention.bytes 配置过大(retention.bytes=-1 表示不限制)。
  • 单个分区异常大:key 倾斜或该 Topic 写入突增。
  • compact Topic 空间不降:min.cleanable.dirty.ratio 未达到,或 Cleaner 因磁盘不足停止。
  • 目录里有非 Kafka 文件/旧日志:需要单独清理,但不能碰活跃 Segment;刚做过分区重分配时,isFuture 副本会临时双倍占用。

处置:先降该 Topic 的 retention.ms 或 retention.bytes,等清理线程执行;清理按整段删除,不会立刻见效。仍不足时扩容磁盘,或用 kafka-reassign-partitions.sh 把分区迁到其他 Broker。清理非 Kafka 文件(旧日志、临时文件、core dump),不要 rm 活跃的 .log/.index。检查副本数是否过多、Segment 是否过小导致文件数爆炸;恢复后确认所有分区 online,再恢复流量,不要在磁盘满时重启 Broker 期待自愈。

场景五:offset 越界 ​

现象:消费者抛 OffsetOutOfRangeException,随后按 auto.offset.reset 跳位;表现为大量重复消费或直接跳过一段数据。

bash
# 分区当前最早/最晚 offset
bin/kafka-get-offsets.sh --bootstrap-server localhost:9092 --topic orders --time -2
bin/kafka-get-offsets.sh --bootstrap-server localhost:9092 --topic orders --time -1

# 组已提交位点
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
  --describe --group order-worker

判断:

  • CURRENT-OFFSET 小于最早 offset:位点对应的数据已被 retention 删除。
  • CURRENT-OFFSET 大于 LEO:位点被错误重置到未来,或 Topic 被删除重建。
  • 组长期没有消费者:位点超过 offsets.retention.minutes(默认 10080,7 天)被清理。
  • 换了 group.id、Topic 重建、分区数变化:都会让旧位点失效。

处置:先确认数据是否还在:用 --time -2 看最早 offset。如果数据已删,只能从 earliest 开始,接受缺口。如果数据还在,按业务决定起点:不能丢就用 --to-earliest 重放,下游必须幂等;可以跳过就用 --to-latest,但要明确记录缺口。重置前必须停掉消费者,组必须处于非活跃状态;先 --dry-run 看影响范围,再 --execute。根因处置:调大 offsets.retention.minutes、避免随意改 group.id、Topic 重建前确认位点策略。不要用重置位点掩盖消费停滞。

边界 ​

  • 重置位点只改变消费起点,不修复根因;重启也不修复 ISR 或磁盘问题。先按上面的场景定位,再决定是否动位点或滚动重启。
  • KRaft 迁移不是滚动重启。3.9 是最后一个支持 ZooKeeper 的版本,4.0 只支持 KRaft。ZK → KRaft 要走 bridge/迁移流程(KIP-866),涉及元数据双写、Controller quorum 和回滚限制;不能把 3.x ZK 集群直接滚成 4.0。
  • 迁移前确认客户端和工具版本,备份元数据;按官方 upgrade 文档逐版本升级,不要跨大版本跳。
  • KIP-848 新消费组协议:3.7 预览、4.0 GA,客户端用 group.protocol=consumer 启用;启用后部分经典消费者参数由 Broker 控制,排障指标也变了,以官方文档为准。

讲清楚 ​

线上排障先分清是哪条线。副本线看 ISR:--describe 里 Isr 少于 Replicas,或 JMX UnderReplicatedPartitions 持续大于 0,说明冗余在丢;先查掉队 Broker 的磁盘、GC、网络,不要先调大 lag 阈值,更不要开 unclean 选举。消费线看 lag 和成员:lag 持续涨先扩消费者到分区数,再看 key 倾斜和下游阻塞;成员反复进出看 max.poll.interval 和静态成员,别靠重启解决。位点越界先确认数据还在不在,再决定 earliest 还是 latest,重置前必须停消费者并 dry-run。

存储线看 kafka-log-dirs.sh 的 size 和 df;磁盘满先降 retention 或迁分区,绝不手工删活跃 Segment。最后记住版本前提:3.9 是最后一个 ZK 版本,4.0 只有 KRaft,迁移不是滚动重启。

把一门技术讲透到既懂也会用