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。
处置顺序
- 先止血:确认是单节点问题就把该节点摘掉(
kafka-server-stop.sh停掉,让 Leader 自动切走),别让一个坏节点拖着整个 ISR 反复抖动。 - 定位到具体分区:
--describe --under-replicated-partitions看重灾区集中在哪个 Broker,集中在一台就是那台的问题。 - 修根因:换盘、加带宽、调 GC、降流量,四选一,别指望调参数糊过去。
- 重建副本(磁盘坏了、数据目录损坏时):用
kafka-reassign-partitions.sh把受影响的分区迁到健康节点,等同于一键重建副本。
把超时从 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 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 期间大部分分区照常消费。注意滚动升级需要两轮:先把新协议加进列表,等所有实例都升级完,再移除旧协议。
处置清单
- 先确认是「心跳超时」还是「poll 超时」——看日志里的错误信息,两者关键字不同。
- 心跳超时查 GC 和网络;poll 超时查业务耗时,尤其是有没有同步调用外部 HTTP 接口。
- 处理确实慢就减小
max.poll.records并把处理逻辑批量化,比盲目调大超时更治本。 - 实例频繁重启先解决重启本身(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 命令速查》,想看官方原始口径的翻《参考资料》,概念模糊了回头再看核心概念与架构。