ZAB 协议详解
ZAB(ZooKeeper Atomic Broadcast,ZooKeeper 原子广播)协议是 ZooKeeper 的核心一致性协议,负责保证分布式系统中数据的一致性。理解 ZAB 协议对于深入掌握 ZooKeeper 至关重要。
什么是 ZAB 协议
ZAB 协议是一种专为 ZooKeeper 设计的崩溃可恢复的原子广播协议。它确保所有事务按照相同的顺序在所有服务器上被处理,从而保证分布式系统中状态的一致性。
设计目标
ZAB 协议的设计主要满足以下几个核心目标:
全序广播(Total Order Broadcast):所有服务器以相同的顺序处理所有事务。这是实现状态一致性的基础。如果服务器 A 先处理事务 1 再处理事务 2,那么服务器 B 也必须按相同的顺序处理。
容错性(Fault Tolerance):系统能够容忍部分节点故障并继续运行。只要多数节点存活,系统就能正常工作。
高可用性(High Availability):在 Leader 故障时,能够快速选举出新的 Leader,最小化服务中断时间。
状态同步(State Synchronization):在故障恢复后,确保所有服务器收敛到相同的状态。新加入的节点或从故障中恢复的节点需要同步到最新状态。
与 Paxos 的区别
ZAB 协议常被拿来与 Paxos 协议比较,两者都是共识协议,但设计侧重点不同:
| 特性 | ZAB | Paxos |
|---|---|---|
| 设计目标 | 为主从架构设计 | 通用共识协议 |
| Leader 角色 | 必须有且唯一 | 可以有多个提议者 |
| 顺序保证 | 全局顺序严格保证 | 只保证单个实例内的顺序 |
| 恢复机制 | 内置崩溃恢复 | 需要额外实现 |
| 适用场景 | ZooKeeper 协调服务 | 通用分布式共识 |
ZAB 协议针对 ZooKeeper 的工作负载进行了优化。ZooKeeper 的工作负载特点是读多写少,而且需要一个稳定的 Leader 来处理所有写请求。ZAB 协议的 Leader 机制简化了协议复杂度,提高了写入效率。
与 Raft 协议的对比
Raft 协议是近年来广受欢迎的共识协议,与 ZAB 有许多相似之处,但也存在关键差异:
| 特性 | ZAB | Raft |
|---|---|---|
| Leader 选举 | 基于 ZXID 选举 | 基于 Term 和日志位置选举 |
| 日志顺序 | 全局单调递增的 ZXID | Term + Index 组合标识 |
| 数据同步 | 支持 DIFF/TRUNC/SNAP 三种模式 | 主要是快照和增量同步 |
| 状态机 | 顺序一致的状态机 | 顺序一致的状态机 |
| 写入流程 | 两阶段:Proposal + Commit | 两阶段:Log Entry + Commit |
关键区别分析:
1. Epoch vs Term
ZAB 使用 epoch 来标识 Leader 任期,每次新 Leader 当选,epoch 递增。Raft 使用 term 来标识选举轮次。两者作用相似,但 ZAB 的 epoch 直接编码在 ZXID 中,而 Raft 的 term 与日志索引分离。
2. 日志标识
- ZAB:ZXID = (epoch, counter),单一数字表示全局顺序
- Raft:(term, index),需要两个字段来定位一条日志
3. 数据恢复
ZAB 提供了更精细的数据恢复机制:
- DIFF:发送差异事务,效率高
- TRUNC:截断未提交事务,保证一致性
- SNAP:完整快照,适用于新节点
Raft 主要依赖快照和日志复制,恢复粒度相对较粗。
4. 实际应用选择
| 场景 | 推荐 | 原因 |
|---|---|---|
| 需要强一致性的协调服务 | ZAB | 专为协调设计,恢复机制完善 |
| 通用分布式存储 | Raft | 实现简单,生态丰富 |
| 高写入吞吐量 | Raft | Leader 优化更好 |
| 需要快速恢复 | ZAB | 多种恢复模式更灵活 |
ZAB 协议的两种模式
ZAB 协议有两种核心工作模式:消息广播(Message Broadcast) 和 崩溃恢复(Crash Recovery)。
┌─────────────────────────────────────────────────────────────┐
│ ZAB 协议工作模式 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 消息广播模式 │◀───────▶│ 崩溃恢复模式 │ │
│ │ (正常运行状态) │ │ (Leader 故障时) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ • Leader 处理写请求 • Leader 选举 │
│ • Proposal 广播 • 数据同步 │
│ • Follower 确认 • 恢复丢失的事务 │
│ • 提交事务 • 确保一致性 │
│ │
│ 触发条件: 触发条件: │
│ • Leader 已确立 • Leader 崩溃 │
│ • 多数 Follower 在线 • 网络分区 │
│ • 新节点加入 │
│ │
└─────────────────────────────────────────────────────────────┘
消息广播模式
消息广播模式是 ZAB 协议在正常运行时的状态,此时有一个稳定的 Leader 和多数 Follower。所有写请求都通过 Leader 处理,Leader 将事务广播给所有 Follower。
工作流程:
┌─────────────────────────────────────────────────────────────┐
│ 消息广播流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 Leader Follower1 Follower2 │
│ │ │ │ │ │
│ │── 写请求 ───▶│ │ │ │
│ │ │ │ │ │
│ │ │── Proposal ───▶│ │ │
│ │ │── Proposal ──────────────────▶│ │
│ │ │ │ │ │
│ │ │ │── ACK ──────▶│ │
│ │ │◀── ACK ────────│ │ │
│ │ │◀── ACK ──────────────────────│ │
│ │ │ │ │ │
│ │ │── Commit ─────▶│ │ │
│ │ │── Commit ───────────────────▶│ │
│ │ │ │ │ │
│ │◀─ 响应 ──────│ │ │ │
│ │ │ │ │ │
│ │
└─────────────────────────────────────────────────────────────┘
详细步骤:
-
接收请求:Leader 接收客户端的写请求,将请求数据封装成事务。
-
创建 Proposal:Leader 为事务分配全局唯一的 ZXID,创建 Proposal(提议)。ZXID 是一个 64 位数字,高 32 位是 epoch(纪元),低 32 位是计数器。
-
广播 Proposal:Leader 将 Proposal 发送给所有 Follower。发送顺序严格按照 ZXID 的顺序。
-
Follower 处理:
- Follower 收到 Proposal 后,先将其写入本地事务日志
- 写入成功后,向 Leader 发送 ACK(确认)
- 此时事务尚未应用到状态机
-
收集 ACK:Leader 等待收到多数 Follower 的 ACK。收到多数确认后,事务就可以提交了。
-
广播 Commit:Leader 发送 Commit 消息给所有 Follower。
-
提交事务:Follower 收到 Commit 消息后,将事务应用到状态机,此时数据对客户端可见。
关键特性:
- FIFO 顺序:Leader 发送 Proposal 的顺序严格按 ZXID 递增,Follower 也按相同顺序接收和处理
- 多数确认:只需要多数 Follower 确认即可提交,不需要全部确认
- 两阶段提交:Proposal 阶段和 Commit 阶段分离,保证原子性
崩溃恢复模式
当 Leader 崩溃或网络分区导致 Leader 与多数 Follower 失去联系时,系统进入崩溃恢复模式。此时需要选举新的 Leader,并确保数据一致性。
触发条件:
- Leader 服务器崩溃或重启
- 网络分区导致 Leader 与多数 Follower 失联
- 初始启动时没有 Leader
恢复流程:
┌─────────────────────────────────────────────────────────────┐
│ 崩溃恢复流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Phase 0: 选举阶段 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. 所有服务器进入 LOOKING 状态 │ │
│ │ 2. 各自投票给拥有最新数据的服务器 │ │
│ │ 3. 收到多数票的服务器成为新 Leader │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 1: 发现阶段(Discovery) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Leader 收集 Follower 的最新 ZXID │ │
│ │ 2. 确定 Follower 的数据状态 │ │
│ │ 3. 确立新的 epoch │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 2: 同步阶段(Synchronization) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Leader 将缺失的事务同步给 Follower │ │
│ │ 2. 丢弃未提交的事务 │ │
│ │ 3. 所有 Follower 与 Leader 对齐 │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 进入消息广播模式 │
│ │
└─────────────────────────────────────────────────────────────┘
ZXID 的结构
ZXID(ZooKeeper Transaction ID)是 ZAB 协议中用于标识事务的全局唯一 ID。理解 ZXID 的结构对于理解 ZAB 协议至关重要。
┌─────────────────────────────────────────────────────────────┐
│ ZXID 结构(64位) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 高 32 位(epoch) 低 32 位(counter) │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ Leader 任期编号 │ │ 事务序号 │ │
│ │ │ │ │ │
│ │ 每次选举新 Leader │ │ 单调递增的计数器 │ │
│ │ epoch 会递增 │ │ │ │
│ └────────────────────────┘ └────────────────────────┘ │
│ │
│ 示例: │
│ ZXID = 0x0000000100000001 │
│ │ │ │ │
│ │ │ └── counter = 1 │
│ │ └─────────── epoch = 1 │
│ └───────────────────── 第 1 个 Leader 的第 1 个事务 │
│ │
│ ZXID = 0x0000000200000005 │
│ │ │ │ │
│ │ │ └── counter = 5 │
│ │ └─────────── epoch = 2 │
│ └───────────────────── 第 2 个 Leader 的第 5 个事务 │
│ │
└─────────────────────────────────────────────────────────────┘
Epoch 的意义:
Epoch 代表 Leader 的任期编号,每次选举出新的 Leader,epoch 就会递增。Epoch 的作用包括:
- 区分不同 Leader:同一个 Leader 任期内产生的 ZXID 具有相同的 epoch
- 防止旧 Leader 干扰:如果旧 Leader 恢复后尝试广播消息,由于 epoch 较小,会被拒绝
- 判断数据新旧:epoch 越大表示数据越新
Counter 的意义:
Counter 是单调递增的事务序号,在同一个 epoch 内,每个新事务的 counter 都比前一个大 1。Counter 保证了同一 Leader 任期内事务的严格顺序。
ZXID 的比较规则:
// 先比较 epoch,epoch 大的 ZXID 更新
// epoch 相同则比较 counter,counter 大的 ZXID 更新
public int compareZxid(long zxid1, long zxid2) {
long epoch1 = zxid1 >> 32;
long epoch2 = zxid2 >> 32;
if (epoch1 != epoch2) {
return epoch1 > epoch2 ? 1 : -1;
}
long counter1 = zxid1 & 0xFFFFFFFFL;
long counter2 = zxid2 & 0xFFFFFFFFL;
return Long.compare(counter1, counter2);
}
Leader 选举详解
Leader 选举是 ZAB 协议中最关键的部分之一。选举的目标是选出一个拥有最新数据的服务器作为 Leader,以最小化数据丢失。
选举状态
每个服务器在选举过程中都有自己的状态:
| 状态 | 说明 |
|---|---|
LOOKING | 正在寻找 Leader,参与选举投票 |
FOLLOWING | 已找到 Leader,作为 Follower 运行 |
LEADING | 已被选为 Leader,作为 Leader 运行 |
OBSERVING | 观察者状态,不参与投票 |
选举算法
ZooKeeper 支持多种选举算法,目前默认使用的是 Fast Leader Election(快速选举算法)。
投票信息:
每个投票包含以下信息:
sid:服务器 ID(myid)zxid:服务器最新的 ZXIDepoch:当前的逻辑时钟
选举规则:
比较优先级:
1. epoch 大的优先(表示数据更可能是最新的)
2. epoch 相同时,zxid 大的优先(表示事务更多)
3. zxid 相同时,sid 大的优先(确保选举有确定结果)
选举流程:
┌─────────────────────────────────────────────────────────────┐
│ Fast Leader Election │
├─────────────────────────────────────────────────────────────┤
│ │
│ Server1 (sid=1, zxid=100) │
│ Server2 (sid=2, zxid=200) │
│ Server3 (sid=3, zxid=150) │
│ │
│ 第一步:各自投自己 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Server1 投票:(1, 100) │ │
│ │ Server2 投票:(2, 200) │ │
│ │ Server3 投票:(3, 150) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第二步:交换投票信息 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Server1 收到 (2, 200) 和 (3, 150) │ │
│ │ → 200 > 100,改投 Server2 │ │
│ │ Server2 收到 (1, 100) 和 (3, 150) │ │
│ │ → 自己 zxid 最大,保持投自己 │ │
│ │ Server3 收到 (1, 100) 和 (2, 200) │ │
│ │ → 200 > 150,改投 Server2 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 第三步:统计投票 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Server2 获得 3 票(全部),超过半数 │ │
│ │ Server2 成为 Leader │ │
│ │ Server1、Server3 成为 Follower │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 结果:Server2 当选 Leader │
│ │
└─────────────────────────────────────────────────────────────┘
选举实现细节
投票广播:
每个服务器在选举过程中会向所有其他服务器发送自己的投票信息:
public class Vote {
private final long sid; // 服务器 ID
private final long zxid; // 最新 ZXID
private final long electionEpoch; // 选举轮次
// 比较两个投票的优先级
public boolean isBetterThan(Vote other) {
if (this.electionEpoch > other.electionEpoch) {
return true;
}
if (this.electionEpoch < other.electionEpoch) {
return false;
}
if (this.zxid > other.zxid) {
return true;
}
if (this.zxid < other.zxid) {
return false;
}
return this.sid > other.sid;
}
}
选票处理:
收到其他服务器的投票后,需要进行比较和更新:
// 伪代码
void processVote(Vote receivedVote) {
// 如果收到的投票比当前投票更优,则更新自己的投票
if (receivedVote.isBetterThan(currentVote)) {
currentVote = receivedVote;
broadcastVote(currentVote); // 广播新投票
}
// 统计当前投票情况
Map<Vote, Integer> voteCounts = countVotes();
// 检查是否有投票获得多数支持
for (Map.Entry<Vote, Integer> entry : voteCounts.entrySet()) {
if (entry.getValue() > servers.size() / 2) {
// 选出 Leader
if (entry.getKey().getSid() == mySid) {
state = State.LEADING;
} else {
state = State.FOLLOWING;
}
return;
}
}
}
数据同步机制
新 Leader 选出后,需要确保所有 Follower 的数据与 Leader 一致,这就是数据同步阶段。
同步场景
┌─────────────────────────────────────────────────────────────┐
│ 数据同步场景分析 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景 1:Follower 数据落后 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Leader: [T1][T2][T3][T4][T5] │ │
│ │ Follower:[T1][T2] │ │
│ │ │ │
│ │ 解决:发送 T3, T4, T5 给 Follower │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 场景 2:Follower 有未提交的事务 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Leader: [T1][T2][T3] (committed) │ │
│ │ Follower:[T1][T2][T3][T4] (T4 未提交) │ │
│ │ │ │
│ │ 解决:丢弃 T4,只保留已提交的事务 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 场景 3:Follower 数据缺失 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Leader: [T1][T2][T3][T4][T5] │ │
│ │ Follower: (新加入,无数据) │ │
│ │ │ │
│ │ 解决:发送完整快照或差异事务 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
同步策略
ZooKeeper 使用不同的策略来处理不同的同步场景:
DIFF(差异同步):
当 Follower 只缺少少量事务时,Leader 发送缺失的事务列表。这是最高效的同步方式。
Leader 发送:
DIFF
TXN: T3
TXN: T4
TXN: T5
TRUNC(如果需要截断)
TRUNC(截断同步):
当 Follower 有未提交的事务时,Leader 指示 Follower 截断到某个 ZXID。
Leader 发送:
TRUNC
ZXID: 0x100000003 // Follower 应该截断到这个 ZXID
SNAP(快照同步):
当 Follower 缺失太多事务,或者 Follower 是新加入的节点时,Leader 发送完整的内存快照。
Leader 发送:
SNAP
[完整的内存快照数据]
同步流程
┌─────────────────────────────────────────────────────────────┐
│ 数据同步流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Leader Follower │
│ │ │ │
│ │◀─────── CONNECT(携带 lastZxid)─────────│ │
│ │ │ │
│ │ ── 判断同步策略 ── │ │
│ │ │ │
│ │────────── DIFF/TRUNC/SNAP ──────────────▶│ │
│ │ │ │
│ │◀───────────── ACK ───────────────────────│ │
│ │ │ │
│ │────────── NEWLEADER ────────────────────▶│ │
│ │ │ │
│ │◀───────────── ACK ───────────────────────│ │
│ │ │ │
│ │────────── UPDATEEPOCH ──────────────────▶│ │
│ │ │ │
│ │ 同步完成,开始消息广播 │ │
│ │ │ │
│ │
└─────────────────────────────────────────────────────────────┘
ZAB 协议的一致性保证
ZAB 协议提供以下一致性保证,这些是 ZooKeeper 能作为可靠协调服务的基础:
顺序一致性
所有客户端看到的操作顺序与它们被应用的顺序一致。这意味着:
- 如果操作 A 在操作 B 之前被执行,那么所有客户端都会先看到 A 的结果,再看到 B 的结果
- ZXID 的全序关系保证了这一点
原子性
每个事务要么成功应用到所有服务器,要么都不应用。不存在"部分服务器应用了,部分没应用"的中间状态。
- 通过两阶段提交(Proposal + Commit)实现
- 多数确认机制确保只有被多数接受的事务才会提交
单一系统映像
无论客户端连接到哪个服务器,看到的系统视图都是一致的。
- Leader 保证所有写请求按相同顺序处理
- Follower 同步 Leader 的状态
- 客户端可能短暂看到旧数据,但不会看到乱序的数据
可靠性
一旦事务被成功应用,它就会持久保存,直到被新的事务覆盖。
- 事务写入磁盘日志后才确认
- 即使服务器崩溃,重启后也能恢复
及时性
客户端看到的系统视图在一定时间延迟后保证是最新的。
- 通过 Sync 操作可以强制获取最新数据
- 读请求可以要求从 Leader 读取以获得最新数据
ZAB 协议的实现细节
事务日志
ZAB 协议使用事务日志(Transaction Log)来持久化事务:
// 事务日志写入流程
public void logTransaction(TxnHeader hdr, Record txn) {
// 1. 序列化事务
byte[] data = serialize(hdr, txn);
// 2. 写入文件(追加写入)
logFile.write(data);
// 3. 刷新到磁盘(确保持久化)
logFile.force(true); // true 表示同步元数据
// 4. 返回成功
}
事务日志的特点:
- 顺序写入,性能高
- 每 MB 会预分配空间,减少磁盘寻址
- 定期打快照(Snapshot),加速恢复
快照机制
为了加速恢复,ZooKeeper 定期将内存状态保存为快照:
// 快照保存流程
public void saveSnapshot() {
// 1. 获取当前状态
long zxid = getLastZxid();
Map<String, DataNode> dataTree = getDataTree();
// 2. 序列化
SnapshotHeader header = new SnapshotHeader(zxid, System.currentTimeMillis());
// 3. 写入快照文件
String filename = "snapshot." + Long.toHexString(zxid);
writeSnapshot(filename, header, dataTree);
}
快照文件命名规则:snapshot.<zxid>,其中 zxid 是快照开始时的事务 ID。
恢复流程
服务器启动或崩溃恢复时的恢复流程:
┌─────────────────────────────────────────────────────────────┐
│ 恢复流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 读取最新的快照文件 │
│ └─ 恢复内存数据树到快照时的状态 │
│ │
│ 2. 读取事务日志 │
│ └─ 从快照 ZXID 开始,重放所有事务 │
│ │
│ 3. 恢复完成 │
│ └─ 内存状态恢复到最新 │
│ │
│ 4. 参与 Leader 选举或恢复 │
│ └─ 根据当前角色进行后续处理 │
│ │
└─────────────────────────────────────────────────────────────┘
ZAB 协议的性能特点
优势
高吞吐量:Leader 处理所有写请求,避免了分布式锁的争用。Follower 只需要确认和复制,开销较小。
低延迟读:Follower 可以处理读请求,分担了 Leader 的压力。客户端可以选择从最近的 Follower 读取。
快速恢复:Fast Leader Election 算法可以在几秒内完成选举。数据同步也经过优化,最小化恢复时间。
局限性
写入瓶颈:所有写请求都经过 Leader,Leader 可能成为瓶颈。对于写入密集的场景,需要考虑其他方案。
内存限制:ZooKeeper 将所有数据保存在内存中,数据量受限于内存大小。不适合存储大量数据。
多数派要求:需要多数节点存活才能工作。3 节点集群只能容忍 1 个节点故障。
性能优化建议
-
合理设置 tickTime:
- 默认 2000ms,可以根据网络延迟调整
- 较小的 tickTime 可以加快故障检测,但增加误判风险
-
使用 Observer:
- Observer 不参与投票,可以无限扩展
- 适合读密集型场景
-
调整日志和快照策略:
snapCount控制快照频率- 定期清理旧的日志和快照
-
分离事务日志和快照存储:
- 使用不同的磁盘可以提高 I/O 性能
生产环境最佳实践
理解 ZAB 协议后,在生产环境中部署和运维 ZooKeeper 需要遵循一些关键原则,以确保系统的稳定性和高性能。
集群规模规划
服务器数量选择:
ZooKeeper 集群需要多数节点存活才能工作,因此建议使用奇数个服务器:
| 集群规模 | 容忍故障数 | 适用场景 |
|---|---|---|
| 1 台 | 0 | 开发测试环境 |
| 3 台 | 1 | 小规模生产环境 |
| 5 台 | 2 | 关键生产环境 |
| 7 台 | 3 | 大规模高可用场景 |
规划原则:
- 最小生产配置:3 台服务器,可容忍 1 台故障
- 维护友好配置:5 台服务器,可在维护期间容忍额外故障
- 避免偶数配置:4 台和 3 台容错能力相同,但增加网络开销
实际案例:
场景:需要在不停止服务的情况下进行维护
方案 A(3 台集群):
- 正常运行:3/3 可用
- 维护 1 台:2/3 可用(可容忍 0 台故障)
- 风险:维护期间再故障 1 台将导致服务不可用
方案 B(5 台集群):
- 正常运行:5/5 可用
- 维护 1 台:4/5 可用(仍可容忍 1 台故障)
- 优势:维护期间保持高可用
硬件资源配置
CPU 要求:
ZooKeeper 对 CPU 要求不高,但需要稳定:
- 最低配置:双核处理器
- 推荐配置:4 核处理器
- 注意:避免 CPU 资源竞争,不要与其他 CPU 密集型应用共享服务器
内存配置:
内存大小取决于数据量和连接数:
# 推荐配置公式
堆内存 = (预期 znode 数量 × 平均 znode 大小) + 连接缓冲 + 安全余量
# 示例配置
# 10 万 znode,平均 1KB,1000 连接
# 堆内存 = 100000 × 1KB + 1000 × 1MB + 512MB ≈ 1.5GB
内存配置最佳实践:
# 设置 JVM 堆内存
# 推荐:物理内存的 75%,但不超过 3GB
export JVMFLAGS="-Xms2g -Xmx2g"
# 重要:避免内存交换
# 确保最大堆内存小于物理内存
磁盘配置:
磁盘性能对 ZooKeeper 影响最大:
# 关键配置:分离事务日志和快照
dataDir=/var/lib/zookeeper/snapshot
dataLogDir=/var/lib/zookeeper/log # 使用独立磁盘
# 事务日志特点:
# - 顺序写入
# - 对延迟敏感
# - 建议使用 SSD 或专用磁盘
磁盘性能指标:
| 磁盘类型 | 顺序写入延迟 | 推荐场景 |
|---|---|---|
| HDD | 5-10ms | 开发环境 |
| SATA SSD | 0.1-0.5ms | 一般生产环境 |
| NVMe SSD | 0.01-0.1ms | 高性能生产环境 |
网络配置
关键配置参数:
# tickTime:基本时间单位(毫秒)
tickTime=2000
# initLimit:Follower 初始化连接 Leader 的最长时间
# 新节点加入集群时使用,单位为 tickTime
initLimit=10
# syncLimit:Follower 与 Leader 同步的时间限制
# 心跳和事务同步使用,单位为 tickTime
syncLimit=5
网络延迟考虑:
网络环境 推荐 tickTime 说明
-----------------------------------------------------------
局域网(<1ms) 2000ms 默认配置
同城双机房(1-5ms) 3000ms 适当增加
跨地域部署(>10ms) 不推荐 考虑使用 Observer
防止网络分区:
# 配置多网卡支持(ZooKeeper 3.6.0+)
# 增加 Leader 选举和 ZAB 协议的可用性
server.1=zoo1:2888:3888:participant;2181
server.1=zoo1:2889:3889:participant;2182
ZAB 协议相关调优
事务日志优化:
# 预分配空间大小(减少磁盘寻址)
preAllocSize=65536 # 64MB,默认值
# 快照触发条件
# 事务数量达到 snapCount 时触发快照
snapCount=100000 # 默认值
# 快照大小限制
snapSizeLimitInKb=4194304 # 4GB,默认值
# 提交日志保留数量(加速 Follower 同步)
commitLogCount=500
选举优化:
# Leader 选举算法
# 默认使用 FastLeaderElection
electionAlg=3
# 选举端口配置
# 格式:server.id=host:quorumPort:electionPort
server.1=zk1:2888:3888
server.2=zk2:2888:3888
server.3=zk3:2888:3888
会话管理:
# 最小会话超时(单位:毫秒)
# 默认为 2 × tickTime
minSessionTimeout=4000
# 最大会话超时
# 默认为 20 × tickTime
maxSessionTimeout=40000
# 客户端配置
zkClientTimeout=30000 # 客户端会话超时
监控与告警
关键监控指标:
| 指标类别 | 指标名称 | 说明 | 告警阈值 |
|---|---|---|---|
| Leader 相关 | zk_server_state | 当前角色(Leader/Follower) | Leader 变化 |
zk_packets_received | 接收包数量 | 异常增长 | |
zk_packets_sent | 发送包数量 | 异常增长 | |
| 事务相关 | zk_znode_count | znode 数量 | 过大影响性能 |
zk_watch_count | watcher 数量 | 过多影响性能 | |
zk_ephemerals_count | 临时节点数量 | 异常增长 | |
| 性能相关 | zk_avg_latency | 平均延迟 | >10ms |
zk_max_latency | 最大延迟 | >100ms | |
zk_min_latency | 最小延迟 | 异常高 | |
| 资源相关 | zk_open_file_descriptor_count | 打开文件描述符 | 接近限制 |
zk_max_file_descriptor_count | 最大文件描述符 | - |
JMX 监控配置:
# 启用 JMX 远程监控
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
四字命令监控:
# 查看服务器状态
echo stat | nc localhost 2181
# 查看是否存活
echo ruok | nc localhost 2181
# 查看连接数
echo cons | nc localhost 2181
# 查看 watcher 统计
echo wchs | nc localhost 2181
# 查看临时节点
echo dump | nc localhost 2181
故障排查指南
理解 ZAB 协议的工作原理有助于快速定位和解决问题。
常见故障场景
1. Leader 选举失败
现象:
- 集群无法选出 Leader
- 客户端连接失败
- 日志显示持续选举
排查步骤:
# 1. 检查网络连通性
ping <other-server-ip>
telnet <other-server-ip> 2888
telnet <other-server-ip> 3888
# 2. 检查配置文件
# 确保所有节点的 zoo.cfg 中 server.x 配置一致
# 3. 检查 myid 文件
cat /var/lib/zookeeper/myid
# myid 必须与配置文件中的 server.x 对应
# 4. 查看日志
grep -i "election" zookeeper.log
grep -i "leader" zookeeper.log
常见原因及解决:
| 原因 | 解决方案 |
|---|---|
| 网络分区 | 检查网络配置,确保多数节点可达 |
| myid 错误 | 检查并纠正 myid 文件内容 |
| 配置不一致 | 确保所有节点配置文件一致 |
| 端口冲突 | 检查端口占用,修改配置 |
2. 数据同步失败
现象:
- Follower 无法同步 Leader 数据
- 日志显示同步错误
- Follower 不断重连
排查步骤:
# 1. 检查数据目录权限
ls -la /var/lib/zookeeper/
# 2. 检查磁盘空间
df -h
# 3. 检查事务日志完整性
ls -la /var/lib/zookeeper/version-2/
# 4. 查看 Leader 日志
grep -i "sync\|follower" zookeeper.log
解决策略:
# 如果 Follower 数据损坏,可以清除数据重新同步
# 注意:确保 Leader 正常运行
# 1. 停止 Follower
./bin/zkServer.sh stop
# 2. 清除数据目录
rm -rf /var/lib/zookeeper/version-2/*
# 3. 重新启动
./bin/zkServer.sh start
# Follower 会从 Leader 同步完整数据
3. 性能下降
现象:
- 请求延迟增加
- Leader 处理能力下降
- 客户端超时
排查步骤:
# 1. 检查系统资源
top
iostat -x 1
netstat -an | grep 2181 | wc -l
# 2. 检查 JVM 状态
jstat -gc <pid> 1000
jmap -histo <pid> | head -20
# 3. 检查 ZooKeeper 状态
echo stat | nc localhost 2181
echo mntr | nc localhost 2181
# 4. 分析日志
grep -i "WARN\|ERROR" zookeeper.log
优化建议:
| 问题 | 优化方案 |
|---|---|
| 磁盘 I/O 高 | 分离事务日志到独立磁盘 |
| 内存不足 | 增加堆内存,避免交换 |
| 网络延迟高 | 增大 tickTime,优化网络 |
| 连接数过多 | 增加 maxClientCnxns |
| watcher 过多 | 优化客户端 watcher 使用 |
4. 会话丢失
现象:
- 客户端频繁重连
- 临时节点消失
- ConnectionLoss 异常
排查步骤:
# 1. 检查会话超时配置
grep -i "session" zookeeper.log
# 2. 检查网络稳定性
ping <client-ip>
traceroute <client-ip>
# 3. 检查服务器负载
echo stat | nc localhost 2181
解决方案:
# 服务端调整会话超时范围
minSessionTimeout=10000
maxSessionTimeout=100000
# 客户端调整
zk.sessionTimeout=30000
zk.connectionTimeout=10000
日志分析技巧
关键日志模式:
# Leader 选举相关
Looking for leader
Notification: ... (sid, zxid, peerEpoch)
LEADING: sid=...
# 数据同步相关
Sync connected to leader
Received NEWLEADER
Sending DIFF/TRUNC/SNAP
# 事务处理相关
Proposal: zxid=...
Commit: zxid=...
# 错误和警告
WARN [QuorumPeer]
ERROR [QuorumPeer]
日志分析命令:
# 统计 Leader 选举次数
grep "Looking for leader" zookeeper.log | wc -l
# 查看最近的事务
grep "Proposal:" zookeeper.log | tail -20
# 查看错误日志
grep -E "ERROR|WARN" zookeeper.log | tail -50
# 分析延迟分布
grep "latency" zookeeper.log | awk '{print $NF}' | sort -n
恢复策略
数据损坏恢复
# 场景:单个节点数据损坏
# 前提:集群中多数节点正常
# 1. 确认其他节点正常
echo stat | nc node2:2181
echo stat | nc node3:2181
# 2. 停止损坏节点
./bin/zkServer.sh stop
# 3. 清除数据
rm -rf /var/lib/zookeeper/version-2/*
# 4. 重启节点
./bin/zkServer.sh start
# 节点会从 Leader 同步完整数据
集群整体恢复
# 场景:集群多数节点故障
# 前提:至少有一个节点的数据完整
# 1. 停止所有节点
for i in 1 2 3; do ssh node$i "./bin/zkServer.sh stop"; done
# 2. 找出数据最新的节点(ZXID 最大)
for i in 1 2 3; do
echo "Node $i:"
ssh node$i "ls -lt /var/lib/zookeeper/version-2/ | head -5"
done
# 3. 以数据最新的节点为基础恢复
# 可以增加该节点的权重,或先启动该节点
# 4. 逐个启动节点
./bin/zkServer.sh start
最佳实践总结
部署层面:
- 使用奇数台服务器(推荐 3 或 5 台)
- 事务日志使用独立磁盘
- 合理设置 JVM 堆内存,避免交换
- 配置自动清理旧快照和日志
配置层面:
- 根据网络环境调整 tickTime
- 设置合理的会话超时范围
- 限制单客户端连接数
- 启用监控和日志
运维层面:
- 定期监控关键指标
- 设置合理的告警阈值
- 制定故障恢复预案
- 定期演练故障恢复流程
应用层面:
- 合理使用 watcher,避免过度监听
- 控制临时节点数量
- 实现客户端重连机制
- 处理 ConnectionLoss 异常
小结
本章深入学习了 ZAB 协议的核心原理:
- 协议目标:全序广播、容错性、高可用性、状态同步
- 两种模式:消息广播(正常运行)和崩溃恢复(故障处理)
- ZXID 结构:epoch + counter,用于标识和排序事务
- Leader 选举:Fast Leader Election 算法,选择数据最新的节点
- 数据同步:DIFF、TRUNC、SNAP 三种策略
- 一致性保证:顺序一致性、原子性、单一系统映像、可靠性
- 生产实践:集群规划、性能调优、故障排查
ZAB 协议是 ZooKeeper 可靠性的基石。理解 ZAB 协议有助于正确使用 ZooKeeper,排查问题,以及进行性能调优。在生产环境中,需要结合 ZAB 协议的特点,合理规划集群、配置参数、监控运行状态,并准备好故障恢复预案。