跳到主要内容

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 协议的特点,合理规划集群、配置参数、监控运行状态,并准备好故障恢复预案。

参考资料