bigywater.tech
ZooKeeper · 20

ZooKeeper 请求幂等与重试语义

客户端收到 ConnectionLoss 时,请求到底成功了没有?这是 ZooKeeper 使用中最容易出错的地方。这篇用一个会丢弃响应的 TCP 代理构造「服务端执行了、客户端不知道」的情形,看每种操作重试时会发生什么。

ConnectionLoss
结果未知:请求可能已经被服务端执行
重试顺序节点
会创建出第二个节点,没有幂等性
BadVersion / NoNode
重试带版本的写或 delete 得到的错误,可能意味着「第一次其实成功了」

构造响应丢失

实测 · 本机执行输出
  第一次调用客户端看到: ConnectionLoss | 服务端实际有 /job: True
  客户端重试 create →  NodeExistsError

真实环境里的网络抖动、服务端切换都会造成这种情形。这里的代理是我用 Python 写的:转发客户端请求、丢弃服务端响应、随后断开连接,让客户端必然看到 ConnectionLoss。

顺序节点

实测 · 本机执行输出
  第一次调用客户端看到: ConnectionLoss | 服务端此时 /seq 的子节点: ['task-0000000000']
  客户端重试 create(sequence) 之后: ['task-0000000000', 'task-0000000001']

版本号与删除

实测 · 本机执行输出
  第一次调用客户端看到: ConnectionLoss | 服务端数据: b'b'  version: 1
  客户端用同一个 version 重试 set → BadVersionError
  第一次调用客户端看到: ConnectionLoss | 服务端已删除: True
  客户端重试 delete → NoNodeError

覆盖写

实测 · 本机执行输出
  第一次调用客户端看到: ConnectionLoss | 服务端数据: b'z'
  重试后数据: b'z' | version = 2 (被写了两次,版本号 +2)

怎么办

总结实测出来的规律:操作能不能安全重试,取决于它是不是「重放结果不变」。覆盖写数据是,顺序节点创建不是,带版本的写需要把 BadVersion 当作「可能已成功」处理。通用做法是让每个请求带一个唯一标识(写进节点数据或节点名),重试前先查询是否已经落地。这是设计建议,本文只验证了上面的现象,没有验证具体的去重方案。

分步演示

重试语义
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / ZooKeeper 3.9.6(brew 安装的 jar,用 java -cp 直接启动自己的实例,OpenJDK 17 Temurin,JVM 堆 128 MiB)/ 本机回环端口 7300+,数据在临时目录;默认 tickTime=2000;计时用 time.perf_counter,数字每次运行会有小幅抖动。代理是 zkl.Proxy(drop_resp 丢弃服务端响应后 close_all),客户端 kazoo;每个场景只构造一次丢失
    • 没有验证:kazoo / Curator 内置重试策略的实际行为(第 97 篇)、会话过期后重试、集群里切换 leader 时的重复提交、multi 操作的重试(第 21 篇)
    京ICP备2026067502号-1京公网安备11010502063487号