Skip to content

ZooKeeper 事务日志损坏与恢复

本文分析一个真实的 ZooKeeper 事务日志损坏案例,日志来源是 K8s 环境中 Kafka 集群的 ZK 节点(zookeeper-kafka-1)启动时产生。

背景

ZooKeeper 版本3.6.3
运行环境Kubernetes StatefulSet,namespace kafka
节点角色myid=2,FOLLOWER
集群规模3 节点 ZK 集群
部署方式Headless Service + StatefulSet

故障现象

ZK 节点启动时出现大量 ERROR 日志:

2026-06-02 10:41:57,412 [myid:2] - ERROR [main:Util@166] - Last transaction was partial.
2026-06-02 10:41:58,712 [myid:2] - ERROR [main:Util@166] - Last transaction was partial.
2026-06-02 10:42:00,510 [myid:2] - ERROR [main:FileTxnSnapLog@336] - 133144055776(highestZxid) > 133144055743(next log) for type -10
2026-06-02 10:42:00,511 [myid:2] - ERROR [main:FileTxnSnapLog@336] - 133144055776(highestZxid) > 133144055744(next log) for type -11
...
  • "Last transaction was partial":最后一条事务日志不完整(写入到一半时进程终止)
  • "highestZxid > next log":快照期望的事务 ID 大于实际日志文件中的下一条事务 ID,说明快照和事务日志之间存在断层
  • type -10 / -11:这些是 Apache BookKeeper 的操作类型,表明上层使用了 BookKeeper 做分布式日志存储

根因分析

事务日志写入机制

ZooKeeper 每次状态变更都采用先写事务日志(Write-Ahead Log)再更新内存的顺序:

Client Request


Leader 接收写请求 → 生成 Proposal → 广播给所有 Follower

    ├── Follower 收到 Proposal → 写入 txn log → 返回 ACK

    └── 超半数 ACK → Leader commit → 更新内存 DataTree

         └── 定期 snapshot(快照序列化 DataTree 到磁盘)

损坏原因

正常流程:
  ┌─────────────┐     ┌──────────────┐     ┌──────────────┐
  │ 写入 txn log │ ──► │ fsync 到磁盘  │ ──► │ 更新 zxid    │
  └─────────────┘     └──────────────┘     └──────────────┘

异常终止:
  ┌─────────────┐     ┌──────────────┐     X 进程被 kill
  │ 写入 txn log │ ──► │ 部分 fsync    │ ──► │ zxid 未更新  │
  └─────────────┘     └──────────────┘


    磁盘上有半条事务记录 → "Last transaction was partial"

K8s 环境中常见触发场景:

原因说明
Pod OOMKilled内存超限被 Kubelet 杀死
Node 驱逐节点资源不足,Pod 被驱逐
强制删除kubectl delete pod --force --grace-period=0
节点故障物理机宕机,无优雅关闭
PreStop hook 缺失StatefulSet 未配置 lifecycle.preStop,终止信号后直接 SIGTERM

zxid 不匹配

Snapshot 中最高 zxid:  0x1e000004af (快照时刻的事务 ID)
快照后产生的事务:       log.5900002c8a (日志文件名编码了起始 zxid)

gap = 0x5900002c8a - 0x1e000004af = 大量的后续事务

快照是定期生成的(默认 snapCount=100000,即每 10 万条事务做一次快照),两次快照之间的事务日志文件是增量。如果最近的事务日志损坏,快照仍然是可用的。

恢复过程

ZooKeeper 的恢复过程分三个阶段:

1. 加载本地快照(Snapshot)

2026-06-02 10:41:40,808 [myid:2] - INFO  [main:FileSnap@85] - Reading snapshot /data/version-2/snapshot.1e000004af
2026-06-02 10:42:03,108 [myid:2] - INFO  [main:FileTxnSnapLog@363] - 86470 txns loaded in 20601 ms
2026-06-02 10:42:03,109 [myid:2] - INFO  [main:ZKDatabase@289] - Snapshot loaded in 22898 ms, highest zxid is 0x5900002c89
  • 从最新有效快照恢复 DataTree
  • 重放快照之后的有效事务日志(86470 条),耗时约 20s
  • 快照最高 zxid 为 0x5900002c89,已恢复到快照关闭前的状态

损坏的事务日志 ERROR 只是警告信息,ZK 会跳过这些损坏的日志记录。日志行 112-143 显示大约 32 条损坏的事务被跳过(highestZxid > next log),这些事务原本应该存在于快照关闭之后,但由于日志损坏无法恢复。

2. Leader 选举

2026-06-02 10:42:08,109 [myid:2] - INFO  - LOOKING
2026-06-02 10:42:08,210 [myid:2] - INFO  - New election. My id = 2, proposed zxid=0x5900002c89
2026-06-02 10:42:10,112 [myid:2] - INFO  - FOLLOWING
2026-06-02 10:42:11,011 [myid:2] - INFO  - LEADER ELECTION TOOK - 2801 MS
  • 节点进入 LOOKING 状态,发起选举
  • 收到 myid=3 (LEADING, zxid=0x5800000005) 和 myid=1 (FOLLOWING, leader=3) 的通知
  • 本节点 zxid 更高但 Leader 已经确认,转为 FOLLOWING
  • 选举耗时 2.8 秒

3. Leader 同步(Diff 同步)

2026-06-02 10:42:11,508 [myid:2] - INFO  [Learner@551] - Getting a diff from the leader 0x5900002d3f
2026-06-02 10:42:13,510 [myid:2] - INFO  [Learner@701] - Learner received UPTODATE message
  • 从 Leader 拉取缺失的事务(zxid 0x5900002c8a0x5900002d3f,约 182 条)
  • 通过 diff 方式同步(只同步差异,而非全量 SNAP 同步),说明大部分数据一致
  • 收到 UPTODATE 后进入 BROADCAST 状态,正式对外服务
2026-06-02 10:42:13,809 [myid:2] - INFO  [QuorumPeer@864] - Peer state changed: following - broadcast

恢复时间线

10:41:25  读取配置
10:41:40  开始加载快照
10:42:03  快照加载完成(22.9s),zxid=0x5900002c89
  ↓  Leader 选举
10:42:11  完成选举,转为 FOLLOWING(2.8s)
  ↓  Leader 同步
10:42:13  同步完成(2.0s)
10:42:13  进入 BROADCAST,对外服务
───────────────────────────
总恢复时间: ~48 秒

总结

要点说明
故障本质Pod 异常终止导致事务日志写入不完整
恢复手段快照恢复 + 跳过损坏日志 + Leader diff 同步
数据安全3 节点多数派 + 快照机制保证了数据完整性,未丢失已提交数据
恢复时间~48s(快照加载 23s + 选举 3s + 同步 2s)

预防措施

措施说明
配置 PreStop hookStatefulSet 中设置 lifecycle.preStop,给 ZK 足够时间优雅关闭
增加资源 limit合理设置 JVM heap 和 memory limit,避免 OOMKilled
使用 PodDisruptionBudget限制同时不可用的 ZK Pod 数量
监控 ZK 指标关注 fsync 耗时、pending syncs、outstanding requests
KRaft 迁移长期方案:迁移到 KRaft 模式,消除 ZK 依赖

参考