Zookeeper 学习笔记

一、简介

是一个分布式协调服务的开源框架。主要用来解决分布式集群中应用系统的一致性问题。

本质上是一个分布式的小文件存储系统,提供基于类似文件系统的目录树方式的数据存储。并且可以对树中的节点进行有效管理,从而用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。诸如:统一命名服务、分布式配置管理、分布式消息队列、分布式锁、分布式协调等功能。

二、主要应用及相关特性

应用 涉及特性
数据发布、订阅 注册与异步通知(Watcher)
负载均衡(DNS) 注册与异步通知(Watcher)
命名服务 一致性、顺序节点
分布式协调、通知 注册与异步通知(Watcher)
集群管理 临时节点、注册与异步通知(Watcher)
Master选举 强一致性
分布式锁 强一致性、临时节点
分布式队列 顺序节点

三、分布式特性

Zookeeper 可以保证如下分布式一致性特性:

  • 顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到 zookeeper 中。
  • 原子性:一次请求,要么整个集群都应用了该请求,要么都没有应用,不会出现一部分机器应用,一部分机器没有应用的情况。
  • 单一视图:同一客户端无论什么时候连接到哪个服务器上,都不会看到比自己之前看到的数据更早版本的数据。
  • 可靠性:一旦服务端成功应用了一个事务,并完成了对客户端的响应,那么该事务所引起的服务端状态变更一定会被保留下来。
  • 实时性:zookeeper 能够保证在一定的时间内,客户端最终(不一定是第一时间)能从服务端读取到最新的数据状态。

四、基本概念

4.1 集群角色

在 zookeeper 中,集群中的机器分为 3 种角色:

  • Leader:集群中的所有机器通过一个 Leader 选举过程选定一个 Leader,Leader 可以处理事务(写)和非事务(读)请求,调度内部其它服务器。
  • Follower:处理非事务请求,转发事务请求给 Leader,参与事务请求的投票,参与 Leader 选举投票。
  • Observer:处理非事务请求,转发事务请求给 Leader。

4.2 会话

会话是指客户端会话,每个客户端会和其中一台 zookeeper 服务器建立一个 tcp 长连接,通过这个连接客户端能够通过心跳检测与服务器保持有效的会话,也能够向 Zookeeper 服务器发送请求并接受响应,同时还能通过该连接接受来自服务器的 watch 事件通知。会话会设置一个超时时间,当客户端因为各种原因和服务器断开连接之后,只要能在超时时间内重新连接上任意一台服务器,那么之前创建的会话仍有效。

4.3 数据节点

Zookeeper 的数据模型是一棵树,树上的每个节点是一个最小数据单元,被称为数据节点(ZNode),保存了数据内容、ACL 列表、节点状态、父节点引用、子节点列表。每个数据节点可以被一条由斜杠 (/)分割的路径表示,比如/foo/path1。

在 Zookeeper 中数据节点有以下四种类型:

  • 持久节点:zookeeper 中最常见的一种节点,在创建后就会一直存在于服务器上,直到有删除操作来主动清除这个节点。
  • 持久顺序节点:基本特性和持久节点一致,但在创建时会在名字后面加上一个顺序递增的数字后缀,用于表示同一目录下节点的创建顺序。
  • 临时节点:临时节点和持久节点不同,它的生命周期和客户端的会话绑定在一起,当客户端会话失效时,节点会被自动清理。临时节点只能作为叶子节点。
  • 临时顺序节点:在临时节点的基础上,添加了顺序的特性。

4.4 Watcher

Watcher(事件监听器)是 zookeeper 的一个重要特性。zookeeper 允许用户在指定数据节点上注册一些 watcher,并且在一些节点发生特定事件(比如变更、删除)时,服务端会将事件通知到感兴趣的客户端上去。

五、ZAB 协议

5.1 协议介绍

zookeeper 通过 ZAB 协议保证集群中数据的一致性。ZAB 协议包括两种基本的模式,分别是崩溃恢复和消息广播。当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了同步状态之后,ZAB 协议就会退出恢复模式,进入消息广播模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。

5.2 消息广播

Zookeeper 中只允许唯一的一个 Leader 服务器来进行事务请求处理,Leader 服务器在接收到一个事务请求后,会生成对应的事务提案并发起一轮广播协议。ZAB 协议使用的广播协议类似于一个二阶段提交过程:Leader 向 Follower 发送事务提案,Follower 在收到事务提案后,会将其以事务日志的形式写到磁盘中,然后反馈一个 Ack 响应。当 Leader 服务器接收到超过半数 Follower 的 Ack 响应后,就会广播一个 Commit 消息给所有的 Follower 服务器以通知其进行事务提交,同时 Leader 自身也会完成事务的提交,而每一个 Follower 服务器在接收到 Commmit 消息后,也会完成对事务的提交。

ZAB 协议需要保证每一个消息严格的因果关系,因此 Leader 服务器会为每一个事务分配一个全局单调递增的唯一 ID,我们称之为事务 ID(ZXID)。Leader 服务器会为每一个 Follower 服务器都各自分配一个单独的队列,将需要广播的事务按 ZXID 的顺序依次放入这些队列中去,并且根据 FIFO 策略进行消息发送。发送的方式使用 TCP,这样可以保证 Follower 接收的顺序和 Leader 发送的顺序是一致的。

在二阶段提交模型下,是无法处理 Leader 服务器崩溃退出而带来的数据不一致问题的,因此在 ZAB 协议中添加了另一个模式,即采用崩溃恢复模式来解决这个问题。

5.3 崩溃恢复

正常工作时 ZAB 协议会一直处于消息广播模式,但是一旦 Leader 服务器出现崩溃,或者说由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。在 ZAB 协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的 Leader 服务器。除了选举出一个新的 Leader,整个恢复过程还要作以下保证:

  • ZAB 协议需要确保已经在 Leader 服务器上提交的事务不丢失。
  • ZAB 协议需要确保只在 Leader 服务器上被提出的事务不再出现。

什么时候会出现已经提交的事务请求被丢失呢?

当 Leader 收到超过半数 Follower 的 ACK 后,就向各个 Follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回成功。但是如果在部分 Follower 在收到 COMMIT 命令前 Leader 就挂了,导致剩下的服务器并没有执行到这条消息。

那么如何解决这个问题呢?

1、选举拥有 Proposal 的 zxid 超过半数的节点作为新的 Leader。由于所有事务提案被 COMMIT 之前必须有超过半数的 Follower ACK,即必须有超过半数的服务器的事务日志上记录了该事务提案,因此,Proposal 的 zxid 超过半数的节点保存了所有被 COMMIT 的事务提案(此时这个提案在节点中不一定以 COMMIT 的形式记录,也可能是以 Proposal 的形式记录)。
2、新的 Leader 将本地事务日志中没有提交的 proposal 提交(这是为了确保已经在 Leader 上提交的事务不丢失,因为可能存在 Leader 刚提交了一个事务,但尚未发出任何 COMMIT 就挂掉的情况)。
3、新的 Leader 会为每一个 Follower 服务器都准备一个队列,并将那些没有被各 Follower 服务器同步的事务以 Proposal 消息的形式逐个发送给各个 Follower,并在每一个 Proposal 消息后面紧跟一个 COMMIT,以表示该事务被提交。

什么时候会出现只在 Leader 服务器上被提出的事务又出现呢?

当挂掉的 Leader 服务器作为 Follower 重新连接时,此时该服务器中可能存在一些只在自己本地被提出的事务,这些事务的 zxid 可能和新 Leader 生成的事务 ID 相同,很显然它们的内容不同。当新 Leader 提交新的事务时,由于 zxid 相同,会出现旧的 Leader 服务器中把之前只在自己本地出现过的事务提交的情况。导致了数据的不一致。

解决方案:

确保旧的 Leader 产生的 zxid 不会和 新的 Leader 产生的 zxid 相同。把 ZXID 的高 32 位当作 Leader 周期 epoch 的编号,每当选举产生一个新的 Leader 对其加 1;低 32 位当作一个简单的递增计数器,每次产生新 Leader 后清空。这样每一轮 Leader 周期产生的 ZXID 都会不一样,并且上一轮产生的 ZXID 一定小于当前轮次产生的。因此当旧的 Leader 重连时,一定无法成为 Leader,因为当前集群中包含的 ZXID 一定比它的大。所以这台机器只能以 Follower 角色加入集群中,当它连接上新的 Leader 后,新的 Leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除。

六、会话管理

客户端和服务端建立链接之后就会形成一个会话,会话是两者见交互操作的基础,临时节点的生命周期、Watcher 通知等都与其有关。

6.1 会话状态

会话主要有以下状态:CONNECTING、CONNECTED、RECONNECTING、RECONNECTED、CLOSE。一旦客户端和服务器建立连接,客户端状态就变更为 CONNECTED。当网络发生闪断或是其它原因,客户端与服务器之间的连接会出现断开情况,此时客户端会自动进行重连操作,同时客户端状态变为 CONNECTING,直到重新连接上后,客户端状态又会再次转变成 CONNECTED。如果出现会话超时、权限检查失败或是客户端主动退出的情况,客户端的状态会变成 CLOSE。

6.2 会话建立

每次创建链接时,服务端即分配一个 SessionID,是一个 64 值,高 8 位 服务器 id,后 56 位是通过当前时间获得的一个随机值。会话在服务端会交给 SessionTracker 管理。SessionTracker 负责会话的创建、管理和清理等工作。每一个会话在 SessionTracker 内部都保留了三份,具体如下;

  • sessionsById:这是一个 HashMap<Long,SessionImpl> 类型的数据结构,用于根据 sessionID 来管理 Session 实体。
  • sessionsWithTimeout:这是一个 ConcurrentHashMap<Long,Integer> 类型的数据结构,用于根据 sessionID 来管理会话的超时时间。
  • sessionSets:这是一个 HashMap<Long,SessionSet> 类型的数据结构,用于根据会话超时时间来归档会话。

会话创建共分为以下几步:

  1. NIOServerCnxn 接收请求
  2. 反序列化 ConnectRequest
  3. 协商超时时间:需要在 2~20 tickTime 间
  4. 判断是否需要重新建立会话:判断客户端请求中是否有 SessionId
  5. 生成 SessionId
  6. 注册会话:向 SessionTracker 注册会话,添加到两个数据结构中,sessionWithTimeout 和 sessionById
  7. 激活会话
  8. 产生会话密码:集群间转移凭证

6.3 会话激活

会话管理采用的方式是:分桶策略,将类似的会话放在同一区块中进行管理,如下图:

分配的原则是每个会话的“下次超时时间点”。运行期间服务端会定时的进行会话超时检查,其时间间隔是 ExpirationInterval,单位是毫秒,默认值是 tickTime 的值,默认情况下,每隔 3000 毫秒进行一次会话超时检查。因此超时时间在一个批次内的会话会被放入同一个桶中。

为了保持客户端会话的有效性,服务端需要不断地接收到来自客户端的心跳包,然后重新激活对应的客户端会话,会话激活的流程如下:

  1. 接收客户端心跳连接
  2. 检查该会话是否已关闭,如果已关闭则不再继续激活
  3. 计算新的超时时间
  4. 迁移会话到新桶中

实际执行中,客户端每次请求都会触发一次会话激活。而如果客户端发现在 sessionTimeout/3 时间内尚未和服务端进行过任何通信,那么会主动发起一个 PING 请求来主动触发激活。

6.4 会话清理

服务端会定期查看到期的桶,如果不为空,那么会清理这些会话。清理的流程如下:

  1. 标记会话状态为“已关闭”,在清理期间即使有该客户端请求也不再处理
  2. 向整个集群发起“会话关闭”请求
  3. 收集需要清理的临时节点:一旦某个会话失效后,和该会话相关的临时节点需要一并被清除掉
  4. 添加“节点删除”事物变更
  5. 删除临时节点
  6. 移除会话
  7. 关闭对应的 NIOServerCnxn(socket)

6.5 会话重连

当客户端和服务器之间的网络连接断开时,客户端会自动进行反复的重连,直到连上集群中的任意一台机器。

客户端的每次请求都会得到服务端最新的 zxid,客户端会把它保存起来。重连时,新的服务器的 zxid 如果比客户端的小,说明新服务器的数据版本较旧,就会拒接该连接请求。(单一视图的保证)

七、Leader 选举

选举过程是通过各节点投票实现的,经过几轮内部投票和外部投票的比较,各机器投出的票趋向一致,获得投票过半即可作为 leader。

7.1 比较规则

选举的比较规则遵循 ZAB 协议,优先 zxid 大的;zxid 相同,优先 sid 较大者。

7.2 选举时机

两种情况下会启动选举流程,一种情况是在整个服务器刚刚初始化启动时,另一种是在运行期间 Leader 挂掉。在运行期间 Leader 挂点后,Follower 会自动把自身当前状态变为 LOOKING,表示在进行 Leader 选举。这里可能有种情况是网络分区导致部分节点误以为 Leader 挂掉,然后这些节点开始 Leader 选举,即脑裂。Zookeeper 的选举机制确保了这些节点会一直处于 LOOKING 状态,从而保证了一致性。

7.2 选举流程

  1. 自增选举轮次:每台服务器内部有一个 logicalclock 属性,保存了当前的 Leader 选举轮次,只有当选票上的轮次和当前记录的轮次相同时,选票才有效。
  2. 初始化选票:初始化阶段,每台服务器都会将自己推为 Leader。
  3. 发送初始化选票
  4. 接收外部选票:每台服务器会不断接收外部选票。如果服务器发现无法获取到任何的外部选票,会立即确认自己是否和集群中其它服务器保持连接。如果没有建立连接就马上建立连接;如果已经建立了连接就再次发送自己当前的内部选票。
  5. 判断选举轮次:服务器会判断外部投票的选举轮次。1)如果外部投票的选举轮次大于自己的选举轮次,就会清空所有已经收到的选票,然后用初始化的选票来 PK 外部选票以确定是否要变更内部选票,最终将内部选票发送出去。2)如果外部选票的选举轮次小于自己的选举轮次,直接忽略该外部选票。3)如果外部选票和自身轮次一次,就进行选票 PK。
  6. 选票PK:按照比较规则比较外部选票和内部选票,如果外部选票优于内部选票,就要进行选票变更。
  7. 变更投票:如果确定了外部选票优于内部选票,就使用外部投票的信息覆盖内部选票,然后将变更后的内部投票发送出去。
  8. 选票归档:无论是否进行了选票变更,都要将这一次的外部选票记录归档,服务器内部会保存一份本轮选举中所有外部服务器的投票记录。
  9. 统计投票:根据归档记录统计当前是否已经有过半服务器认可了当前的内部投票,如果已有过半服务器认可,就终止本轮投票,否则返回步骤 4。
  10. 更新服务器状态:终止投票后,根据投票结果判断当前被过半服务器认可的 Leader 是否是自己,如果是的话,更新自己的服务器状态为 LEADING;如果不是的话,就根据情况来确定自己是 FOLLOWING 或是 OBSERVING。

八、事务请求

会改变 Zookeeper 内部数据状态的请求统称事务请求。包括数据的增删改、会话的创建删除等。事务请求都是由 Leader 主导完成的,如果 Follower 或者 Observer 收到事务请求,需要将请求转给 Leader 完成

事务请求具体流程如下:

8.1 Sync 流程

将事务请求记录到日志。对于 Leader,在发起提议前会对事务做记录;对于 Follower,在收到 Leader 的提议后会对事务做记录,然后会向 Leader 服务器发送 ACK 消息,Leader 统计投票。

8.2 Proposal 流程

1)发起提议
2)广播提议:将该提议放入投票箱 outstandingProposals 中,投票箱会按顺序广播提议。
3)收集投票:Follower 在收到提议后,会进入 Sync 流程进行事务记录,一旦记录完成,会发送 ACK 给 Leader 服务器。Leader 服务器根据 ACK 来统计每个提议的投票情况。
4)请求放入 toBeAppley 队列:提议获得超过半数 ACK 后,会被放入 toBeAppley 队列,该队列会按顺序提交其中的事务。
5) 广播 Commit 消息:向所有的 Follower 和 Observer 广播提交消息。

8.3 Commit 流程

1)请求交给 CommitProcessor,存储到 queuedRequests 中
2)处理线程从 queuedRequests 中取出,并将 nextPending 标记为当前的 Request
3)等待投票完成
4)投票通过,将 reques t添加到 committedRequests 中
5)提交请求,committedRequests 中取出开始处理,为确保有序性,会与 nextPending 进行比较

8.4 事务应用

1)变更添加到内存中
2)加入commitProposal队列,便于后续集群间数据同步。