上一篇【第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 → Broker1(Leader选举) • P0 的 Follower 从 Broker3 → 缺失(等它回来或补新副本) • P1 的 Follower 从 Broker3 → 缺失(同上) 时间线: T+0s: Broker3 宕机 T+0~6s: ZooKeeper 心跳超时(zookeeper.session.timeout.ms=6000) T+6s: Controller 检测到 Broker3 离线 T+6~8s: Controller 执行 Leader 选举 T+8s: 新 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.ms=6000 # Broker 与 ZK 的会话超时 zookeeper.connection.timeout.ms=6000 # 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.enable=false → P0/P1/P2 这些分区不可用(拒绝读写) RF=3, min.isr=2 时: 同时挂两个 → ISR 只剩 B3 一个 → ISR 数量 1 < min.isr=2 → 拒绝写入!宁可不可用,不冒险丢数据3.4 场景四:脑裂(网络分区)
【脑裂场景】 网络故障导致集群分裂为两半: 分区A(ZooKeeper可达): 分区B(ZooKeeper不可达): ┌──────────┐ ┌──────────┐ │ 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:9092""broker2:9092""broker3:9092")BOOTSTRAP_SERVER="broker1:9092,broker2:9092,broker3:9092"SSH_USER="kafka"forbrokerin"${BROKERS[@]}";dohost=$(echo$broker|cut-d:-f1)echo"=== 重启$host==="# 1. 停止 Kafkassh$SSH_USER@$host"sudo 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@$host"sudo 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发现宕机Broker
- Leader自动选举:Controller从ISR中选第一个在线副本,秒级完成
- 脑裂防御:ZooKeeper连接断开 → Broker自动退化为"孤岛" → 不能成为Leader
- 滚动重启要领:一次只动一个Broker,等副本同步完再动下一个,间隔3~5分钟
- 数据验证:检查ISR恢复、under-replicated分区、Lag突变,再加一条端到端测试
核心点:只要unclean.leader.election.enable=false且RF≥3且min.isr≥2,绝大多数故障场景下Kafka都能自动恢复。
下一篇,我们将用混沌工程的思路来验证Kafka系统的可靠性——主动制造故障,验证你的保护措施是否真的有效。
上一篇【第64篇】Kafka消费者可靠性实战——偏移量提交的那些坑
下一篇【第66篇】Kafka生产环境系统可靠性验证——测试套件与混沌工程