跳到主要内容

Kafka 故障排查与最佳实践

先分类,再排查​

Kafka 的故障看起来五花八门,落到现象上其实就四类:副本不健康、消费跟不上、磁盘不够、成员反复抖动。先把现象归到某一类,再顺着固定的路径往下挖,比漫无目的地翻日志高效得多。

一个通用的判断入口:

业务说"消息延迟/收不到"
→ 先查 lag:是堆积还是根本没生产?
├─ 有生产、lag 涨 → 消费侧问题(第 2 节、第 4 节)
├─ 没生产 → 生产侧问题(查 Broker 可用性、ISR)
└─ lag 正常但业务报错 → 业务逻辑或下游,不是 Kafka

Broker 报警
→ UnderReplicatedPartitions > 0 → 副本问题(第 1 节)
→ 磁盘水位高 → 空间问题(第 3 节)

ISR 收缩与副本不同步​

症状​

三处能看到同一个事实:

# 1. 有多少分区副本不足
bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe \
--under-replicated-partitions

# 2. Broker 日志里反复出现
# Shrinking ISR for partition order-created-3 from (1,2,3) to (1,2)
# Expanding ISR for partition order-created-3 from (1,2) to (1,2,3)

# 3. 生产端报错(ISR 掉到 min.insync.replicas 以下时)
# org.apache.kafka.common.errors.NotEnoughReplicasException

如果只是偶尔收缩又立刻恢复,通常不用管。持续收缩、或者 ISR 长期少一个,就必须查了——此时集群实际上已经在「少一份备份」的状态下运行,再挂一台就丢数据。

五类根因​

根因现场特征验证方式
磁盘慢单盘 await 高、util 接近 100%iostat -x 1,看 await 是否大于 50ms
网络问题跨机架/跨可用区流量大、TCP 重传增长node_netstat_Tcp_RetransSegs 持续上涨
GC 停顿停顿期间心跳和拉取全断,之后又恢复GC 日志里单次 pause 超过 1 秒
Broker 过载IO 线程空闲率低、请求排队RequestHandlerAvgIdlePercent 低于 0.3
副本拉取配置太小大消息 Topic 的 Follower 一直追不上replica.fetch.max.bytes 小于单条消息大小

逐个看​

磁盘慢是最常见的原因。Kafka 写入虽然顺序,但 Follower 拉取和消费者读取是随机的,机械盘在高并发下 await 很容易飙到几百毫秒。iostat -x 1 里三个值重点看:await(单次 IO 平均耗时)、%util(设备繁忙度)、aqu-sz(排队深度)。await 超过 50ms 就说明这块盘已经不适合放 Kafka 数据了;云盘还要额外看 IOPS 和吞吐是否被打到配额上限——很多云盘的「突发性能」用完就会掉到基础性能,表现就是白天好好的、晚上突然 ISR 收缩。

网络问题往往和部署形态有关。跨可用区部署时,副本同步的流量要走专线或公网,带宽打满、丢包、抖动都会让 Follower 追不上。查 TCP 重传率比看带宽更有效:重传持续增长说明链路质量有问题,不是容量问题。

GC 停顿是隐蔽杀手。Broker 用 G1 时推荐堆 6GB 到 8GB,堆太大反而单次 Full GC 更久。停顿 2 秒以上,Follower 就错过了心跳和拉取窗口,直接掉出 ISR。这类故障的特征是「周期性」——每隔一段时间收缩一次,和 GC 日志的时间点能对上。

Broker 过载看两个空闲率指标:RequestHandlerAvgIdlePercent 低于 0.3 说明 IO 线程忙不过来,NetworkProcessorAvgIdlePercent 低说明网络线程是瓶颈。前者加 num.io.threads(磁盘数的 2 到 3 倍),后者加 num.network.threads(CPU 核数)。但注意这只能缓解,根因通常是流量真的超过了机器能力。

副本拉取配置容易被忽略:replica.fetch.max.bytes 默认 1MB,如果 Topic 里有 5MB 的大消息,Follower 永远拉不到完整的一批,就会陷入「拉取、失败、重试」的循环。大消息场景要同时调大 replica.fetch.max.bytes、message.max.bytes 和消费者的 fetch.max.bytes。

处置顺序​

  1. 先止血:确认是单节点问题就把该节点摘掉(kafka-server-stop.sh 停掉,让 Leader 自动切走),别让一个坏节点拖着整个 ISR 反复抖动。
  2. 定位到具体分区:--describe --under-replicated-partitions 看重灾区集中在哪个 Broker,集中在一台就是那台的问题。
  3. 修根因:换盘、加带宽、调 GC、降流量,四选一,别指望调参数糊过去。
  4. 重建副本(磁盘坏了、数据目录损坏时):用 kafka-reassign-partitions.sh 把受影响的分区迁到健康节点,等同于一键重建副本。
不要靠调大 replica.lag.time.max.ms 解决

把超时从 30 秒调到 5 分钟,看起来 ISR 不收缩了,实际上只是把「副本落后」藏起来:数据仍然没同步,故障时照样丢。这个参数只在「网络偶发抖动、确认是瞬时问题」时才值得临时放宽。

消息堆积​

lag 上涨是最常见的告警,但「lag 涨了」本身不构成结论,得先分清是生产变多还是消费变慢。

三步定位​

第一步:看生产速率和消费速率。

生产速率(BytesInPerSec / MessagesInPerSec)明显高于平时  → 生产激增
生产速率正常,消费速率下降 → 消费变慢
两者都正常但 lag 不降 → 位点提交有问题,或消费者卡死

第二步:看 lag 是不是均匀分布在所有分区。

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--describe --group order-service
  • 所有分区 lag 均匀上涨:整体消费能力不足,要么扩容消费者,要么优化单条处理耗时。
  • 少数分区 lag 特别高:分区倾斜。多半是 key 设计有问题(比如按省份分 key,广东的数据量是别处的十倍),或者有个别分区所在的 Broker 有问题。

第三步:看消费者实例是不是打满了。

消费者数量已经等于分区数,lag 还在涨,说明加实例没用,瓶颈在单条处理逻辑或下游。这时候扩消费者是自欺欺人。

处置手段​

判据结论动作
生产激增,消费正常上游流量问题限流上游、临时扩容消费者、必要时扩分区
消费慢,实例数小于分区数并行度不够扩消费者到分区数
消费慢,实例数等于分区数单实例处理慢优化业务逻辑、批量处理、异步化下游调用
单个实例卡住,其他正常该实例故障重启实例,检查 GC 和下游超时
分区 lag 严重倾斜key 分布不均改 key 设计,或单独为热 key 建 Topic

扩容消费者有两个硬约束必须记住:

  • 消费者数不能超过分区数,超出的实例分不到分区,纯属浪费资源。
  • 扩分区会破坏 key 的顺序性(哈希取模的除数变了)。有顺序要求的 Topic,宁可优化消费逻辑也不要扩分区。

临时救火还有几招,但都有代价:

  • 调大 max.poll.records(比如 500 到 2000):减少 poll 次数,提升吞吐,但单批处理时间变长,可能触发 max.poll.interval.ms。
  • 把「逐条同步处理」改成「批量处理」或「异步提交下游」:见效最快,但要评估下游承压能力。
  • 丢弃非核心消息:只在极端情况下用,且要有明确的业务确认。
追平时间比 lag 数量更有意义

lag 100 万条,如果消费速率是 5 万条/秒,200 秒就追平了,不用慌;lag 1 万条但消费速率接近 0,那才是真出事。告警里加上「lag 除以消费速率」的推导,能大幅减少误报。

磁盘写满​

症状​

kafka.log.LogManager: Error while writing to ...
java.io.IOException: No space left on device

Broker 不会因为磁盘满而「优雅停止」,它会持续报错,写入失败、副本落后、ISR 收缩,最后连元数据和位点都写不进去。这是最该提前防、而不是事后救的故障。

紧急处置(按生效速度排序)​

手段生效速度代价
缩短 Topic 的 retention.ms分钟级(等下一次检查)数据提前过期,需业务确认
降低 log.retention.check.interval.ms立即加速检查频繁扫描,轻微增加 IO
kafka-delete-records.sh 按 offset 删秒级精确但危险,容易删多
迁移分区到其他 Broker分钟到小时产生大量网络流量
加盘 / 扩节点小时级需要停机或重分配

具体操作:

# 1. 临时把保留时间压到 6 小时,等段删除释放空间
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type topics --entity-name user-behavior-log \
--add-config retention.ms=21600000

# 2. 加速过期检查(Broker 级动态配置)
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
--alter --entity-type brokers --entity-name 1 \
--add-config log.retention.check.interval.ms=60000

# 3. 还不行就按 offset 精确删(慎用,删了就回不来)
cat > offsets.json <<'EOF'
{"partitions":[{"topic":"user-behavior-log","partition":0,"offset":5000000}],"version":1}
EOF
bin/kafka-delete-records.sh --bootstrap-server localhost:9092 \
--offset-json-file offsets.json

三个不要​

  • 不要直接 rm 日志文件。Kafka 的内存索引和磁盘文件是对应的,手工删文件会导致 OffsetOutOfRange、索引损坏,甚至 Broker 起不来。真要删,只能停掉 Broker 删整个分区目录。
  • 不要设置 unclean.leader.election.enable=true 来「恢复服务」。磁盘满导致的副本落后,靠这个开关切过去只会丢掉更多数据。
  • 不要只盯着「磁盘使用率」这个数字。前面说过,日志是锯齿曲线,关键看「写入速率 × 剩余空间」还能撑多久。

长期措施​

# 给每个 Topic 加大小上限,时间和大小的先到先删
log.retention.bytes=214748364800
# 单段 1GB,保证过期删除的粒度不会太粗
log.segment.bytes=1073741824
# 磁盘水位 80% 告警,留出处置时间

再配三条纪律:日志型 Topic 必须显式设 retention.ms(别依赖 Broker 默认值);数据目录独占磁盘,不和系统盘、日志盘混用;每季度做一次容量评审,按业务增长重算保留天数。

消费者组 rebalance 频繁​

症状​

消费者日志里反复出现:

Revoking previously assigned partitions [...]
(Re-)joining group order-service
Successfully joined group with generation 42

日志里 generation 数字快速上涨,同时消费速率出现规律性的「停一下、冲一下」——每次 rebalance 都要重新分配分区,期间整个组停摆(协议决定的,不是配置能消除的)。

三类原因​

1. 心跳断了。 session.timeout.ms(默认 45 秒)内没收到心跳,协调器就把成员踢出组。原因通常是消费者 GC 停顿、网络抖动、或者处理线程把心跳线程饿死。

2. 处理太慢。 两次 poll() 的间隔超过 max.poll.interval.ms(默认 5 分钟),成员会被主动踢出组。这是「消费逻辑慢」引发的 rebalance,和心跳无关,很多人会误判。

3. 成员频繁上下线。 滚动发布、K8s 驱逐、OOMKilled、健康检查失败,都会让实例反复退出再加入。每次成员变化都要 rebalance 一次。

关键配置​

# 会话超时:太短容易误踢,太长故障发现慢;建议 25 到 45 秒
session.timeout.ms=30000
# 心跳间隔,一般是会话超时的 1/3
heartbeat.interval.ms=3000
# 处理超时:按单批最坏耗时估,别超过 5 分钟
max.poll.interval.ms=300000
# 单次拉取条数:处理慢就调小,让 poll 更频繁
max.poll.records=200
# 增量协作式再均衡,替代全量 Stop-The-World
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
# 新组首次 rebalance 的等待时间,避免实例陆续启动触发多轮
group.initial.rebalance.delay.ms=3000

两个进阶手段:

  • 静态成员(group.instance.id):给每个实例配一个稳定 ID,实例重启后在 session.timeout.ms 内回来,不触发 rebalance。K8s 里配合 StatefulSet 的固定 Pod 名很好用。
  • 增量协作式再均衡:CooperativeStickyAssignor 只回收需要迁移的分区,不再全量 revoke,rebalance 期间大部分分区照常消费。注意滚动升级需要两轮:先把新协议加进列表,等所有实例都升级完,再移除旧协议。

处置清单​

  1. 先确认是「心跳超时」还是「poll 超时」——看日志里的错误信息,两者关键字不同。
  2. 心跳超时查 GC 和网络;poll 超时查业务耗时,尤其是有没有同步调用外部 HTTP 接口。
  3. 处理确实慢就减小 max.poll.records 并把处理逻辑批量化,比盲目调大超时更治本。
  4. 实例频繁重启先解决重启本身(OOM、探针、驱逐),再考虑静态成员。

最佳实践清单​

Topic 设计

  • 命名带业务域前缀,如 order-created、user-behavior-log,别用 test1。
  • 分区数按「吞吐、并行度、顺序性」三者取交集,一次定到位;单 Broker 分区数控制在 2000 以内。
  • 副本因子 3,min.insync.replicas=2,unclean.leader.election.enable=false。
  • 关闭 auto.create.topics.enable,所有 Topic 走申请和评审。

生产端

  • acks=all + enable.idempotence=true 是默认姿势。
  • linger.ms 给 5 到 20ms,compression.type=lz4,吞吐立竿见影。
  • 一定要处理发送回调,失败的落本地表或补偿队列。
  • 有顺序要求就用 key,别靠单分区硬扛。

消费端

  • enable.auto.commit=false,处理完再 commitSync。
  • 消费逻辑幂等:至少一次语义下重复不可避免。
  • max.poll.records 和处理耗时匹配,单批处理别超过 max.poll.interval.ms 的一半。
  • 多实例部署用静态成员 + 协作式再均衡。

运维

  • 三层监控齐全,重点告警:URP、离线分区、ISR 收缩、P99 延迟、lag 导数。
  • 磁盘水位 80% 预警,日志型 Topic 必设保留时间。
  • 任何变更(扩分区、改副本、迁移、改清理策略)都走低峰窗口,并且限流。
  • 版本升级前确认客户端兼容性,KRaft 集群不要混用 ZooKeeper 时代的配置文件。

小结​

  • 排查先分类:副本不健康、消费跟不上、磁盘不够、成员抖动,四类各有固定路径。
  • ISR 收缩的根因按概率排:磁盘慢、网络抖动、GC 停顿、Broker 过载、拉取配置太小;调大超时只是掩盖问题。
  • 堆积先判断「生产多」还是「消费慢」,再看分区是否倾斜;消费者数超过分区数没有意义,扩分区会破坏 key 顺序。
  • 磁盘满要提前防:retention.bytes 设上限、水位告警、数据目录独占磁盘;应急时先缩保留时间,再考虑精确删,绝不手删文件。
  • rebalance 频繁分清 session.timeout.ms(心跳)和 max.poll.interval.ms(处理),配静态成员和协作式再均衡能显著减少抖动。
  • 最佳实践的核心就一句:可靠性参数别省,容量和分区一次算准,变更永远挑低峰。

六篇正文到此结束。命令记不全的翻附录《Kafka 命令速查》,想看官方原始口径的翻《参考资料》,概念模糊了回头再看核心概念与架构。