主题
生产故障处置
一句话:线上故障大多落在三条线上——副本跟不上、消费者分不到或处理不完、磁盘和位点越界;处置顺序是先取证再动手。
适用版本: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,迁移不是滚动重启。