bigywater.tech
ZooKeeper · 84

用 ZooKeeper 实现两阶段提交协调器

把「多个参与者要么都做、要么都不做」交给 ZooKeeper 来协调:投票、决定都存在节点里,崩溃后别的进程能接着做。这篇我自己写一个最小的两阶段提交,分别验证全票通过、有人反对、参与者崩溃、协调者崩溃四种情形。

52 ms
20 个全 yes 事务从 prepare 到全部应用的中位耗时
3 s
参与者崩溃时协调者的投票超时,之后写 abort
阻塞
协调者在写 decision 前崩溃,参与者无法自行决定,需要恢复进程

正常提交

实测 · 本机执行输出
  20 个事务中应用完成的:20;耗时(ms)中位 52,最小 35,最大 61
  一个事务在 ZooKeeper 里的节点: ['decision', 'prepare', 'votes'] | votes: ['P1', 'P2', 'P3'] | decision = commit

ZooKeeper 在这里的作用是一个可靠、有序、可 watch 的共享记录板。它自己不是事务协调器,协议是我写的最小实现(没有处理参与者重复投票、网络分区下的 watch 丢失等)。每个事务三个节点:prepare、votes(每个参与者一个子节点)、decision。

有人反对

实测 · 本机执行输出
  COORD  PREPARE_SENT tno
  P2     PREPARE tno
  P1     PREPARE tno
  P3     PREPARE tno
  P1     VOTE tno yes
  P2     VOTE tno no
  P3     VOTE tno yes
  COORD  VOTES tno {'P2': 'no', 'P3': 'yes'}
  COORD  DECISION tno abort
  P1     APPLIED tno abort
  P2     APPLIED tno abort
  P3     APPLIED tno abort
  decision = abort;应用了 abort 的参与者:['P1', 'P2', 'P3']

协调者看到 no 就不再等其余的票(日志里 VOTES 只记录了 P2 和 P3 的票,P1 的票虽然也写了但协调者没来得及读)。

参与者崩溃

实测 · 本机执行输出
  协调者用时 3.1 s(投票超时 3 s);decision = abort;votes 目录里有 ['P1', 'P2']
  P1、P2 应用了: [('P1', 'APPLIED tdie abort'), ('P2', 'APPLIED tdie abort')]
  /members 现在: ['P1', 'P2'] (协调者已等了 3 秒,P3 的会话已过期、临时节点已消失)

没有票就不能提交,所以超时视为 no。超时设短了会误杀慢参与者,设长了让事务久悬(这期间参与者持有的资源锁不能释放)。

协调者崩溃:2PC 的老问题

实测 · 本机执行输出
  协调者被杀后 5 秒:/tx/tcoord/decision 存在 = False;已应用的参与者数 = 0;votes = {'P1': 'yes', 'P2': 'yes'}
  + 0.09s  RECOVER  RECOVER_VOTES {'P1': 'yes', 'P2': 'yes'}
  + 0.10s  RECOVER  RECOVER_DECISION commit
  + 0.10s  P2       APPLIED tcoord commit
  + 0.10s  P1       APPLIED tcoord commit
  恢复者读出所有票为 yes → 写 commit;参与者应用数 = 2

阻塞问题是 2PC 本身固有的,ZooKeeper 缓解了其中一部分:投票持久地保存在节点里,所以任何进程都能读到完整的票并替协调者做决定(如果所有票都在,就能确定结果)。两个写 decision 的人都用 create,按设计后写的会得到 NodeExists、从而决定只有一个(这个并发竞争我没有测)。但如果有参与者的票没来得及写,恢复进程只能选 abort。

分步演示

两阶段提交
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / ZooKeeper 3.9.6(brew 安装的 jar,用 java -cp 直接启动自己的实例,OpenJDK 17 Temurin,JVM 堆 128 MiB)/ 本机回环端口 7300+,数据在临时目录;默认 tickTime=2000;计时用 time.perf_counter,数字每次运行会有小幅抖动。协议是我写的最小实现;协调者与参与者都是独立子进程,会话超时 3 s;事务 20 个全 yes,另各 1 个 no / 参与者崩溃 / 协调者崩溃
    • 没有验证:参与者在投 yes 之后崩溃再恢复的行为(应从 votes 与 decision 重建状态)、「恢复进程写 NodeExists 保证单一决定」的并发竞争(原协调者与恢复进程同时写 decision)、跨数据库的真实事务资源、与 XA / Seata 之类框架的比较
    京ICP备2026067502号-1京公网安备11010502063487号