写点什么

探究 Zookeeper 原理

作者:止水
  • 2022 年 6 月 20 日
  • 本文字数:7576 字

    阅读完需:约 25 分钟

探究 Zookeeper 原理

Zookeeper 简介

Zookeeper 是一个开放源代码的分布式协调服务,设计目标是将那些复杂且容易出错的的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。分布式应用服务可以基于 Zookeeper 实现发布/订阅服务、集群管理、命名服务、分布式锁、分布式队列、分布式配置管理、负载均衡等功能。

Zookeeper 数据模型

Zookeeper 将所有数据存储在内存中,数据模型就是一棵树(ZNode Tree),由斜杠(/)进行分割路径(/app1/a1),和文件系统非常类似,树的每个数据节点称之为 ZNode,ZNode 是 Zookeeper 中数据的最小单元,每个 Znode 上都可以保存数据,挂载子节点,保存一系列属性信息。



在 Zookeeper 中,ZNode 有一下四种节点类型:

持久节点(persistent)

该数据节点被创建后,会一直存在于 Zookeeper 上,直到主动请求删除这个节点。

持久顺序节点(persistent_sequential)

在持久节点的基础上,每个节点是有顺序的,每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的先后顺序。在创建节点的过程中,Zookeeper 会自动为给定的节点名加上一个数字后缀,这个数字后缀的上限是整型的最大值。

临时节点(ephemeral)

临时节点的生命周期和客户端的会话绑定在一起,如果客户端会话失效,那么这个节点就会被自动清理掉。临时节点只能作为叶子节点。

临时顺序节点(ephemeral_sequential)

在临时节点的基础上,增加了节点顺序性。

Zookeeper 集群

集群模式


Zookeeper 集群中的每台机器都会在内存中维护当前服务器的状态,每台服务器之间都相互保持通信,只要集群中存在超过一半的机器(图示 5/2+1=3,包括 Leader 自己, 不能包括 Observer)能够正常工作,集群就能够正常对外提供服务。


Zookeeper 客户端会选择集群中任意一台机器创建一个 TCP 连接,如果连接断开会自动连接到集群中的其他机器。

集群角色

在 Zookeeper 集群中, 有三种不同的服务器角色,分别是 Leader、Follower、Observer。

Leader

Leader 服务器是 Zookeeper 集群的核心,主要工作:

  • 负责处理客户端的事务请求(写),保证集群事务处理的顺序行。

  • 负责集群内部各服务器的调度。

Follower

Follower 服务器是 Leader 的跟随者,主要工作:

  • 负责处理客户端的非事务(读)请求,转发事务请求给 Leader 服务器

  • 参与事务请求 Proposal 的投票

  • 参与 Leader 选举投票

Observer

Observer 是 Zookeeper 集群的观察者, 观察 Zookeeper 集群的最新状态并将这些状态变更同步过来。Observer 的工作原理和 Follower 差不多,负责处理客户端的非事务请求,转发事务请求给 Leader 服务器,但是不参与任务形式的投票,包括事务请求 Proposal 的投票和 Leader 选举的投票。Observer 通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

ZAB 协议

ZAB 协议简介

ZAB(Zookeeper Atomic Broadcast) 协议是为 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。基于该协议,Zookeeper 实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播:

  • 崩溃恢复:当整个集群启动过程中,或是 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式。当选举出新的 Leader 服务器,并且集群中已经有过半的机器与 Leader 服务器完成数据同步之后,ZAB 协议就会退出恢复模式进入消息广播模式。

  • 消息广播:消息广播就是将 Leader 服务器上的数据顺序的同步给 Follower 服务器并且一起提交的过程。 当有一台遵守 ZAB 协议的服务器加入到集群中时,如果集群中已经有 Leader 服务器在负责消息广播,则加入的服务器自动进入数据恢复模式,同步数据,然后一起参与到消息广播流程中。当 Leader 服务器出现异常,或集群中没有过半服务器与 Leader 服务器保持正常连接时,ZAB 协议就会从消息广播模式进入崩溃恢复模式。

ZXID

Leader 每次广播事务 Proposal(提议) 之前,都会为这个事务 Proposal 分配一个 ZXID,即事务 ID,ZXID 是全局单调递增的,ZAB 协议将每一个事务 Proposal 按照 ZXID 的先后顺序来进行排序和处理。


ZXID 是一个 64 bit 的数字:

  • 低 32 bit :可以看作是一个单调递增的计数器,Leader 在产生一个新的事务 Proposal 的时候,都会对该计数器加 1。

  • 高 32 bit :代表了 Leader 周期 epoch 的编号,每当选举产生一个新的 Leader ,就会从这个 Leader 上取出本地日志中最大事务 Proposal 的 ZXID,并从改 ZXID 中解析出对应的 epoch 值,然后对其加一,之后会以此编号作为新的 epoch,并将低 32 bit 置为 0 来开始生成新的 ZXID。


ZXID 的高 32 位或 低 32 位超过最大值,就会重新触发一次选举。

消息广播


  1. Leader 接收到来自客户端事务请求;

  2. Leader 为这个事务 Proposal 分配一个全局单调递增的唯一 ID,即事务 ID(ZXID);

  3. Leader 会为每个 Follower 都各自分配一个单独的 FIFO 队列,然后将事务 Proposal 放入到这些队列中进行消息发送;

  4. 每一个 Follower 在接收到事务 Proposal 之后,首先将其以事务日志的形式写入到本地磁盘中,写入成功后反馈给 Leader 一个 ACK 响应;

  5. 当 Leader 接收到超过半数 Follower 的 ACK 响应后,就会广播一个 Commit 消息给所有的 Follower 通知其进行事务提交,同时 Leader 自身也会完成事务提交。

崩溃恢复(Leader 选举)

从 3.4.0 版本开始,Zookeeper 只保留了 TCP 版本的 FastLeaderElection 选举算法。

概念
  • myid

myid 是一个数字, 集群配置文件中指定的服务器编号,用来唯一标识一台集群中的机器,每台机器不能重复,也称为 SID。

  • ZXID

参考 3.3.2。

  • 选举轮次

也成为逻辑时钟,初始值为 0,每次投票前都会对其加一,Leader 选举时会对这个值做比较,选举轮次低的投票将被抛弃。造成选举轮次低的原因是机器故障,未参加某次投票,当机器恢复时,会从 Leader 同步选举轮次。

  • 内部投票

服务器自身当前的投票

  • 外部投票

接收其他服务器发来的投票

  • Zookeeper 集群中每台机器的状态标识

  • LOOKING : 寻找 Leader 状态,表明集群中没有 Leader 角色,进入 Leader 选举流程之前的状态。

  • FOLLOWING : 跟随者角色状态,表明当前服务器角色是 Follower。

  • LEADING : 领导者角色状态,表明当前服务器角色是 Leader。

  • OBSERVING : 观察者角色状态,表明当前服务器角色是 Observer。

服务启动时的 Leader 选举
  1. 集群启动的时候,每个服务器的状态都是 LOOKING,然后进入 LEADING 选举流程,首先对自己的选举轮次+1,然后会发出一个投票,都会先将票投给自己,然后将投票结果发给集群中的其他服务器(每次投票最基本的元素包括:所推举服务器的 myid 和 ZXID)。

  2. 集群中每个服务器接收来自其他服务器的投票,判断该投票的有效性,包括判断选举轮次,是否来自 LOOKING 状态的服务器。

  3. 判断选举轮次:

  4. 外部投票的选举轮次大于内部的投票

  5. 更新自己的选举轮次,清空已经收到的投票,投票变更为外部投票。

  6. 外部投票的选举轮次小于内部投票

  7. 直接忽略该外部投票,不做任务处理。

  8. 外部投票的选举轮次和内部投票的一致

  9. 绝大多数场景,直接对比内外投票。

  10. 处理投票,每个服务器将其他服务器的投票和自己的投票根据一定的规则进行对比,根据对比结果来决定是否需要变更自己的投票。 对比规则:

  11. 检查 ZXID

  12. 外部投票大于内部投票的 ZXID,投票变更为外部投票,并将该投票发送出去。

  13. 外部投票小于内部投票的 ZXID,坚持自己的投票,不做任何变更。

  14. 外部投票等于内部投票的 ZXID,继续判断 myid。

  15. 检查 myid

  16. 外部投票大于内部投票的 myid,投票变更为外部投票,并将该投票发送出去。

  17. 外部投票小于内部投票的 myid,坚持自己的投票,不做任何变更。

  18. 一句话总结:选择 ZXID 大的投票,如果 ZXID 相同则选择 myid 大的投票。如果服务器变更了投票,就将该投票发送出去通知其他服务器。

  19. 统计投票,每次投票后,服务器都会统计所有的投票,如果一台机器收到了超过半数相同的投票(n/2+1),那么这个服务器就会选为 Leader。

  20. 改变服务器的状态,Leader 选定后,每个服务器都会更新自己的状态,如果是 Followe 就会变更为 FOLLOWING,如果是 Leader 就会变更为 LEADING,如果是 Obsever 就会变更为 OBSERVING。


举例说明服务启动时 Leader 选举:假设集群中有三台机器并且同时启动,myid 分别为 1,2,3,ZXID 都为零(刚启动),投票信息我们简写为(myid,ZXID),选举 Leader 过程如下:

  1. Server 1 启动,状态置为 LOOKING,等待进入 Leader 选举流程。

  2. Server 2 启动,状态置为 LOOKING,等待进入 Leader 选举流程。

  3. Server 1 和 Server 2 分别对自己的选举轮次+1。

  4. Server 1 和 Server 2 发起投票,投票分别投给自己,Server 1 投票给自己(1,0),Server 2 投票给自己(2,0)。

  5. 发送投票给各个服务器。

  6. 服务器接收到外部投票,校验外部投票的有效性,投票轮次的判断等。

  7. Server 1 接收到来自 Server 2 的投票(2,0),与自己的投票做对比,先进行 ZXID 的对比,ZXID 相等都为零,然后再对比 myid,Server 2 大于 Server 1 的 myid,所以 Server 1 将自己的投票变更为 (2, 0),然后将变更后的投票(2,0)发送出去。

  8. 同 7,Server 2 接收到来自 Server1 的投票 (1,0),经过对比,自己的投票(2, 0)优于 Server 1 的投票。投票无需变更。

  9. Server 1 和 Server 2 投票完成,Server 2 获得的投票已经超过了集群中的半数(3/2+1=2)则 Server 2 选举为 Leader。

  10. Server 2 将状态变更为 LEADING,Server 1 将状态变更为 FOLLOWING。

  11. Server 3 启动,将状态置为 LOOKING,进入 Leader 选举流程,选举轮次+1,投票,发送投票。

  12. 当 Server 1 和 Server 2 接收到 Server3 的投票,发现自己未处于 LOOKING 状态,则忽略该投票,并且将 Leader 信息以投票的方式发送出去。

  13. Server 3 接收到投票发现集群中已经有 Leader ,然后变更自己的状态为 FOLLOWING。

  14. Server 3 与 Server 2(Leader)进行数据同步。

服务器运行期间的 Leader 选举

如果集群中的 Leader 服务器发生故障,集群暂时无法对外提供服务,而是进入新一轮的 Leader 选举。服务器运行期间的 Leader 选举和启动时的 Leader 选举过程基本是一致的。


还是以上面的集群为例,假如 Leader Server 2 挂了,Leader 选举流程:

  1. Server 2 故障后,Server 1 和 Server 3 与 Server 2 无法建立连接,此时集群中没有 Leader 服务器,Server 1 和 Server 2 由 FOLLOWING 状态变更为 LOOKING 状态,进入 Leader 选举流程。

  2. Server 1 和 Server 3 进入选举流程,对各自选举轮次+1,

  3. Server 1 和 Server 3 分别将选票投给自己,Server 1(1,168),Server 3(3,167)。

  4. 发出投票。

  5. Server 1 和 Server 3 接收投票,验证投票有效性,并进行投票对比。

  6. Server 1 大于 Server 3 的 ZXID,所以 Server 1 不需要变更投票,Server 3 将投票变更为 (1,168)并发送出去。

  7. Server 1 获得了半数以上选票,确认 Leader 为 Server 1。

  8. 变更状态,Server 1 变更为 LEADING,Server 2 变更为 FOLLOWING。

  9. 等待 Server 3 恢复重新加入集群后,发现集群中已经存在 Leader 服务器,就与 Leader 服务器建立连接,将自己状态变更为 FOLLOWING,然后进行数据同步。


因为 Zookeeper 的 Leader 选举使用的是过半机制,即收到一半以上(n/2+1)机器就可选举出 Leader ,所以 Zookeeper 集群不会发生脑裂情况。

数据同步

在完成 Leader 选举之后,Follower 和 Observer (下面将两者统称为 Learner)会向 Leader 进行注册,完成注册后就会进入数据同步环节,数据同步就是将 Leader 服务器上的数据同步到 Learner 服务器上的过程。

初始化 ZXID

在数据同步初始化阶段会完成以下三个 ZXID 值的初始化:

  • peerLastZxid:该 Learner 服务器最后处理的 ZXID。

  • minCommittedLog:Leader 服务器提议缓存队列 committedLog 中的最小 ZXID。

  • maxCommittedLog:Leader 服务器提议缓存队列 committedLog 中的最大 ZXID。

数据同步分类

数据同步分为四类:

  • 直接差异化同步 (DIFF 同步)

  • 当 peerLastZxid 在 minCommittedLog 和 maxCommittedLog 之间,就使用直接差异化同步(DIFF 同步)。

  • Leader 向 Learner 发送一个 DIFF 指令,通知 Learner 进入差异化数据同步阶段。

  • 针对每个差异 Proposal,Leader 会通过发送两个数据包来完成,分别是 PROPOSAL 内容数据包和 COMMIT 提交指令数据包,Learner 会依次将其应用到内存数据库中。

  • 发送完差异数据后,Leader 立即发送一个 NEWLEADER 指令,用于通知 Learner 已经将差异数据同步完成。

  • Learner 反馈一个 ACK 消息给 Leader,用于通知 Leader 自己已经完成了对差异数据的同步。

  • Leader 进入 过半策略 等待阶段,当集群中有过半的 Learner 都响应了 Leader ACK 消息,Leader 会向所有已经完成数据同步的 Learner 发送一个 UPTODATE 指令,用于通知 Learner 已经完成数据同步。

  • Learner 再次返回一个 ACK 消息 给 Leader,数据同步完成,集群可以对外提供服务。

  • 先回滚再差异化同步(TRUNC + DIFF 同步)

  • 当 peerLastZxid 在 minCommittedLog 和 maxCommittedLog 之间,存在一种特殊情况,

  • Leader 将事务记录到本地事务日志中,再将 Proposal 发送给其他 Follower 时,Leader 挂了,

  • 当其他 Follower 升级为 Leader 时,并不包含这个事务 ID。

  • 举例说明:

    假设当前有 A,B,C 三台机器,A 是 Leader,Leader_Epoch(选举周期) 为 5,minCommittedLog 为 0x5000001,maxCommittedLog 为 0x500002,此时,Leader 处理新事务 ZXID 为 0x500003,已经将该事物写入本地事务日志中,在将该 Proposal 发送给 Follower 时,Leader 服务器挂了,Proposal 并没有同步到 Follower。

    此时集群会进行 Leader 选举,假如 B 选为 Leader,Leader_Epoch 为 6,B 和 C 对外提供服务,又处理了两个事务,ZXID 为:0x600001,0x600002,这时 A 恢复,开始进行数据同步。

    此时 peerLastZxid 为 0x500003,minCommittedLog 为 0x500001,maxCommittedLog 为 0x600002,显然,peerLastZxid 在 minCommittedLog 和 maxCommittedLog 之间,但是 Leader 上并不存在 ZXID 为 peerLastZxid 的事务。

    对于这个特殊场景,需要先让 A 事务回滚,回滚到 Leader 上存在并且是最接近 peerLastZxid 的 ZXID,针对这个例子,A 需要回滚到 ZXID 为 0x500002 的事务,然后再进行差异化同步。

  • 一句话概括上面的特殊场景:Learner 存在的事务记录在 Leader 中不存在,需要让 Learner 进行事务回滚,然后再进行差异化同步(DIFF 同步)。

  • 针对上面的情况,就使用先回滚再差异化同步(TRUNC + DIFF 同步)。

  • Leader 向 Learner 发送一个 TRUNC 指令,用于通知 Learner 进行事务回滚。剩下的 DIFF 同步和直接差异化同步(DIFF 同步)一致。

  • 仅回滚同步(TRUNC 同步)

  • 当 peerLastZxid 大于 maxCommittedLog ,就会使用仅回滚同步(TRUNC 同步)。

  • 仅回滚同步是先回滚再差异化同步的简化版,Leader 发送一个 TRUNC 指令通知 Learner 回滚到 ZXID 为 maxCommittedLog 对应的事务。

  • 全量同步(SNAP 同步)

  • 一下两种场景需使用全量同步(SNAP 同步):

  • peerLastZxid 小于 minCommittedLog。

  • Leader 上没有提议缓存队列,并且 peerLastZxid 不等于 Leader 最大的 ZXID。

  • 全量同步就是 Leader 将本机上的全量内存数据同步给 Learner。

  • Leader 首先发送一个 SNAP 指令给 Learner,用于通知 Learner 进入全量数据同步,

  • 然后 Leader 会将全量数据同步给 Learner,Learner 接收到数据后会应用到自己的内存数据库中。

Watcher 机制

客户端向 Zookeeper 服务器注册一个 Watcher 监听,当服务器的指定事件触发了这个 Watcher,就会向指定客户端发送一个事件通知。

主要流程


  1. 客户端向 Zookeeper 服务器注册 Watcher 监听。

  2. 同时会将 Watcher 对象存储在客户端的 WatcherManager 中。

  3. 当服务器触发 Watcher 事件后,会向客户端发送通知,然后客户端线程从 WatcherManager 中取出对应的 Watcher 对象来执行回调逻辑。

Watcher 特性

  • 一次性

  • 一旦一个 Watcher 被触发,Zookeeper 就会将其从存储中移除。需要反复注册。

  • 客户端串行执行

  • 客户端 Watcher 是串行同步的,这样保证了顺序执行。

  • 轻量

  • WatcherEvent 是 Watcher 通知的最小单元,只包含通知状态、事件类型、节点路径,不包含事件的具体内容。如果想要获取具体的数据需要客户端主动重新去获取。

应用场景

配置中心

集群中不同主机需要共享同一份配置数据,配置中心实现共享数据的集中管理和变更通知。将配置数据发布到 Zookeeper 的一个或一系列节点上,客户端向服务端注册关注节点的 Watcher 监听,一旦配置数据发生变化,服务端会向客户端发送 Watcher 事件通知,客户端收到通知主动获取变更配置数据。


Zookeeper 作为配置中心弊端:

  1. Zookeeper 单个节点大小不能超过 1 M。

  2. Watcher 通知一次即失效,需要重复注册。

  3. 节点数据版本的变化也会触发 NodeDataChanged,setData() 成功之后无论数据是否发生变化都会触发 NodeDataChanged,而这种情况是不需要 Watcher 通知的。

命名服务

命名服务是指客户端可以通过名字来获取资源(服务地址,提供者信息等)。在 Zookeeper 中创建一个全局唯一的节点,这个节点路径可以作为一个名字,指向集群中的实体,提供的服务地址或远程对象等。

全局唯一 ID

利用 Zookeeper 的顺序节点,可以生成全局唯一 ID。

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式,如果不同系统(不同进程)共享一个资源,那么在访问的时候需要通过一些互斥的手段来防止彼此的干扰,以保证系统的一致性。

排他锁

当前有且仅有一个进程可以获取锁。


获取锁:通过创建临时节点来获取锁,如果节点创建成功,则获取锁成功,如果创建节点失败可以注册一个 Watcher 监听,通知客户端再次获取锁。


释放锁:当客户端执行完业务逻辑后主动删除临时节点锁就会释放。如果持有锁的客户端挂了,Zookeeper 会将这个临时节点移除也会释放锁。

共享锁

获取锁的进程可以进行读操作,其他进程也可以获取共享锁进行读操作,但是不能进行写操作。


获取锁:通过创建临时节点来获取锁,如果是读请求就创建临时节点例如/shared_lock/192.168.0.1-R-000001,如果是写请求就创建临时节点例如/shared_lock/192.169.0.1-W-000001。创建完子节点,对/shared_lock节点注册子节点变更通知。对于读操作:如果没有比自己小的节点或者比自己小的节点都是读请求,则获取共享锁成功,如果比自己小的节点有写请求,则获取锁失败,进入等待;对于写操作:如果自己你不是最小的节点,则进入等待。


释放锁:和排它锁一致。


羊群效应:如果对/shared_lock下的子节点注册 Watcher 通知的话,会造成很多无效的 Watcher 通知及操作,这就是羊群效应。我们只需要向自己关注的节点注册 Watcher 监听即可避免羊群效应。


对于写请求:只需要向比自己序号小的最后一个节点注册 Watcher 监听。

对于读请求:只需要向比自己序号小的最后一个写请求节点注册 Watcher 监听。


Reference

<<从 Paxos 到 Zookeeper 分布式一致性原理与实践>>


发布于: 刚刚阅读数: 6
用户头像

止水

关注

博观而约取,厚积而薄发 2018.11.06 加入

记录工作技术。如果对文章有异议,或者有不同观点,请留言告知呦 ❣️

评论

发布
暂无评论
探究 Zookeeper 原理_zookeeper_止水_InfoQ写作社区