一个诡异的故障
SpringBoot 服务订阅的 Kafka Topic 收不到数据了。不是偶尔延迟,是持续收不到。更诡异的是,服务端没有任何报错,消费者像睡着了。
消费组 russian-gas-regulation-group1 订阅的多个 Topic(vehicle_real_time_info、alarm_event 等)全部静默。
这篇文章复盘这次 P1 故障:数据链路中断,历史数据丢失。重点是为什么”无报错”,以及根因如何一步步定位到”元数据错位”。
影响范围
| 项目 | 影响 |
|---|---|
| 消费者组 | russian-gas-regulation-group1 等 5 个成员无法消费 |
__consumer_offsets | 故障态(修复前)50 分区全部 Replicas: 9、Leader: none,group coordinator 不可用;修复后重建为 Replicas: 0、Leader: 0 |
| 业务 Topic | 共 8 个全部 Leader: none |
| 数据损失 | 8 个业务 Topic 的历史数据永久丢失(broker 9 磁盘已不存在) |
时间线
| 时间 | 事件 |
|---|---|
| 8/26 之前 | 旧 Kafka 集群(broker 9)正常运行,所有 Topic 建在 broker 9 上 |
| 8/26 15:44~16:54 | ZK 快照固化 broker 9 的 Topic 副本元数据 |
| 8/26 17:03 | 重装 Kafka,broker.id 配为 0,复用旧 ZK → 所有 Topic 因 broker 9 已消失而 Offline |
| 8/27 08:59 | 首次尝试手动迁移 event.helix(副本 9→0),失败 |
| 8/28 10:34 | event.helix 迁移成功(Leader=0) |
| 8/28 10:51~11:15 | 迁移 alarm 系列 4 个 Topic,数据丢失(offset 从 0 起) |
| 8/28 11:19 | broker 重启,alarm 系列被删除重建(Topic ID 变更) |
| 8/28 12:00~12:26 | 排查定位:确认幽灵 broker 9、__consumer_offsets 副本错位 |
| 8/28 12:26 | 误操作①:在 broker 运行中直接 deleteall /brokers/topics/* 手删 ZK 节点 → 删不掉(controller 立即重建) |
| 8/28 12:37 | __consumer_offsets 已重建为健康态(50 分区 Leader: 0),但 describeConsumerGroups 仍超时 |
| 8/28 12:40 | 误操作②:在 broker 运行中又 deleteall 了健康的 __consumer_offsets 元数据 → 内存/ZK/磁盘三方不一致,报 Timed out waiting for a node assignment |
| 8/28 12:40~12:49 | 停应用 → 停 broker → 清理磁盘残留 → 干净重启,三方状态重新一致 |
| 8/28 12:49 | 消费者组稳定(5 members, generation 2),恢复正常 |
前置知识:Group Coordinator 与 __consumer_offsets
Kafka 核心角色
| 组件 | 职责 |
|---|---|
| Broker | 存储分区数据、处理读写请求的服务节点 |
| Topic / Partition | Topic 是逻辑分类,Partition 是物理分片;每个分区有多个副本(Replica),Leader 负责读写,Follower 同步备份 |
| Controller | 某个 broker 兼任的”管理员”,负责分区 Leader 选举、副本管理、元数据变更 |
| Group Coordinator | 消费者组的管理者:组成员管理(JoinGroup)、分区分配(rebalance)、offset 提交管理 |
__consumer_offsets | 内部 Topic(默认 50 分区),专门存储各消费者组的 offset 提交记录和组元数据 |
消费者消费任何 Topic 的前提:先”入组”
消费者不能直接读分区,必须先经过 Group Coordinator:
1. 消费者启动 → FIND_COORDINATOR 找到本组对应的 coordinator2. 向 coordinator 发 JoinGroup → coordinator 触发 rebalance,给每个成员分配分区3. 分配完成后,消费者才知道自己该读哪些分区,才开始拉取消息4. 消费过程中定期 OffsetCommit,coordinator 把 offset 写入 __consumer_offsets一个 group 的 coordinator 是谁,由 __consumer_offsets 决定:
groupId → hash → __consumer_offsets 的某个分区 → 该分区 Leader 所在的 broker 就是 coordinator所以 Group Coordinator 的生命线就是 __consumer_offsets 的分区 Leader。
为什么 coordinator down 会拖垮业务 Topic
这是本故障最反直觉的地方,也是”无报错”的根源:
- 业务 Topic 的数据、分区 Leader 可能完全正常,但消费者依然收不到。
- 因为消费者卡在”入组”这一步:coordinator 不可用 → 无法 JoinGroup → 拿不到分区分配 → 永远不会开始拉取消息。
- 类比:餐馆正常营业、菜品齐全,但发号叫号的前台坏了,客人排不上队、进不了店。
- “无报错”是因为消费者只是在后台反复重试
FIND_COORDINATOR,日志里是重试信息而非业务错误,极易被忽略。
本案例的完整因果链
重装 Kafka(broker.id=0)复用旧 ZK ↓ZK 中 __consumer_offsets(及业务 Topic)副本仍指向已消失的 broker 9 ↓controller 只能从"该分区的副本集合"里选 Leader,而副本集合被固化为 [9],broker 9 又不在 /brokers/ids → 选不出 Leader → 分区 Offline ↓__consumer_offsets 50 分区全部 Leader:none ↓Group Coordinator 无法确定(依赖 __consumer_offsets 分区 Leader) ↓消费者 FIND_COORDINATOR 返回哨兵值 2147483647(Int.MaxValue,表示 coordinator 不可用),无法 JoinGroup ↓消费者不开始消费 → 业务 Topic 有数据也收不到 ↓表现:无报错、静默收不到数据根因分析
直接根因
8/26 重装 Kafka 时,broker.id 配置为 0,但复用了旧 ZooKeeper;ZK 中所有 Topic 的副本元数据仍指向已消失的 broker 9,导致这些 Topic 全部 Leader: none,group coordinator 无法选举,消费者静默收不到数据。
证据链
kafka-consumer-groups.sh --describe报FIND_COORDINATOR超时 → coordinator 不可用。__consumer_offsets50 分区全部Leader: none,Replicas: 9,但 ZK/brokers/ids中只有[0]。cluster.id与 broker 0 的meta.properties完全一致 → 排除”接错 ZK”。- broker 0 的
meta.properties创建于 8/26 17:03,且当时log.dirs中仅有event.helix-0一个分区目录 → 证明 broker 0 是 8/26 重装的全新 broker,非 broker 9 改名。 controller.log从 broker 0 启动第一秒(8/26 17:03:41)起持续报Topics not in preferred replica for broker 9→ broker 9 在 broker 0 上线前已不存在。state-change.log中无任何 broker 9 上下线记录 → broker 9 是”旧集群的幽灵”。- 全盘
find /data -name meta.properties仅有一个(broker 0 的)→ broker 9 数据目录已彻底不存在。
关键结论
不是 ZooKeeper 进程故障,也不是 Kafka 进程故障,而是元数据错位(旧 ZK 数据 + 新 broker),属于”重装 Kafka 时未同步处理 ZK 元数据”的运维操作失误。
排查过程
- 分层定位:先用
kafka-console-consumer验证数据链路,确认问题在消费者端。 - 查 coordinator:
kafka-consumer-groups.sh --describe报FIND_COORDINATOR超时,定位到 group coordinator 不可用。 - 查
__consumer_offsets:--describe发现 50 分区全部Leader: none,副本在 broker 9。 - 确认 broker 现状:ZK
/brokers/ids仅[0];cluster.id一致;meta.properties时间戳指向 8/26 重装。 - 查历史痕迹:
controller.log、state-change.log、ZK 事务日志,确认 broker 9 早已消失、且event.helix等 Topic 曾被部分迁移过。 - 确认数据不可恢复:全盘搜索 broker 9 数据目录,确认已不存在。
排查中的弯路(反面教材)
排查过程中一度在 broker 仍在运行的情况下,直接用 zookeeper-shell.sh ... deleteall /brokers/topics/<topic> 手删 ZK 元数据,走了两个弯路:
- 删不掉:删完
ls /brokers/topics节点又在,因为 controller 感知到 ZK 与自身内存状态不一致,立即把节点重建了回来。ZK 里的 topic 节点只是元数据,broker 内存和磁盘日志都还在。 - 越删越乱:进一步把当时已经健康的
__consumer_offsets(50 分区Leader: 0)也deleteall掉,导致 broker 内存 / ZK 元数据 / 磁盘日志三方不一致,kafka-consumer-groups.sh --describe从原来的FIND_COORDINATOR超时恶化为Timed out waiting for a node assignment(AdminClient 拿不到可用节点)。
正确姿势:删 topic 必须走 kafka-topics.sh --delete(需 delete.topic.enable=true);副本错位应走 kafka-reassign-partitions.sh 重分配;只有在必须彻底重建时,才「停应用 → 停 broker → 删 ZK 节点 → 删磁盘日志 → 重启」四步同步做,保证三方一致。
解决方案
前提判断:重分配(保数据)还是删除重建(丢数据)? 副本指向已下线 broker 时,首选
kafka-reassign-partitions.sh把副本从 broker 9 迁到 broker 0,只要旧 broker 磁盘还在,数据不丢(时间线里event.helix那次就是在做迁移)。只有磁盘彻底丢失才被迫删除重建。本次 broker 9 磁盘已不存在,才无奈走了删除重建、接受数据丢失。
- 确认数据不可恢复:全盘搜索确认 broker 9 磁盘已不存在,历史数据无法找回,重分配路线不可行。
- 停应用:先停掉消费/生产应用,避免边清理边被自动创建/写入。
- 清理幽灵元数据:删除 ZK 中
/admin/delete_topics、/brokers/topics、/config/topics下 9 个 Topic 的残留节点。 - 重建业务 Topic:重建 8 个业务 Topic(单分区、RF=1)。
- 干净重启 broker:停 broker → 清理
__consumer_offsets-*磁盘残留(与已删的 ZK 元数据保持一致)→ 重启。broker 启动时按offsets.topic.num.partitions(默认 50)自动重建__consumer_offsets(Leader=0),GroupCoordinator 正确初始化。 - 验证恢复:消费者组成功 join,
kafka-consumer-groups.sh --describe正常返回 LAG。
经验教训
- 重装 Kafka 必须同步处理 ZK 元数据:broker.id 变更后,要么换干净 ZK,要么全量迁移/清理 ZK 中 Topic 副本分配,否则出现”幽灵 broker”。
- ZK 数据目录不能放
/tmp:本案例 ZK 实际 dataDir 在/data/iot/third_party/zk/data(独立目录),但 Kafka 目录下的zookeeper.properties却指向/tmp/zookeeper,配置与实际不符,增加了排查成本。 - “无报错收不到数据”先查 coordinator/offset:不要只看业务日志,先看
kafka-consumer-groups.sh --describe的 LAG 和 coordinator 状态。 - 绝不在运行中的 broker 上手删 ZK 的 topic 节点:ZK 节点只是元数据,broker 内存和磁盘日志才是真身。手删要么被 controller 立即重建(表现为”删不掉”),要么造成内存/ZK/磁盘三方不一致,越删越乱。删 topic 走
kafka-topics.sh --delete;副本错位走kafka-reassign-partitions.sh;必须彻底重建时,“停应用→停 broker→删 ZK→删磁盘→重启”四步同步做。 - 副本指向死 broker 优先重分配而非重建:
kafka-reassign-partitions.sh在旧磁盘尚存时可保数据,删除重建必丢数据,删除重建应是最后手段。 - 运维操作必须留档:本案例的 Topic 迁移/删除重建无任何记录,导致”没人承认”、排查时需从日志反推时间线。
- 关键数据需备份:broker 9 数据丢失说明缺少 Kafka 数据备份/迁移预案。
改进措施(Action Items)
| 序号 | 措施 | 责任人 | 优先级 |
|---|---|---|---|
| 1 | 建立 Kafka 重装/迁移 SOP:变更 broker.id 前必须先处理 ZK 元数据 | 运维 | 高 |
| 2 | 统一 ZK 配置,杜绝 /tmp 与独立 dataDir 混用 | 运维 | 高 |
| 3 | 为 Kafka 业务 Topic 建立数据备份/多副本策略(生产环境 RF≥3,需先将集群扩容到 ≥3 broker,非单机改配置即可) | 架构 | 高 |
| 4 | 建立运维操作日志/变更记录制度 | 运维 | 中 |
| 5 | 接入消费监控告警(LAG 持续为 0 或 coordinator 异常告警) | 研发 | 中 |
| 6 | 补充 Kafka 故障排查手册(本案例可作为范本) | 运维 | 低 |
附录:关键排查命令
# 查消费者组状态(定位 coordinator 是否可用)kafka-consumer-groups.sh --bootstrap-server <host>:9092 --group <group> --describe
# 查内部 topic 副本/leader 状态kafka-topics.sh --bootstrap-server <host>:9092 --describe --topic __consumer_offsets
# 查 ZK 中注册的 brokerzookeeper-shell.sh <host>:2181 ls /brokers/ids
# 查 broker 注册的地址(确认 broker 存活 + advertised 地址是否可达)zookeeper-shell.sh <host>:2181 get /brokers/ids/0
# 查 topic 元数据(确认副本分配,重点看副本是否指向已下线 broker)zookeeper-shell.sh <host>:2181 get /brokers/topics/<topic>
# 副本指向死 broker 时的正确修复方式:重分配(磁盘在则不丢数据)kafka-reassign-partitions.sh --bootstrap-server <host>:9092 --reassignment-json-file reassign.json --execute
# 查 broker 日志中 coordinator/副本状态grep -iE 'GroupCoordinator|preferred replica|leader' logs/controller.loggrep -iE 'Leader|Offline|reassign' logs/state-change.log