某天凌晨,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 最佳实践:节点数量
| 节点数 | 容错数 | 推荐场景 |
|---|
| 1 | 0 | 仅开发环境 |
| 3 | 1 | 小规模生产 |
| 5 | 2 | 中大规模生产 |
| 7 | 3 | 极高可用需求 |
七、排查工具集
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 |
| 预防 | 监控 + 内存优化 + 网络检查 |
最大教训:
- Zookeeper 对内存敏感,务必配置合理的 JVM 堆大小
- 设置 OOM 保护,让 ZK 进程不容易被杀
- 监控 Leader 数量,超过 1 个立即告警
- 定期演练故障恢复流程