【Kafka源码解读和使用指南】第65篇:Kafka故障转移实战——Broker宕机了怎么办
上一篇【第64篇】Kafka消费者可靠性实战——偏移量提交的那些坑下一篇【第66篇】Kafka生产环境系统可靠性验证——测试套件与混沌工程摘要凌晨3点告警响了——Broker3挂了。生产环境、大量订单、实时数据流……这时候最怕两件事一是慌二是操作失误。Kafka的故障转移机制能在秒级内自动完成Leader选举和分区重新分配但理解其内部流程才能在关键时刻做出正确决策。本文将故障转移分为三个阶段讲解自动恢复Controller怎么发现的、选举怎么发生的、人工介入什么时候需要手动干预、怎么操作、预防措施脑裂场景处理、滚动重启步骤、数据一致性验证。读完这篇下次遇到Broker宕机你就不慌了。一、故障转移全景——谁在替宕机Broker撑伞【单个Broker宕机的完整故障转移流程】 正常状态3 Broker, Topic有多个分区 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Broker1 │ │ Broker2 │ │ Broker3 │ │ │ │ │ │ │ │ P0(L) ✓ │ │ P1(L) ✓ │ │ P2(L) ✓ │ │ P1(F) ✓ │ │ P2(F) ✓ │ │ P0(F) ✓ │ │ P2(F) ✓ │ │ P0(F) ✓ │ │ P1(F) ✓ │ └──────────┘ └──────────┘ └──────────┘ Broker3 宕机 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Broker1 │ │ Broker2 │ │ Broker3 │ │ │ │ │ │ │ │ P0(L) ✓ │ │ P1(L) ✓ │ │ │ │ P1(F) ✓ │ │ P2(L▲) │ │ │ │ P2(F▲→L)│ │ P0(F) ✓ │ │ │ └──────────┘ └──────────┘ └──────────┘ ▲ 新Leader ✓ 正常Follower 变化 • P2 的 Leader 从 Broker3 → Broker1Leader选举 • P0 的 Follower 从 Broker3 → 缺失等它回来或补新副本 • P1 的 Follower 从 Broker3 → 缺失同上 时间线 T0s: Broker3 宕机 T0~6s: ZooKeeper 心跳超时zookeeper.session.timeout.ms6000 T6s: Controller 检测到 Broker3 离线 T6~8s: Controller 执行 Leader 选举 T8s: 新 Leader 就位写入恢复 ────────────────────────────── 总宕机影响时间约 6~8 秒二、Controller 的故障检测与响应2.1 检测机制【Controller 如何发现 Broker 宕机】 ┌──────────────────────────────────────────────┐ │ ZooKeeper │ │ │ │ /brokers/ids/1 [在线] │ │ /brokers/ids/2 [在线] │ │ /brokers/ids/3 [在线 → 超时删除] │ │ │ │ Controller (Broker1) 监听了 /brokers/ids │ │ 的子节点变化 → 发现 3 被删除了 → Broker3 挂了 │ └──────────────────────────────────────────────┘ 关键配置 zookeeper.session.timeout.ms6000 # Broker 与 ZK 的会话超时 zookeeper.connection.timeout.ms6000 # ZK 连接超时 注意KRaft 模式下Controller 通过 Raft 心跳检测2.2 Controller 的响应动作【Controller 发现 Broker 宕机后的处理流程】 ┌─────────────────────────────────────────────┐ │ Step 1: 识别受影响的分区 │ │ │ │ Broker3 上有哪些分区的 Leader哪些是 Follower│ │ │ │ 遍历所有分区 │ │ • orders-0: Leader(B3) → 需要重新选举 │ │ • orders-1: Follower(B3) → 从ISR移除 │ │ • orders-2: Leader(B3) → 需要重新选举 │ │ • payments-0: Follower(B3) → 从ISR移除 │ └─────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Step 2: 受影响的分区进行 Leader 选举 │ │ │ │ 对于 orders-0 │ │ AR [B3(dead), B1, B2] │ │ ISR [B3(dead), B1] │ │ → 在 ISR 中选第一个在线的: B1 │ │ → B1 成为 orders-0 的新 Leader ✓ │ │ │ │ 对于 orders-2 │ │ AR [B1, B3(dead), B2] │ │ ISR [B3(dead), B1, B2] │ │ → 在 ISR 中选第一个在线的: B1 │ │ → B1 成为 orders-2 的新 Leader ✓ (B1扛两个Leader) │ └─────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Step 3: 更新 ISR 列表 │ │ │ │ 所有包含 Broker3 的分区 │ │ → 将 Broker3 从 ISR 移除 │ │ → 如果 ISR 数量 min.insync.replicas │ │ → 该分区进入只读状态拒绝写入 │ └─────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Step 4: 广播元数据变更 │ │ │ │ → Producer: 更新metadata缓存知道新Leader是谁 │ │ → Consumer: 更新metadata缓存从新Leader读取 │ └─────────────────────────────────────────────┘三、多种故障场景处理3.1 场景一单个 Follower 宕机【Follower 宕机 → 影响最小】 宕机前 宕机后 ┌──────────┐ ┌──────────┐ │ B1(L) ✓ │ │ B1(L) ✓ │ │ B2(F) ✓ │ │ B2(F) ✓ │ │ B3(F) ✓ │ │ B3(F) │ └──────────┘ └──────────┘ ISR[B1,B2,B3] ISR[B1,B2] 影响 • 写入不受影响Leader还在ISR够 min.isr • 读取不受影响从 Leader 读 • 副本数从3降为2 → 风险轻微升高 • 不触发 Leader 选举 处理 • 自动不需要人工介入 • 手动检查 Broker3 为什么挂了修复后重启3.2 场景二单个 Leader 宕机【Leader 宕机 → 自动选举短暂中断】 宕机前 宕机后自动恢复 ┌──────────┐ ┌──────────┐ │ B1(L) │ │ B1 │ │ B2(F) ✓ │ │ B2(L▲) ✓ │ │ B3(F) ✓ │ │ B3(F) ✓ │ └──────────┘ └──────────┘ Producer 视角 ┌────────────────────────────────────────────┐ │ 发送 msg100 → Broker1(没有响应) │ │ → 自动重试 → metadata 刷新 │ │ → 发现新 Leader Broker2 │ │ → 重新发送到 Broker2 → 成功 │ │ │ │ 中断时间元数据刷新间隔 网络往返 │ │ 大约 100ms ~ 5s │ └────────────────────────────────────────────┘3.3 场景三多个 Broker 同时宕机【两个 Broker 宕机 → 分区可用性取决于 ISR 分布】 宕机前3 Broker, 3分区 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ B1 (L) ✓ │ │ B2 (F) ✓ │ │ B3 (F) ✓ │ │ P0 Leader│ │ P1 Leader│ │ P2 Leader│ └──────────┘ └──────────┘ └──────────┘ B1 B2 同时宕机 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ B1 │ │ B2 │ │ B3 (L) ✓ │ │ │ │ │ │ P0/P1/P2 │ └──────────┘ └──────────┘ │ 全部Leader│ └──────────┘ B3 扛下所有 ✓ 如果 B3 在 ISR 中 → 可以正常工作和选举 ✗ 如果 B3 不在 ISR 中 → unclean.leader.election.enablefalse → P0/P1/P2 这些分区不可用拒绝读写 RF3, min.isr2 时 同时挂两个 → ISR 只剩 B3 一个 → ISR 数量 1 min.isr2 → 拒绝写入宁可不可用不冒险丢数据3.4 场景四脑裂网络分区【脑裂场景】 网络故障导致集群分裂为两半 分区AZooKeeper可达 分区BZooKeeper不可达 ┌──────────┐ ┌──────────┐ │ B1 (旧L) │ │ B2 │ │ B3 │ │ B4 │ └──────────┘ └──────────┘ Kafka 防止脑裂的机制 ┌────────────────────────────────────────────┐ │ 1. ZooKeeper 会话超时 │ │ 分区B 的 Broker 丢失 ZK 连接 │ │ → session timeout → ZK 删除临时节点 │ │ │ │ 2. Controller 选举基于 ZK │ │ 只有分区A 的 Broker 能连上 ZK │ │ → Controller 必定在分区A │ │ → Leader 选举只在分区A发生 │ │ │ │ 3. 分区B 变成孤岛 │ │ 所有 Broker 丢失 ZK 连接 │ │ 不能成为 Leader │ │ 不能向外提供写入服务 │ │ 不会有另一个Leader出现 ← 防止脑裂 │ └────────────────────────────────────────────┘ 预期行为 • 分区A 正常运行有新Controller • 分区B 停止服务不会产生双写 • 网络恢复后分区B 重新加入集群四、滚动重启——不停机维护4.1 操作步骤【Kafka 滚动重启操作流程】 Step 1: 准备确认集群状态 ┌──────────────────────────────────────────┐ │ $ kafka-topics.sh --describe │ │ # 确认所有分区的 ISR 都正常 │ │ │ │ $ kafka-consumer-groups.sh --describe │ │ --group group-name │ │ # 确认消费者没有大面积 Lag │ └──────────────────────────────────────────┘ Step 2: 选择一个 Broker优雅停机 ┌──────────────────────────────────────────┐ │ $ kafka-server-stop.sh │ │ # 等待进程退出 │ │ # Controller 会自动将这个 Broker 上的 │ │ # Leader 迁移到其他 Broker │ └──────────────────────────────────────────┘ Step 3: 确认 Leader 迁移完成 ┌──────────────────────────────────────────┐ │ # 确认没有分区处于 Under Replicated 状态 │ │ # 确认所有 Leader 不在要重启的 Broker 上 │ │ │ │ $ kafka-topics.sh --describe │ │ --under-replicated-partitions │ └──────────────────────────────────────────┘ Step 4: 重启 Broker ┌──────────────────────────────────────────┐ │ $ kafka-server-start.sh -daemon │ │ $KAFKA_HOME/config/server.properties │ │ │ │ # 等待 Broker 完全启动加入集群 │ │ # Follower 副本开始从 Leader 同步 │ └──────────────────────────────────────────┘ Step 5: 确认恢复完成 ┌──────────────────────────────────────────┐ │ # 确认所有 ISR 恢复 │ │ # 确认没有 under-replicated 分区 │ │ # 确认 Lag 没有异常增长 │ └──────────────────────────────────────────┘ Step 6: 继续下一个 Broker重复 Step 2~5 ┌──────────────────────────────────────────┐ │ 每个 Broker 之间间隔 3~5 分钟 │ │ 确保所有副本追上数据 │ └──────────────────────────────────────────┘4.2 自动化脚本#!/bin/bash# kafka-rolling-restart.sh# Kafka 滚动重启脚本BROKERS(broker1:9092broker2:9092broker3:9092)BOOTSTRAP_SERVERbroker1:9092,broker2:9092,broker3:9092SSH_USERkafkaforbrokerin${BROKERS[]};dohost$(echo$broker|cut-d:-f1)echo 重启$host# 1. 停止 Kafkassh$SSH_USER$hostsudo systemctl stop kafka# 2. 等待 Controller 完成 Leader 迁移echo等待 Leader 迁移...sleep30# 3. 检查是否有 Under-Replicated 分区under_replicated$(kafka-topics.sh --bootstrap-server $BOOTSTRAP_SERVER\--describe--under-replicated-partitions2/dev/null|wc-l)if[$under_replicated-gt0];thenecho警告: 存在$under_replicated个 Under-Replicated 分区!echo等待恢复...sleep60fi# 4. 重启 Kafkassh$SSH_USER$hostsudo systemctl start kafka# 5. 等待 Broker 完全启动echo等待$host恢复...sleep120# 6. 再次检查under_replicated$(kafka-topics.sh --bootstrap-server $BOOTSTRAP_SERVER\--describe--under-replicated-partitions2/dev/null|wc-l)if[$under_replicated-gt0];thenecho错误:$host重启后仍有$under_replicated个分区未恢复!breakfiecho$host重启完成 done五、故障后的数据一致性验证【数据一致性验证方法】 1. 检查分区状态 ┌──────────────────────────────────────────┐ │ kafka-topics.sh --describe --topic orders │ │ │ │ 重点关注 │ │ • Leader 是否分散在不同 Broker │ │ • Replicas 数量是否恢复 │ │ • ISR 数量是否 RF │ │ • 没有 Under-Replicated 标记 │ └──────────────────────────────────────────┘ 2. 检查生产者端告警 ┌──────────────────────────────────────────┐ │ 监控应用日志中的发送失败回调 │ │ 发送失败的记录数 0 → 需要检查 │ └──────────────────────────────────────────┘ 3. 检查消费者端 Lag ┌──────────────────────────────────────────┐ │ kafka-consumer-groups.sh │ │ --bootstrap-server localhost:9092 │ │ --group order-service --describe │ │ │ │ LAG 突然变为 0 → 可能跳过了消息丢数据 │ │ LAG 突然变大 → 消费者组出问题了 │ └──────────────────────────────────────────┘ 4. 端到端验证 ┌──────────────────────────────────────────┐ │ 发送一条测试消息追踪全链路 │ │ │ │ ① Producer → 发送测试消息带唯一ID │ │ ② Broker → 确认消息写入各分区 │ │ ③ Consumer → 消费到测试消息 │ │ ④ 数据库 → 确认业务数据已写入 │ │ │ │ 全链路通过 系统正常 │ └──────────────────────────────────────────┘本篇小结Kafka的故障转移是在设计层面就嵌入的不是外挂自动故障检测ZooKeeper会话超时6s默认触发Controller发现宕机BrokerLeader自动选举Controller从ISR中选第一个在线副本秒级完成脑裂防御ZooKeeper连接断开 → Broker自动退化为孤岛 → 不能成为Leader滚动重启要领一次只动一个Broker等副本同步完再动下一个间隔3~5分钟数据验证检查ISR恢复、under-replicated分区、Lag突变再加一条端到端测试核心点只要unclean.leader.election.enablefalse且RF≥3且min.isr≥2绝大多数故障场景下Kafka都能自动恢复。下一篇我们将用混沌工程的思路来验证Kafka系统的可靠性——主动制造故障验证你的保护措施是否真的有效。上一篇【第64篇】Kafka消费者可靠性实战——偏移量提交的那些坑下一篇【第66篇】Kafka生产环境系统可靠性验证——测试套件与混沌工程