聊聊「ZooKeeper」(下)

阅读本文前,请先阅读我的文章  聊聊「分布式系统」聊聊「ZooKeeper」(上)

聊聊「ZooKeeper」(上) 中我举了一个分布式环境下各个节点保持数据一致性的例子,针对这种场景,我们看看 ZooKeeper 是怎么做的。

先看看 ZooKeeper 是什么

ZooKeeper 是一个开放源代码的分布式协调服务,由知名互联网公司雅虎创建,是Google Chubby的开源实现。ZooKeeper的设计目标将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。ZooKeeper 是一个典型的分布式数据一致性方案

ZooKeeper是一个分布式数据一致性解决方案,为分布式应用提供一个高性能、高可用,且具有严格顺序访问控制能力的分布式协调服务。

可以看到, ZooKeeper 是一个『分布式数据一致性解决方案』。

那么具体来说,ZooKeeper 到底可以做什么?

它可以保证分布式数据具有以下一致性:

  1. 顺序一致性
  2. 原子性
  3. 单一视图(Single System Image
  4. 可靠性
  5. 实时性

 

1. 顺序一致性
从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到 ZooKeeper 中去。

2. 原子性
所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况。

3. 单一视图(Single System Image)
无论客户端连接的是哪个 ZooKeeper 服务器,其看到的服务端数据模型都是一致的。

4. 可靠性
一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。

5. 实时性
通常人们看到实时性的第一反应是,一旦一个事务被成功应用,那么客户端能够立即从服务端上读取到这个事务变更后的最新数据状态。这里需要注意的是,ZooKeeper 仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态


ZooKeeper 的设计目标

  1. 简单数据模型
  2. 可以构建集群
  3. 顺序访问
  4. 高性能

 

1. 简单数据模型
ZooKeeper 将全量数据存储在内存中,ZooKeeper 的内存模型是一个树形结构。

2. 可以构建集群
一般由 3-5 台机器就能组成一个 ZooKeeper 集群,集群中每台机器之间都保持通信。只要集群中超过一半的机器能够正常工作,整个 ZooKeeper 集群就能正常对外提供服务。ZooKeeper 客户端会选择和 ZooKeeper 集群中任意一台机器创建一个 TCP 连接。

3. 顺序访问
对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增序号,这个序号代表了所有事务操作的先后顺序。

4. 高性能
ZooKeeper 适合以读为主的应用场景。经测试 ZooKeeper 的读性能能达到 10万+ QPS。


ZooKeeper 中的基本概念

1. 集群角色
Leader :Leader 服务器为客户端提供读和写服务。
Follower :Follower 服务器为客服端提供读服务。参与 Leader 选举。
Observer :Observer 服务器为客服端提供读服务。不参与 Leader 选举。

Follower及 Observer 统称 Learner。Learner 需要同步 Leader 的数据。Follower 还参与选举及事务决策过程。

2. 会话(Seesion)
ZooKeeper 客户端和 ZooKeeper 服务端的一次会话。

3. 数据节点(ZNode)
持久节点创建之后就会一直存在,除非用户主动删除。
临时节点临时节点的生命周期和客户端会话绑定,会话结束,则临时节点销毁。该节点的生命周期依赖于创建它们的会话。一旦会话(Session)结束,临时节点将被自动删除,当然可以也可以手动删除。虽然每个临时的Znode都会绑定到一个客户端会话,但他们对所有的客户端还是可见的。另外,ZooKeeper 的临时节点不允许拥有子节点。

4. 版本
ZooKeeper 的每个 ZNode 上都会存储数据,对应于每个 ZNode ,ZooKeeper 都会为其维护一个叫作 Stat 的数据结构,Stat 中记录了这个 ZNode 的三个数据版本,分别是 version (当前 ZNode 的版本)、cversion ((当前 ZNode 子节点的版本)和 aversion (当前 ZNode 的 ACL 版本)。

5. Watcher
Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher ,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。

6. ACL
ZooKeeper 采用 ACL(Access Control Lists) 策略来进行权限控制。


ZAB 协议

ZAB 协议是一个『原子广播协议』,全称是“Zookeeper Atomic Broadcast”,是为 Zookeeper 所专门设计的一种支持崩溃恢复的原子广播协议。ZAB 协议并不像 Paxos 算法那样是一种通用的分布式一致性算法,而是专为 Zookeeper 所设计的。

在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。具体来说,ZooKeeper 使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用 ZAB 的原子广播协议,将服务器数据的状态变更以事务 Proposal 的形式广播到所有的副本进程上去。

ZAB 协议的这个主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更。另一方面,考虑到在分布式环境中,顺序执行的一些状态变更其前后会存在一定的依赖关系,有些状态变更必须依赖于比它早生成的那些状态变更,例如变更 C 需要依赖变更 A 和变更 B。这样的依赖关系也对 ZAB 协议提出了一个要求:ZAB 协议必须能够保证一个全局的变更序列被顺序应用,也就是说,ZAB 协议需要保证如果一个状态变更已经被处理了,那么所有其依赖的状态变更都应该已经被提前处理掉了。最后,考虑到主进程在任何时候都有可能出现崩溃退出或重启现象,因此,ZAB 协议还需要做到在当前主进程出现上述异常情况的时候,依旧能够正常工作。

所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为 Leader 服务器,而余下的其他服务器则成为 Follower 服务器。

Leader 服务器负责将一个客户端事务请求转换成一个事务 Proposal(提议),并将该 Proposal 分发给集群中所有的 Follower 服务器。之后 Leader 服务器需要等待所有 Follower 服务器的反馈,一旦超过半数的 Follower 服务器进行了正确的反馈后,那么 Leader 就会再次向所有的 Follower 服务器分发 Commit 消息,要求其将前一个 Proposal 进行提交。

ZAB 协议包括两种基本的模式:崩溃恢复模式消息广播模式

崩溃恢复模式

在 ZooKeeper 集群在启动过程中,或者 Leader 服务器出现网络中断、崩溃推出、重启等异常情况,ZAB 协议就会进入『崩溃恢复模式』并选举出新的 Leader 服务器。当选举出了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。

简言之,「崩溃恢复模式」目的是“选出新的 Leader ”以及“选出新 Leader 后集群状态同步”。

消息广播模式

当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入『消息广播模式』了。当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,ZooKeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理。Leader 服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非 Leader 服务器会首先将这个事务请求转发给 Leader 服务器

ZAB 协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交过程。针对客户端的事务请求, Leader 服务器会为其生成对应的事务 Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交。在 ZAB 协议的二阶段提交过程中,所有的 Follower 服务器要么正常反馈 Leader 提出的事务 Proposal ,要么就抛弃 Leader 服务器。同时,ZAB 协议将二阶段提交中的中断逻辑移除意味着我们可以在过半的 Follower 服务器已经反馈 Ack 之后就开始提交事务 Proposal 了,而不需要等待集群中所有的 Follower 服务器都反馈响应。当然,在这种简化了的二阶段提交模型下,是无法处理 Leader 服务器崩溃退出而带来的数据不一致问题的,因此在 ZAB 协议中添加了另一个模式,即采用崩溃恢复模式来解决这个问题。另外,整个消息广播协议是基于具有 FIFO 特性的 TCP 协议来进行网络通信的,因此能够很容易地保证消息广播过程中消息接收与发送的顺序性

在整个消息广播过程中,Leader 服务器会为每个事务请求生成对应的 Proposal 来进行广播,并且在广播事务 Proposal 之前,Leader 服务器会首先为这个事务 Proposal 分配一个全局单调递增的唯一 ID,我们称之为事务 ID (即 ZXID )。由于 ZAB 协议需要保证每一个消息严格的因果关系,因此必须将每一个事务 Proposal 按照其 ZXID 的先后顺序来进行排序与处理

具体的,在消息广播过程中,Leader 服务器会为每一个 Follower 服务器都各自分配一个单独的队列,然后将需要广播的事务 Proposal 依次放入这些队列中去,并且根据 FIFO 策略进行消息发送。每一个 Follower 服务器在接收到这个事务 Proposal 之后,都会首先将其以事务日志的形式写人到本地磁盘中去,并且在成功写入后反馈给 Leader 服务器一个 Ack 响应。当 Leader 服务器接收到超过半数 Follower 的 Ack 响应后,就会广播一个 Commit 消息给所有的 Follower 服务器以通知其进行事务提交,同时 Leader 自身也会完成对事务的提交,而每一个 Follower 服务器在接收到 Commit 消息后,也会完成对事务的提交。

简言之,「消息广播模式」就是 Leader 把数据同步给各个 Follower。

Leader 会处理所有事务请求,如果 Follower 接收到事务请求会转发给 Leader 处理。Leader 会对每个事务请求生成一个全局唯一递增的事务 ID (ZXID),并按照事务 ID 的先后顺序处理事务。Leader 会为每个 Follower 各自分配一个 FIFO 队列,将要广播给各个 Follower 的事务放进这个 FIFO 队列中发送,这样就能保证各个 Follower 接收事务数据的顺序和 Leader 处理事务数据的顺序是一样的。每个 Follower 接收到 Leader 发送来的事务数据,会先到自己磁盘上的事务日志文件中,成功写入后 Ack 给 Leader 一个响应,Leader 收到过半数 Follower 的响应就开始广播 Commit 消息并提交事务,每个 Follower 接收到 Leader 的 Commit 消息就开始提交事务。

ZAB协议需要确保那些「已经在 Leader 服务器上提交的事务」最终被所有服务器都提交。

假设一个事务在 Leader 服务器上被提交了,并且已经得到过半 Follower 服务器的 Ack 反馈,但是在它将 Commit 消息发送给所有 Follower 机器之前,Leader 服务器挂了。

如上图,Server1 是 Leader 服务器,其先后广播了消息 Pl、P2、C1、P3 和 C2,其中,当 Leader 服务器将消息 C2(C2 是 Commit Of Proposal2 的缩写,即提交事务 Proposal2)发出后就立即崩溃退出了。针对这种情况, ZAB 协议就需要确保事务 Proposal2 最终能够在所有的服务器上都被提交成功,否则将出现不一致。

ZAB 协议需要确保丢弃那些只在 Leader 服务器上被提出的事务。

如上图所示,假设初始的 Leader 服务器 Server1 在提出了一个事务 Proposal3 之后就崩溃退出了,从而导致集群中的其他服务器都没有收到这个事务 Proposal。于是,当 Server1 恢复过来再次加入到集群中的时候,ZAB 协议需要确保丢弃 Proposal3 这个事务。

总结一下就是:能够确保提交已经被 Leader 提交的事务 Proposal,同时丢弃已经被跳过的事务Proposal。

简言之,如果一个事务经过了投票、Ack 然后已经被 Leader 提交,则必须保证该事务在集群中每个机器上都提交(即使 Leader 提交之后集群崩溃没有广播 Commit 消息);而如果一个事务没有经过投票或者经过投票但 Leader 还没有提交(不管如何总之就是该事务 Leader 没有提交),则该事务必须被丢弃/跳过。


ZooKeeper 选主(Leader)过程

选主阶段,集群间互传的消息称为投票,投票Vote主要包括二个维度的信息:ID、ZXID :
(1)ID 是被推举的 Leader 的服务器 ID,集群中的每个 zk 节点启动前就要配置好这个全局唯一的 ID。
(2)ZXID 是被推举的 Leader 的事务ID ,该值是从机器 DataTree 内存中取的,即事务已经在机器上被 commit 过了(注意 ZXID 的前 32 位代表选举周期,后 32 位是一个递增序列值)。

节点进入选举阶段后的大体执行逻辑如下:
(1)设置状态为LOOKING,初始化内部投票Vote (id,zxid) 数据至内存,并将其广播到集群其它节点。节点首次投票都是选举自己作为 Leader ,将自身的服务ID、处理的最近一个事务请求的ZXID(ZXID是从内存数据库里取的,即该节点最近一个完成 commit 的事务 id )及当前状态广播出去。然后进入循环等待及处理其它节点的投票信息的流程中。
(2)循环等待流程中,节点每收到一个外部的 Vote 信息,都需要将其与自己内存 Vote 数据进行PK,规则为取 ZXID 大的,若 ZXID 相等,则取 ID 大的那个投票。若外部投票胜选,节点需要将该选票覆盖之前的内存 Vote 数据,并再次广播出去;同时还要统计是否有过半的赞同者与新的内存投票数据一致,无则继续循环等待新的投票,有则需要判断 Leader 是否在赞同者之中,在则退出循环,选举结束,根据选举结果及各自角色切换状态, Leader 切换成 LEADING、Follower 切换到 FOLLOWING、Observer 切换到 OBSERVING 状态。

数据同步

完成 Leader 选举之后,在正式开始工作(即接收客户端的事务请求,然后提出新的提案)之前,Leader 服务器会首先确认事务日志中的所有 Proposal 是否都已经被集群中过半的机器提交了,即是否完成数据同步。下面我们就来看看 ZAB 协议的数据同步过程。所有正常运行的服务器,要么成为 Leader,要么成为 Follower 并和 Leader 保持同步。
Leader 服务器需要确保所有的 Follower 服务器能够接收到每一条事务 Proposal,并且能够正确地将所有已经提交了的事务 Proposal 应用到内存数据库中去。具体的,Leader 服务器会为每一个 Follower 服务器都准备一个队列,并将那些没有被各 Follower 服务器同步的事务以 Proposal 消息的形式逐个发送给 Follower 服务器,并在每一个 Proposal 消息后面紧接着再发送一个 Commit 消息,以表示该事务已经被提交。等到 Follower 服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功应用到本地数据库中后,Leader 服务器就会将该 Follower 服务器加入到真正的可用 Follower 列表中,并开始之后的其他流程。


上面讲到的是正常情况下的数据同步逻辑,下面来看 ZAB 协议是如何处理那些需要被丢弃的事务 Proposal 的。在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位可以看作是一个简单的单调递增的计数器,针对客户端的每一个事务请求,Leader 服务器在产生一个新的事务 Proposal 的时候,都会对该计数器进行加l操作;而高 32 位则代表了 Leader 周期 epoch 的编号,每当选举产生一个新的 Leader 服务器,就会从这个 Leader 服务器上取出其本地日志中最大事务 Proposal 的 ZXID,并从该 ZXID 中解析出对应的 epoch 值,然后再对其进行加1操作,之后就会以此编号作为新的 epoch,并将低 32 位置 0 来开始生成新的ZXID。ZAB 协议中的这一通过 epoch 编号来区分 Leader 周期变化的策略,能够有效地避免不同的 Leader 服务器错误地使用相同的 ZXID 编号提出不一样的事务 Proposal 的异常情况,这对于识别在 Leader 崩溃恢复前后生成的 Proposal 非常有帮助,大大简化和提升了数据恢复流程。
基于这样的策略,当一个包含了上一个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,其肯定无法成为 Leader,原因很简单,因为当前集群中一定包含一个 Quorum 集合,该集合中的机器一定包含了更高 epoch 的事务 Proposal,因此这台机器的事务 Proposal 肯定不是最高,也就无法成为 Leader 了。当这台机器加入到集群中,以 Follower 角色连接上 Leader 服务器之后,Leader 服务器会根据自己服务器上最后被提交的 Proposal 来和 Follower 服务器的 Proposal 进行比对,比对的结果当然是 Leader 会要求 Follower 进行一个回退操作一回退到一个确实已经被集群中过半机器提交的最新的事务 Proposal。


练习题

zk 集群中都有哪些角色?都是什么作用?
zk 集群什么情况下需要选主?
zk 集群选主的过程是怎样的?
zk 集群启动的过程是怎样的?
zk 集群中 Leader 挂掉后,zk 集群会怎么做?
zk 集群写入一个值的过程是怎样的?(即 zk 一个事务提交的过程是怎样的)
zk 集群的 Leader节点的作用是什么?
客户端配置连接 zk 集群后,客户端是和几个 zk 节点连接的?
啥时候 zk 集群会进行选主?
zk 集群里投票叫做啥意思?
选主后 zk 集群的数据同步是怎样的过程?
zk 里面的事务是什么意思?
zk 里面的投票是什么意思?
zk 中的事务日志是什么意思?
zk 中的过半原则是什么意思?

完。

码先生
Author: 码先生

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注