ZooKeeper 组合操作原子化

ZooKeeper(ZK) 能够保证一件看起来很自然的事情,就是客户端提交的操作到达服务器后能够按照原子广播协议 ALL OR NONE 的应用到所有集群成员上。换句话说,读取节点上的数据和往节点写入新的数据是两个不会交错的原子过程,从而不会读到一半是旧数据一半是新数据的结果。

然而,早期的 ZK 客户端只支持基础的操作,例如节点的读写和子节点列表等,这些操作的特点是只能针对一个节点进行操作。现实世界里的一种常见需求是原子化地操作多个节点的数据,也就是某种形式的事务。ZK 3.4.0 版本以后引入了 multiop 请求来支持这个场景。

multiop 的适用场景

ZK 提供了两种发起 multiop 的方式,分别形如下列的框架代码。

第一种方式,使用 ZK 客户端的 multi 方法。

ZooKeeper zk = ...;zk.multi(Arrays.asList(  Op.check(...),  Op.create(...)));

第二种方式,使用 ZK 封装的 Transaction 接口。这个接口只支持描述 check 和写操作。

Transaction t = new Transaction();t.check(...);t.create(...);t.commit();

上面列举的代码还有相应的异步回调接口,这里不再罗列。

multiop 的一个典型适用场景是节点的移动,也就是删除节点 a 的同时创建节点 b。

这种场景的实例比如基于 ZK 实现任务调度时,从候选队列挑选一个任务移入执行集合。如果不使用 multiop 而是顺序执行删除和创建,那么两个动作之间客户端崩溃,就有可能丢失任务。

multiop 的另一个典型适用场景是原子地检查节点并执行操作。讲解实现原理的时候我们会提到 ZK 的 multiop 必须是全读或全写操作,但是写操作中可以加入一个节点检查的 check 动作来辅助判断,实现 if-then 式的逻辑。

此前介绍 ZK 网络故障应对法里提及 leadership 在服务器丢失而客户端尚未知悉的问题,为了避免客户端做出越权操作,可以将原先的写操作和检查 leadership 的操作一并放入 multiop 中提交。其中,检查 leadership 的操作就是检查对应 ephemeral sequential 节点的存在性。

multiop 的实现原理

multiop 的分类

multiop 一共接受六种操作,按照事务操作和只读操作分为两组。只读操作包括读取节点数据和读取节点子节点列表,事务操作包括创建节点、删除节点、修改节点数据和检查节点版本号。

ZK 内部将 multiop 分为 multi 和 multiread,分别对应上面的事务操作和只读操作。实现上,事务操作会走原子广播协议,而只读操作会在当前连接的服务器上直接处理完成返回,因此 ZK 并不支持混合两种操作的 multiop。道理上,multiop 的事务操作只支持简单的节点版本号检查,并不能支持取出数据后比对数据的功能,因此混合读写的意义也不大。

具体分析两种操作的执行过程之前,我们先看到 ZK 服务器的线程模型和处理流水线。这两个问题讲清楚以后 multiop 的实现就水到渠成了。

ZK 服务器的流水线

ZK 服务器的流水线分为四种,即 Leader 流水线,Follower 流水线,Observer 流水线和独立服务器的流水线。我们讨论普遍的 ZK 集群,因此略过最后一种的讨论;Observer 的流水线与 Follower 大同小异,但是不参与选举和事务投票,我们只讨论 Follower 流水线。

Leader 流水线如下图所示。

Prep 处理器接受客户端的请求,执行这个请求,并生成一个事务。

Proposal 处理器根据事务准备一个提议,并将该提议同步给 Follower 要求投票。随后,请求被转发给 Commit 处理器。对于写操作请求,还会被转发给 Sync 处理器。

Sync 处理器将事务持久化到磁盘上,并将请求进一步转发给 Ack 处理器,Ack 处理器会生成一个确认消息并提交给自己,这是因为原子广播协议的投票过程 Leader 也需要参加。

Commit 处理器会直接放过不需要投票的读请求,而对于写请求会在收到足够多的确认消息后提交事务,即转发给 Final 处理器。

图示 Final 处理器之前还有一个简单的处理器,即 ToBeApplied 处理器。它的实际执行逻辑在调用 Final 处理器并在事务提交成功之后。也就是说这个处理器的处理逻辑首先调用 Final 处理器,并在 Final 处理器返回之后清理 Leader 用于和 Follower 同步投票过程的队列。

Final 处理器会处理事务的提交,也就是对于写请求,修改 DataTree 并持久化,对于读请求,从 DataTree 中读取数据并返回给客户端。

Follower 流水线如下图所示。

Follower 处理器接受客户端的请求并转发给 Commit 处理器。对于写请求,它还会转发给 Leader 进行处理。

Commit 处理器直接转发读请求到 Final 处理器读取数据并返回给客户端。对于写请求,它会在转发之前等待提交事务。

Follower 收到 Leader 发出的提议后转发给 Sync 处理器,Sync 处理器持久化事务后通过 SendAck 处理器发送确认信息给 Leader。

Commit 处理器确认事务提交后,会将写请求转发给 Final 处理器,后者修改 DataTree 并持久化,随后返回客户端。

实现上,Follower 只有 Follower 处理器和 SendAck 处理器是特殊的,其他三个处理器的逻辑与 Leader 无异。

ZK 服务器的线程模型

再来看到 ZK 服务器的线程模型。

ZK 的并发编程实践非常有趣。它作为一个横跨了十几年开发历史的长寿项目,代码中杂糅了从最初的 Thread + 共享变量 + 中断异常,到 Executor 任务级并发框架,再到 Future / Promise 编程模型等等并发编程技术。有机会我会展开讲解 Java 生态的并发编程发展史,每一个阶段 ZK 代码中都有丰富的实例。

回到 ZK 服务器的线程模型话题,跟 multiop 最为相关的是,multiop 作为客户端请求的一种,同样会走上面提到的流水线进行处理。流水线处理客户端请求的入口是一个独立运转的单线程,随后通过责任链模式和同步队列先进先出地逐个转发到达流水线的末端。

这里的重点是,ZK 服务器处理客户端请求是单线程地逐个处理,中间转发过程是先进先出的。也就是说,multiop 打包的一组操作会作为一个请求保持原来的顺序被集中处理,这样也就不会出现一开始提到的两个操作中间乱入了另一个请求的操作,从而打破原子性的问题。

multiop 的处理过程

multiread 相对简单,无非是一个循环搞定所有读操作后打包返回,没有回滚的问题,这里略过不谈。

multi 的实现由于 ZK 服务器的单线程处理实现,也不会非常复杂。其主要处理过程分三步,在 Prep 处理器上检查操作的可行性,在 Final 处理器中修改 DataTree 也就是修改节点状态,以及在 Final 处理器中打包操作结果并返回。

第一步也是最关键的一步,Prep 处理器循环处理 multiop 打包的操作,通过 pRequest2Txn 方法逐一检查操作的可行性。例如,check 操作校验的节点版本是否正确,create 操作创建的节点是否存在,以及 ACL 的检查等等。由于单线程处理请求的实现,我们可以放心地在流水线线程里逐个检查正确性,并将结果传递到后续过程,而不用担心并发导致的各种状态改变的问题。

如果检查通过,这个过程还会节点状态变更请求放进待处理的变更队列中。由于检查是一个一个进行的,这会部分地改变待处理的变更队列的状态。如果前面的检查通过而后面的检查失败,这些变更请求就要被回滚。因此在检查之前还有一个记录 multiop 涉及的节点路径及其父节点路径的变更的步骤,以支持失败时回滚。记录其父节点路径是因为创建操作会影响父节点的状态。

如果检查失败,除了上述提到的回滚动作以外,还会设置 multiop 失败的相关信息,并快速跳过后面请求的处理。因为 multiop 的语义即一个失败,全部失败。

第二步,如果 multiop 打包的操作全部检查成功,那么在经过原子广播协议在集群内达成一致后,请求就会进入 Final 处理器并委托给 DataTree 来修改持久化数据,DataTree 通过 processTxn 方法来处理 Final 处理器下发的事务。

DataTree 首先整体判断下发的事务是否失败,如果失败则走快速路径全部打包失败结果了事,否则逐个创建对应的操作事务并应用到持久化数据上。

第三步,根据前面处理的成功或失败的结果对应打包处理结果,并在处理结果落盘后发送回客户端。可以看到,这里存在在服务器成功但是客户端由于网络问题不能正确收到结果的情况。具体处理方法可以参考 ZooKeeper 网络故障应对法一文。

相似功能的主题讨论

最近事情比较忙,虽然在写这一期内容时实际还对下面内容进行了调研和分析,但是逐一讨论内容也不少。

如果对下面的内容感兴趣,可以通过文末的知识星球二维码到本文对应的主题下评论 +1,根据反馈考虑要不要展开讲讲。

  • etcd 的事务实现

  • TiKV 的事务实现

  • 算子下推的分析角度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值