bigywater.tech
ZooKeeper · 90

ZooKeeper 与分布式任务调度

调度不一定要有 master:让所有 worker 去抢一个「只能有一个成功」的节点就行。这篇用 create 的原子性做两件事 —— 无 master 的任务抢占,以及多实例部署时的定时任务单实例执行 —— 并在执行中途杀进程,看任务怎么被接走。

300 / 300
5 个 worker 抢任务:全部完成、0 个重复
3.6 s
执行中被 kill -9 的任务被别的 worker 接手
25 / 25
3 个实例抢定时 tick:全部执行,0 个重复,0 个遗漏

抢占:create 就是互斥

实测 · 本机执行输出
  完成 300 / 300,用时 4.3 s;执行次数 >1 的任务:[];各 worker 抢到的任务数 {'W0': 62, 'W1': 56, 'W2': 63, 'W3': 60, 'W4': 59}

这是分布式锁最简单的形态:每个任务一个节点名,只有一个 create 能成功。和有 master 的方案相比没有单点,但每个 worker 都要扫描 /tasks,任务多时目录大(见大目录)。

崩溃与接手

实测 · 本机执行输出
  第一次 START 由 W1 开始;我在 START 后 0.0 s 时 kill -9 它
  第二次 START(接手者):W3,在 kill 后 3.6 s;END 在 kill 后 6.6 s,由 W3 完成;/done/slow00 的写入者 = W3
  slow00 一共 START 2 次、END 1 次 —— 至少一次执行:被杀的那一次做了一半,任务必须幂等

claim 是临时节点,所以崩溃的 worker 持有的任务在会话过期后自动释放。因为无法区分「还没开始」和「做了一半」,只能提供至少一次的语义。完成标记 /done 是在任务做完之后才写的。

定时任务只跑一次

实测 · 本机执行输出
  kill -9 N0(前 10 个 tick 它执行了 5 次)
  25 个 tick:被执行的 25 个,漏掉 [],重复执行 [];各实例执行数 {'N0': 5, 'N1': 9, 'N2': 11}
  执行时刻相对 tick 整点的偏差(ms):中位 10,最大 16

每个 tick 用一个持久节点占位,谁先 create 成功谁执行。要注意:如果抢到的实例在 create 之后、执行完之前崩溃,这个 tick 就丢了(节点已存在,别人不会再做)—— 我没有测这种情况。需要「一定执行」就要像上面的 claim 一样用临时节点加完成标记。所有实例在同一台机器上,时钟没有偏差;跨机器要考虑时钟差。

分步演示

任务调度
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / ZooKeeper 3.9.6(brew 安装的 jar,用 java -cp 直接启动自己的实例,OpenJDK 17 Temurin,JVM 堆 128 MiB)/ 本机回环端口 7300+,数据在临时目录;默认 tickTime=2000;计时用 time.perf_counter,数字每次运行会有小幅抖动。worker 与定时实例都是独立的 Python 子进程(kazoo);会话超时 3 s;5 个 worker 在同一台机器
    • 没有验证:抢到 tick 后崩溃导致该 tick 丢失(见正文)、跨机器时钟偏差、延迟任务(到点才可抢)、任务数上万时的扫描成本、与 Quartz / xxl-job 等调度框架的对比
    京ICP备2026067502号-1京公网安备11010502063487号