目录

从崩溃到修复:生产环境 Zookeeper 集群脑裂排查实录

某天凌晨,Kafka 消息堆积告警。排查发现 Zookeeper 集群出现脑裂,两个节点都认为自己是 Leader。这篇文章记录了完整的排查和修复过程。

一、故障现象

1.1 告警信息

1
2
3
[CRITICAL] Kafka consumer lag > 100000
[WARN] Kafka broker 1 lost connection to zk
[WARN] Kafka broker 2 lost connection to zk

1.2 初步检查

1
2
3
4
# 检查 Kafka 状态
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --all-groups

# 结果:大量 consumer 显示 "UNKNOWN"

Kafka 依赖 Zookeeper 做元数据管理和 Leader 选举。ZK 出问题,Kafka 必然瘫痪。

二、Zookeeper 状态排查

2.1 集群拓扑

1
2
3
4
5
6
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    ZK-1     │    │    ZK-2     │    │    ZK-3     │
│ 192.168.1.1 │    │ 192.168.1.2 │    │ 192.168.1.3 │
└─────────────┘    └─────────────┘    └─────────────┘
      ↑                   ↑                   ↑
      └─────── Kafka Brokers connect ─────────┘

2.2 检查各节点状态

1
2
3
4
5
# 正常情况下,应该有 1 leader + 2 follower
for host in 192.168.1.{1,2,3}; do
  echo "=== $host ==="
  echo stat | nc $host 2181 | grep Mode
done

异常输出

1
2
3
4
5
6
=== 192.168.1.1 ===
Mode: leader
=== 192.168.1.2 ===
Mode: leader      ← 两个 Leader!脑裂!
=== 192.168.1.3 ===
Error: Connection refused

2.3 检查日志

1
2
3
4
5
6
# ZK-1 日志
tail -100 /var/log/zookeeper/zookeeper.log

# 发现
WARN  [QuorumPeer] - Cannot open channel to 3 at election address /192.168.1.3:3888
ERROR [QuorumPeer] - Unexpected exception causing shutdown while sock still open
1
2
3
4
# ZK-3 日志
# 发现进程已死
systemctl status zookeeper
# Active: failed (Result: exit-code)

三、脑裂原因分析

3.1 什么是脑裂?

正常的 Zookeeper 选举需要获得 Quorum (多数派) 同意:

1
2
3 节点集群:Quorum = 2
5 节点集群:Quorum = 3

脑裂场景

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
网络分区前:
  ZK-1(Leader) ←→ ZK-2(Follower) ←→ ZK-3(Follower)

网络分区后:
  分区 A: ZK-1, ZK-2     分区 B: ZK-3
  
  ZK-1: "我有 2 票,我是 Leader"
  ZK-3: "我收不到 Leader 心跳,发起选举"
       → 只有 1 票,无法选出新 Leader

正确行为:ZK-3 变成只读模式

但这次的情况不同…

3.2 根因定位

查看 ZK-3 的死亡原因:

1
2
3
4
5
# 系统日志
journalctl -u zookeeper --since "2021-10-14 02:00"

# 发现
Out of memory: Kill process 12345 (java) score 800

真相:ZK-3 被 OOM Killer 杀死,不是网络分区!

时间线重建

1
2
3
4
5
6
7
02:15 ZK-3 因 OOM 被杀死
02:16 ZK-1 和 ZK-2 发现失去 ZK-3
02:17 ZK-1 依然是 Leader (2 节点仍有 Quorum)
02:30 ZK-1 和 ZK-2 之间网络抖动
02:31 ZK-2 收不到 ZK-1 心跳,认为 Leader 挂了
02:32 ZK-2 发起选举……但没有 Quorum!
02:33 ZK-1 恢复联系,但 ZK-2 已经自认为是 Leader

关键问题:ZK-2 在网络恢复后没有正确 step down。

四、紧急修复

4.1 恢复健康节点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1. 停止所有 ZK 节点
for host in 192.168.1.{1,2,3}; do
  ssh $host "systemctl stop zookeeper"
done

# 2. 检查数据一致性
for host in 192.168.1.{1,2}; do
  ssh $host "cat /var/lib/zookeeper/version-2/currentEpoch"
done
# 确保 epoch 一致

# 3. 恢复 ZK-3
ssh 192.168.1.3 "systemctl start zookeeper"
# 等待 10 秒启动

# 4. 依次启动其他节点
ssh 192.168.1.1 "systemctl start zookeeper"
sleep 5
ssh 192.168.1.2 "systemctl start zookeeper"

4.2 验证恢复

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 检查集群状态
for host in 192.168.1.{1,2,3}; do
  echo "=== $host ==="
  echo stat | nc $host 2181 | grep Mode
done

# 期望输出
=== 192.168.1.1 ===
Mode: follower
=== 192.168.1.2 ===
Mode: follower
=== 192.168.1.3 ===
Mode: leader

# 检查 Kafka 恢复
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list

五、预防措施

5.1 内存配置优化

1
2
3
4
5
6
7
8
9
# /etc/zookeeper/java.env
export JVMFLAGS="-Xmx2g -Xms2g"

# 系统层面
# /etc/sysctl.conf
vm.swappiness=1  # 尽量不使用 swap

# 设置 OOM 优先级
echo -1000 > /proc/$(pgrep -f zookeeper)/oom_score_adj

5.2 监控告警

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Prometheus 告警规则
groups:
- name: zookeeper
  rules:
  - alert: ZookeeperDown
    expr: up{job="zookeeper"} == 0
    for: 1m
    labels:
      severity: critical
      
  - alert: ZookeeperNoLeader
    expr: zk_server_leader == 0
    for: 1m
    labels:
      severity: critical
      
  - alert: ZookeeperTooManyLeaders
    expr: sum(zk_server_leader) > 1
    for: 30s
    labels:
      severity: critical
      annotations:
        summary: "脑裂!检测到多个 Leader"

5.3 集群配置优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# zoo.cfg

# 心跳间隔(ms)
tickTime=2000

# Leader 等待 Follower 连接的 tick 数
initLimit=10

# Leader-Follower 同步的 tick 数
syncLimit=5

# 4 letter words 白名单(监控用)
4lw.commands.whitelist=stat,ruok,mntr,envi

# 开启 Leader 自动重选(防止僵死 Leader)
leaderServes=no

5.4 网络配置

1
2
3
4
5
6
7
8
9
# 确保 ZK 节点之间网络延迟 < 100ms
ping -c 10 192.168.1.2
# rtt min/avg/max = 0.5/0.8/1.2 ms ✓

# 检查防火墙
for port in 2181 2888 3888; do
  echo "Port $port:"
  nc -zv 192.168.1.2 $port
done

六、深入理解 ZAB 协议

6.1 选举过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Phase 1: Leader Election
  - 每个节点投票给自己
  - 收集其他节点的投票
  - 比较 (epoch, zxid, myid)
  - 切换到最优候选
  - 获得 Quorum 票数者当选

Phase 2: Discovery
  - Leader 收集 Follower 的最新 zxid
  - 确定需要同步的数据

Phase 3: Synchronization
  - Leader 向 Follower 同步数据
  - 所有 Follower 达成一致

Phase 4: Broadcast
  - Leader 开始接受客户端请求
  - 使用 2PC 协议广播更新

6.2 为什么需要 Quorum?

1
2
3
4
5
6
7
8
9
3 节点,Quorum = 2
5 节点,Quorum = 3
7 节点,Quorum = 4

公式:Quorum = N/2 + 1

任意两个 Quorum 必有交集
→ 保证数据一致性
→ 防止脑裂

6.3 最佳实践:节点数量

节点数容错数推荐场景
10仅开发环境
31小规模生产
52中大规模生产
73极高可用需求

七、排查工具集

7.1 四字命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 检查节点状态
echo stat | nc localhost 2181

# 检查健康
echo ruok | nc localhost 2181
# 返回 "imok"

# 监控指标
echo mntr | nc localhost 2181
# zk_version  3.6.3
# zk_server_state leader
# zk_num_alive_connections 10
# zk_outstanding_requests 0

# 环境信息
echo envi | nc localhost 2181

7.2 zkCli 操作

1
2
3
4
5
6
7
8
9
# 连接集群
zkCli.sh -server 192.168.1.1:2181,192.168.1.2:2181,192.168.1.3:2181

# 查看元数据
ls /brokers/ids
get /controller

# 查看 ACL
getAcl /kafka

7.3 日志分析技巧

1
2
3
4
5
6
7
8
# 查找选举相关日志
grep -E "LOOKING|LEADING|FOLLOWING|Election" zookeeper.log

# 查找连接问题
grep "Cannot open channel" zookeeper.log

# 查找会话超时
grep "Session expired" zookeeper.log

八、总结

步骤操作
发现监控告警 + Kafka 异常
定位四字命令检查各节点 Mode
分析日志 + 系统日志确定根因
修复有序重启,优先恢复 Quorum
预防监控 + 内存优化 + 网络检查

最大教训

  1. Zookeeper 对内存敏感,务必配置合理的 JVM 堆大小
  2. 设置 OOM 保护,让 ZK 进程不容易被杀
  3. 监控 Leader 数量,超过 1 个立即告警
  4. 定期演练故障恢复流程