跳到主要内容

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 协议比较,两者都是共识协议,但设计侧重点不同:

特性ZABPaxos
设计目标为主从架构设计通用共识协议
Leader 角色必须有且唯一可以有多个提议者
顺序保证全局顺序严格保证只保证单个实例内的顺序
恢复机制内置崩溃恢复需要额外实现
适用场景ZooKeeper 协调服务通用分布式共识

ZAB 协议针对 ZooKeeper 的工作负载进行了优化。ZooKeeper 的工作负载特点是读多写少,而且需要一个稳定的 Leader 来处理所有写请求。ZAB 协议的 Leader 机制简化了协议复杂度,提高了写入效率。

与 Raft 协议的对比​

Raft 协议是近年来广受欢迎的共识协议,与 ZAB 有许多相似之处,但也存在关键差异:

特性ZABRaft
Leader 选举基于 ZXID 选举基于 Term 和日志位置选举
日志顺序全局单调递增的 ZXIDTerm + 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实现简单,生态丰富
高写入吞吐量RaftLeader 优化更好
需要快速恢复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 ───────────────────▶│ │
│ │ │ │ │ │
│ │◀─ 响应 ──────│ │ │ │
│ │ │ │ │ │
│ │
└─────────────────────────────────────────────────────────────┘

详细步骤:

  1. 接收请求:Leader 接收客户端的写请求,将请求数据封装成事务。

  2. 创建 Proposal:Leader 为事务分配全局唯一的 ZXID,创建 Proposal(提议)。ZXID 是一个 64 位数字,高 32 位是 epoch(纪元),低 32 位是计数器。

  3. 广播 Proposal:Leader 将 Proposal 发送给所有 Follower。发送顺序严格按照 ZXID 的顺序。

  4. Follower 处理:

    • Follower 收到 Proposal 后,先将其写入本地事务日志
    • 写入成功后,向 Leader 发送 ACK(确认)
    • 此时事务尚未应用到状态机
  5. 收集 ACK:Leader 等待收到多数 Follower 的 ACK。收到多数确认后,事务就可以提交了。

  6. 广播 Commit:Leader 发送 Commit 消息给所有 Follower。

  7. 提交事务: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:服务器最新的 ZXID
  • epoch:当前的逻辑时钟

选举规则:

比较优先级:
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 个节点故障。

性能优化建议​

  1. 合理设置 tickTime:

    • 默认 2000ms,可以根据网络延迟调整
    • 较小的 tickTime 可以加快故障检测,但增加误判风险
  2. 使用 Observer:

    • Observer 不参与投票,可以无限扩展
    • 适合读密集型场景
  3. 调整日志和快照策略:

    • snapCount 控制快照频率
    • 定期清理旧的日志和快照
  4. 分离事务日志和快照存储:

    • 使用不同的磁盘可以提高 I/O 性能

生产环境最佳实践​

理解 ZAB 协议后,在生产环境中部署和运维 ZooKeeper 需要遵循一些关键原则,以确保系统的稳定性和高性能。

集群规模规划​

服务器数量选择:

ZooKeeper 集群需要多数节点存活才能工作,因此建议使用奇数个服务器:

集群规模容忍故障数适用场景
1 台0开发测试环境
3 台1小规模生产环境
5 台2关键生产环境
7 台3大规模高可用场景

规划原则:

  1. 最小生产配置:3 台服务器,可容忍 1 台故障
  2. 维护友好配置:5 台服务器,可在维护期间容忍额外故障
  3. 避免偶数配置: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 或专用磁盘

磁盘性能指标:

磁盘类型顺序写入延迟推荐场景
HDD5-10ms开发环境
SATA SSD0.1-0.5ms一般生产环境
NVMe SSD0.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_countznode 数量过大影响性能
zk_watch_countwatcher 数量过多影响性能
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

最佳实践总结​

部署层面:

  1. 使用奇数台服务器(推荐 3 或 5 台)
  2. 事务日志使用独立磁盘
  3. 合理设置 JVM 堆内存,避免交换
  4. 配置自动清理旧快照和日志

配置层面:

  1. 根据网络环境调整 tickTime
  2. 设置合理的会话超时范围
  3. 限制单客户端连接数
  4. 启用监控和日志

运维层面:

  1. 定期监控关键指标
  2. 设置合理的告警阈值
  3. 制定故障恢复预案
  4. 定期演练故障恢复流程

应用层面:

  1. 合理使用 watcher,避免过度监听
  2. 控制临时节点数量
  3. 实现客户端重连机制
  4. 处理 ConnectionLoss 异常

小结​

本章深入学习了 ZAB 协议的核心原理:

  1. 协议目标:全序广播、容错性、高可用性、状态同步
  2. 两种模式:消息广播(正常运行)和崩溃恢复(故障处理)
  3. ZXID 结构:epoch + counter,用于标识和排序事务
  4. Leader 选举:Fast Leader Election 算法,选择数据最新的节点
  5. 数据同步:DIFF、TRUNC、SNAP 三种策略
  6. 一致性保证:顺序一致性、原子性、单一系统映像、可靠性
  7. 生产实践:集群规划、性能调优、故障排查

ZAB 协议是 ZooKeeper 可靠性的基石。理解 ZAB 协议有助于正确使用 ZooKeeper,排查问题,以及进行性能调优。在生产环境中,需要结合 ZAB 协议的特点,合理规划集群、配置参数、监控运行状态,并准备好故障恢复预案。

参考资料​