四、ZooKeeper技术内幕

0.182字数 9674阅读 2359

本文将从系统模型、序列化与协议、客户端工作原理、会话、服务端工作原理以及数据存储等方面来揭示ZooKeeper的技术内幕。

一、系统模型

1.1 数据模型

ZooKeeper的视图结构使用了其特有的“数据节点”概念,我们称之为ZNode。ZNode是ZooKeeper中数据的最小单元,每个ZNode上都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,我们称之为树。

1.2 节点特性

我们已知,ZooKeeper的命名空间是由一系列数据节点组成的,我们将对数据节点做详细讲解。

节点类型

在ZooKeeper中,每个数据节点都是有生命周期的,其生命周期的长短取决于数据节点的节点类型。在ZooKeeper中,节点类型可以分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)三大类,ju'ti具体在节点创建过程中,通过组合使用,可以生成以下四种组合型节点类型:

  • 持久节点(PERSISTENT)

数据节点被创建后,就会一直存在于ZooKeeper服务器上,直到有删除操作来主动清除这个节点。

  • 持久顺序节点(PERSISTENT_SEQUENTIAL)

他的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在ZooKeeper中,每个父节点都会为他的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,ZooKeeper会自动为给定节点加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。

  • 临时节点(EPHEMERAL)

临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。这里提到的客户端会话失效,而非TCP连接断开。

  • 临时顺序节点(EPHEMERAL_SEQUENTIAL)

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

状态信息

每个数据节点除了存储了数据内容外,还存储了数据节点本身的一些状态信息。

状态属性 说明
czxid 即Created ZXID,表示该节点被创建时的事务ID
mzxid 即Modified ZXID,表示该节点最后一次被更新时的事务ID
ctime 即Created Time
mtime 即Modified Time
version 数据节点的版本号
cversion 子节点的版本号
aversion 节点的ACL版本号
ephemeralOwner 创建该临时节点的会话的sessionID。如果该节点是持久节点,那么这个属性值为0
dataLength 数据内容长度
numChildren 当前节点的子节点个数
pzxid 表示该节点的子节点列表最后一次被修改时的事务ID。注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid。

1.3 版本-保证分布式数据原子性操作

ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。

版本类型 说明
version 当前数据节点数据内容的版本号
cversion 当前数据节点子节点的版本号
aversion 当前数据节点ACL变更版本号

在ZooKeeper中,version属性正是用来实现乐观锁机制中的“写入校验”的。

version = setDataRequest.getVersion();
int currentVersion = nodeRecord.stat.getVersion();
if(version != -1 && version != currentVersion) {
    throw new KeeperException.BadVersionException(path);
}
version = currentVersion + 1;

1.4 Watcher-数据变更的通知

在ZooKeeper中,引入了Watcher机制来实现这种分布式的通知功能。ZooKeeper允许客户端向服务端注册一个Watcher监听,当服务器的一些指定事件出发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

Wacher机制概述

从图中我们可以看到,ZooKeeper的Watcher机制主要包括ke'hu'duan'xian'c客户端线程、客户端WatcherManager和ZooKeeper服务器三部分。在具体工作流程上,客户端在向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatcherManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑。

1.5 ACL--保障数据的安全

提到权限控制,我们首先看看目前应用最广泛的权限控制方式--UGO(User、Group和Others)权限控制机制。简单地讲,UGO就是针对一个文件或目录,对创建者(User)、创建者所在的组(Group)和其他用户(Other)分别配置不同的权限。从这里可以看出,UGO其实是一种粗粒度的文件系统权限控制模式,利用UGO只能对三类用户jin'xing进行权限控制,即文件的创建者、创建者所在的组以及其他所有用户,很显然,UGO无法解决下面这个场景:

用户U1创建了文件F1,希望U1所在的用户组G1拥有对F1读写和执行的权限,另一个用户组G2拥有读权限,而另一个用户U3则没有任何权限。

下面我们来看另外一种典型的权限控制方式:ACL。ACL,即访问控制列表,是一种相对来说比较新颖且更细粒度的权限管理方式。可以针对任何用户和组进行细粒度的权限控制。

ACL介绍

ZooKeeper的ACL权限控制和Unix/Linux操作系统中的ACL有一些区别,读者可以从三个方面来理解ACL机制,分别是:权限模式(Scheme)、授权对象(ID)和权限(Permission),通常使用“scheme:id:permission”来标识一个有效的ACL信息。

权限模式:Scheme

权限模式用来确定权限验证过程中使用的校验策略。在ZooKeeper中,开发人员使用最多的就是以下四种权限模式。

  • IP

IP模式通过IP地址粒度来进行权限控制。也支持按照网段的方式进行配置。

  • Digest

以类似于“username:password”形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制。

  • World

数据节点的访问权限对所有用户开发,即所有用户可以在不进行任何权限校验的情况下操作ZooKeeper上的数据。另外,World模式也可以看作是一种特殊的Digest模式,他只有一个权限标识,即“world:anyone”。

  • Super

超级用户的意思。

授权对象:ID

权限赋予的用户或一个指定实体。在不同的权限模式下,授权对象是不同的。

权限:Permission

在ZooKeeper中,所有对数据的操作权限分为以下五大类:

  • CREATE(C)
  • DELETE(D)
  • READ(R)
  • WRITE(W)
  • ADMIN(A)

权限扩展体系

实现自定义权限控制器

二、序列化协议

ZooKeeper的客户端和服务端之间会进行一系列的网络通信以实现数据的传输。对于一个网络通信,首先要解决的就是对数据的序列化和反序列化处理,在ZooKeeper中,使用了Jute这一序列化组件来进行数据的序列化和反序列化操作。同时,为了实现一个高效的网络通信程序,良好的通信协议设计也是至关重要的。

通信协议

基于TCP/IP协议,ZooKeeper实现了自己的通信协议来完成客户端与服务端、服务端与服务端之间的网络通信。ZooKeeper通信协议整体上的设计非常简单,对于请求,主要包含请求头和请求体,对于响应,则主要包含响应头和相应体。

三、客户端

客户端是开发人员使用ZooKeeper最主要的途径,因此我们有必要对ZooKeeper客户端的内部原理进行详细讲解。ZooKeeper的客户端主要由以下几个核心组件组成。

  • ZooKeeper实例:客户端的入口。
  • ClientWatchManager:客户端Watcher管理器。
  • HostProvider:客户端地址列表管理器。
  • ClientCnxn:客户端核心线程,其内部又包含两个线程,即SendThread和EventThread。前者是一个I/O线程,主要负责ZooKeeper客户端和服务端之间的网络I/O通信;后者是一个事件线程,主要负责对服务端事件进行处理。

客户端的整个初始化和启动过程大体可以分为以下三个步骤。

    1. 设置默认Watcher。
    1. 设置ZooKeeper服务器地址列表。
    1. 创建ClientCnxn。

3.1 一次会话的创建过程

初始化阶段

  1. 初始化ZooKeeper对象。
  2. 设置会话默认Watcher。
  3. 构造ZooKeeper服务器地址列表管理器:HostProvider。
  4. 创建并初始化客户端网络连接器:ClientCnxn。
  5. 初始化SendThread和EventThread。

会话创建阶段

  1. 启动SendThread和EventThread
  2. 获取一个服务器地址。
  3. 创建TCP连接。
  4. 构造ConnectRequest请求。
  5. 发送请求。

响应处理阶段

  1. 接受服务端响应
  2. 处理Response
  3. 连接成功
  4. 生成时间:SyncConnected-None
  5. 查询Watcher
  6. 处理事件

四、会话

会话(Session)是ZooKeeper中最重要的概念之一,客户端和服务端之间的任何交互操作都与会话息息相关,这其中就包括临时节点的生命周期、客户端请求的顺序执行以及Watcher通知机制等。

4.1 会话状态

在ZooKeeper客户端和服务端成功完成连接创建后,就建立了一个会话。ZooKeeper会话在整个运行期间的声明周期中,会在不同的会话状态之间进行切换,这些状态一般可以分为CONNECTING、CONNECTED、RECONNECTING、RECONNECTED和CLOSE等。

如果客户端需要与服务端创建一个会话,那么客户端必须提供一个使用字符串表示的服务器地址列表:“host1:port,host2:port,host3:port”。一旦客户端开始创建ZooKeeper对象,那么客户端状态就会变成CONNECTING,同时客户端开始从上述服务器地址列表中逐个选取IP地址来尝试进行网络连接,直到成功连接上服务器,然后将客户端状态变更为CONNECTED。

通常,伴随着网络闪断或是其他原因,客户端和服务器之间的连接会出现断开情况。一旦碰到这种情况,ZooKeeper客户端会自动进行重连操作,同时客户端的状态再次变为CONNECTING,直到重新连接上ZooKeeper服务器后,客户端状态又会再次转变成CONNECTED。因此,在通常情况下,在ZooKeeper运行期间,客户端的状态总是介于CONNECTING和CONNECTED两者之一。

另外,如果出现诸如会话超时、权限检查失败或是客户端主动退出程序等情况,那么客户端的状态就会直接变为CLOSE。

4.2 会话创建

Session

Session是ZooKeeper中的会话实体,代表了一个客户端会话。其包含以下4个基础属性、

  • sessionId:会话id,用来唯一标识一个会话,每次客户端创建新会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionId。
  • TimeOut:会话超时时间。客户端在构造ZooKeeper实例的时候,会配置一个sessionTimeout参数用于指定会话的超时时间。ZooKeeper客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定会话的超时时间。
  • TickTime:下次会话超时时间点。
  • isClosing:该属性用于标记一个会话是否已经被关闭。

sessionID

在SeesionTracker初始化的时候,会调用initializeNextSession方法来生成一个初始化的sessionID,之后在ZooKeeper的正常运行过程中,会在该sessionID的基础上为每个会话进行分配,其初始化算法如下:

public static long initializeNextSeesion(long id) {
    long nextSid = 0;
    nextSid = (System.currentTimeMillis() << 24) >> 8;
    nextSid = nextSid | (id << 56);
    return nextSid;
}

上面这个方法就是ZooKeeper初始化sessionID的算法,我们一起深入的探究下。从上面的代码片段中,可以看出sessionID的生成大体可以分为以下5个步骤。

  1. 获取当前的毫秒表示。
  2. 左移24位。
  3. 右移8位。
  4. 添加机器标识:SID。
  5. 将步骤3和步骤4得到的两个64位表示的数值进行“|”操作。

简单地讲,可以将上述算法概括为:高8位确定了所在机器,后56位使用当前时间的毫秒进行随机。

SessionTracker

SessionTracker是ZooKeeper服务端的会话管理器,负责会话的创建、管理和清理等工作。可以说,整个会话的生命周期都离不开SessionTracker的管理。每一个会话在SessionTracker内部都保留了三份,具体如下。

  • sessionsById:这是一个HashMap<Long, SeesionImpl>类型的数据结构,用于根据sessionID来管理Session实体。
  • sessionsWithTimeout:这是一个ConcurrentHashMap<Long, Integer>类型的数据结构,用于根据sessionID来管理会话的超时时间。该数据结构和ZooKeeper内存数据库相连通,会被定期持久化到快照文件中去。
  • sessionSets:这是一个HashMap<Long, SessionSet>类型的数据结构,用于根据下次会话超时时间来归档会话,便于进行会话管理和超时检查。

创建连接

服务端对于客户端的“会话创建”请求的处理,大体可以分为四大步骤,分别是ConnectRequest请求、会话创建、处理器链路处理和会话响应。

4.3 会话管理

分桶策略

ZooKeeper的会话管理主要是由SessionTracker负责的,其采用了一种特殊的会化管理方式,我们称之为“分桶策略”。所谓分桶策略,是指将类似的会话放在同一区块中进行管理,以便于ZooKeeper对会话进行不同区块的格里处理以及同一区块的统一处理。

ZooKeeper将所有的会话都分配在了不同的区块之中,分配的原则是每个会话的“下次超时时间点”(ExpirationTime)。ExpirationTime是指该会话最近一次可能超时的时间点,对于一个新创建的会话而言,其会话创建完毕后,ZooKeeper就会为其计算ExpirationTime,计算方式如下:

ExpirationTime = CurrentTime + SessionTimeout

在ZooKeeper的实际实现中,Zookeeper的Leader服务器在运行期间会定时的进行会话超时检查,其时间间隔是ExpirationInterval,单位是毫秒,默认值是tickTime的值,即默认情况下,每隔2000毫秒进行一次会话超时检查。为了方便对多个会话同时进行超时检查,完整的ExpirationTime的计算方式如下:

ExpirationTime_ = CurrentTime + SessionTimeout
ExpirationTime = (ExpirationTime_/ExpirationInterval + 1) * ExpirationInterval

会话激活

为了保持客户端会话的有效性,在ZooKeeper的运行过程中,客户端会在会话超时时间国企范围内向服务端发送PING请求来保持会话的有效性,我们俗称“心跳检测”。同时,服务端需要不断地接收来自客户端的这个心跳检测,并且需要重新激活对应的客户端会话,我们将这个重新激活的过程称为TouchSession。会话激活的过程,不仅能够使服务端检测到对应客户端的存活性,也能让客户端自己保持连接状态。

会话超时检查

在ZooKeeper中,会话超时检查同样是由SessionTracker负责的。SessionTracker中有一个单独的线程专门进行会话超时检查,这里我们称其为“超时检查线程”,其工作机制的核心思路非常简单:逐个依次对会话桶中剩下的会话进行清理。

4.4 会话清理

当SessionTracker的会话超时检查线程整理出一些已经过期的会话后,那么就要开始进行会话清理了。会话清理的步骤大致可以分为以下七步。

  1. 标记会话状态为“已关闭”

为了保证在清理期间不再处理来自该客户端的新请求,SessionTracker会首先将该会话的isClosing属性标记为true。

  1. 发起“会话关闭”请求

为了使该会话的关闭操作在整个服务端集群中都生效,ZooKeeper使用了提交“会话关闭”请求的方式,并立即交付给PrepRequestProcessor处理器进行处理。

  1. 收集需要清理的临时节点

在ZooKeeper的内存数据库中,为每个会话都单独保存了一份由该会话维护的所有临时节点集合,因此在会话清理阶段,只需要根据当前即将关闭的会话的sessionID从内存数据库中获取到这份临时节点列表即可。

实际上,有如下细节需要处理:在ZooKeeper处理会话关闭请求之前,正好有以下请求到达了服务端并正在处理中:

  • 节点删除请求,删除的目标节点正好是上述临时节点中的一个。
  • 临时节点创建请求,创建的目标节点正好是上述临时节点中的一个。

嘉定我们当前获取的临时节点列表是ephemerals,那么针对第一类请求,我们需要将所有这些请求对应的数据节点路径从ephemerals中移除,以避免重复删除。针对第二类,我们需要将所有这些请求对应的数据节点路径添加到ephemerals中去,以删除这些即将会被创建但是尚未保存到内存数据库中去的临时节点。

  1. 添加“节点删除”事务变更

完成该会话相关的临时节点收集后,ZooKeeper会逐个将这些临时节点转换成“节点删除”请求,并放入事务变更队列outstandingChanges中去。

  1. 删除临时节点

FinalRequestProcessor处理器会触发内存数据库,删除该会话对应的所有临时节点。

  1. 移除会话

完成节点删除后,需要将会话从SessionTracker中移除。主要就是从上面提到的三个数据结构(sessionById、sessionsWithTimeout和sessionSets)中将该会话移除掉。

  1. 关闭NIOServerCnxn

最后,从NIOServerCnxnFactory找到该会话对应的NIOServerCnxn,将其关闭。

4.5 重连

当客户端和服务端之间的网络连接断开时,ZooKeeper客户端会自动进行反复的重连,知道最终成功连接上ZooKeeper集群中的一台机器。在这种情况下,再次连接上服务端的客户端有可能会处于以下两种状态之一。

  • CONNECTED:重连成功
  • EXPIRED:如果是在会话超时时间以外重新连接上,那么服务端其实已经对该会话进行了会话清理操作,因此再次连接上的会话将被视为非法会话。

当客户端和服务端之间的连接断开后,用户在客户端可能会看到两类异常:CONNECTION_LOSS(连接断开)和SESSION_EXPIRED(会话过期)。

五、服务器启动

我们首先看看ZooKeeper服务端的整体架构,如图

ZooKeeper服务端整体架构

5.1 单机版服务器启动

ZooKeeper服务器的启动,大体可以分为以下五个主要步骤:配置文件解析、初始化数据管理器、初始化网络I/O管理器、数据恢复和对外服务。下图是单机版ZooKeeper服务器的启动流程图。

单机版ZooKeeper服务器的启动流程

预启动

预启动的步骤如下。

  1. 统一由QuorumPeerMain作为启动类
  2. 解析配置文件zoo.cfg
  3. 创建并启动历史文件清理器DatadirCleanupManager
  4. 判断当前是集群模式还是单机模式的启动
  5. 再次进行配置文件zoo.cfg的解析
  6. 创建服务器实例ZooKeeperServer

初始化

初始化的步骤如下。

  1. 创建服务器统计器ServerStats
  2. 创建ZooKeeper数据管理器FileTxnSnapLog
  3. 设置服务器tickTime和会话超时时间限制
  4. 创建ServerCnxnFactory
  5. 初始化ServerCnxnFactory
  6. 启动ServerCnxnFactory主线程
  7. 恢复本地数据
  8. 创建并启动会话管理器
  9. 初始化ZooKeeper的请求处理链
  10. 注册JMX服务
  11. 注册ZooKeeper服务器实例

5.2 集群版服务器启动

集群版和单机版ZooKeeper服务器启动过程在很多地方是一致的,所以这里只会对有差异的地方展开进行讲解。下图是集群版ZooKeeper服务器的启动流程图。

集群版ZooKeeper服务器的启动流程

预启动

预启动的步骤如下。

  1. 统一由QuorumPeerMain作为启动类
  2. 解析配置文件zoo.cfg
  3. 创建并启动历史文件清理器DatadirCleanupManager
  4. 判断当前是集群模式还是单机模式启动

初始化

初始化的步骤如下

  1. 创建ServerCnxnFactory
  2. 初始化ServerCnxnFactory
  3. 创建ZooKeeper数据管理器FileTxnSnapLog
  4. 创建QuorumPeer
  5. 创建内存数据库ZKDatabase
  6. 初始化QuorumPeer
  7. 恢复本地数据
  8. 启动ServerCnxnFactory主线程

Leader选举

Leader选举的步骤如下

  1. 初始化Leader选举
  2. 注册JMX服务
  3. 监测当前服务器状态
  4. Leader选举

Leader和Follower启动期交互过程

Leader和Follower启动期交互过程

六、Leader选举

6.1 Leader选举概述

服务器启动时期的Leader选举

要进行Leader选举的时候,隐式条件便是ZooKeeper的集群规模至少是2台机器,只有一台服务器启动的时候,是无法进行Leader选举的。

  1. 每个Server会发出一个投票

初始情况,对于Server1和Server2来说,都会投给自己,每次投票包含的最基本的元素包括:所推举的服务器myid和ZXID。

  1. 接收来自各个服务器的投票

集群中每个服务器在收到投票后,首先会判断投票的有效性,包含检查是否是本轮投票,是否来自LOOKING状态的服务器。

  1. 处理投票

在接收到来自其他服务器的投票后,针对每个投票,服务器都需要将别人的投票和自己的投票进行PK,PK的规则如下

  • 优先检查ZXID。ZXID比较大d服务器优先作为Leader
  • 如果ZXID相同的话,那么就比较myid。myid比较大的服务器作为Leader服务器。
  1. 统计投票

每次投票后,服务器都会统计所有投票,判断是否已经有过半机器接收到相同的投票信息。

  1. 改变服务器状态

一旦确定了Leader,每个服务器就会更新自己的状态:如果是Follower,那么就变更为FOLLOWING,如果是Leader,那么就变更为LEADING。

服务器运行期间的Leader选举

在ZooKeeper集群正常运行过程中,一旦选出一个Leader,那么所有服务器的集群角色一般不会再发生变化,不管是是非Leader集群挂了还是新机器加入集群,都不会影响Leader。一旦Leader挂了,那么整个集群将暂时无法对外服务,而是进入新一轮的Leader选举。

6.2 Leader选举的算法分析

在ZooKeeper中,提供了三种Leader选举的算法,分别是LeaderElection、UDP版本的FastLeaderElection和TCP版本的FastLeaderElection,可以通过在配置文件zoo.cfg中使用electionAlg属性来指定,分别用数字0-3表示。0表示LeaderElection,1表示UDP版本的FastLeaderElection,并且是非授权模式,2表示UDP版本的FastLeaderElection,使用授权模式,3代表TCP版本的FastLeaderElection。从3.4.0版本开始,Zookeeper废弃了0-2这三种算法,只保留了TCP版本的FastLeaderElection选举算法。

术语解释

  • SID:服务器ID
  • ZXID:事务ID
  • Vote:投票
  • Quorum:过半机器数

算法分析

进入Leader选举

当ZooKeeper集群中的一台服务器出现以下两种情况时,就会开始进入Leader选举

  • 服务器初始化启动
  • 服务器运行期间无法和Leader保持连接

而当一台机器进入Leadeader选举流程时,当前集群也可能会处于以下两种状态

  • 集群中本来就存在一个Leader
  • 集群中确实不存在Leader

第一种情况,这种情况通常是某一台服务器启动比较晚,在他启动之前,集群已经可以正常工作。针对这种情况,当该机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立起连接,并进行状态同步即可。

下面我们看看集群中不存在Leader的情况下,如何进行Leader选举。

开始第一次投票

通常有两种情况会导致集群中不存在Leader,一种是整个服务器刚刚初始化启动时,另一种情况就是运行期间当前Leader所在的服务器挂了。此时,集群中所有机器都处于LOOKING的状态。当一台服务器处于LOOKING状态时,那么他就会向集群中所有其他机器发送消息,我们称这个消息为“投票”。

在这个投票消息中包含了两个最基本的信息:所推举的服务器SID和ZXID,用(SID,ZXID)表示。一般都是投自己。

变更投票

集群中每台机器发出自己的投票后,也会接收到来自集群中其他机器的投票。每台机器都会根据一定的规则,来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票。这个规则也成了整个Leader选举算法的核心所在。我们首先定义一些术语。

  • vote_sid:接收到的投票中所推举Leader服务器的SID
  • vote_zxid:接收到的投票中所推举Leader服务器的ZXID
  • self_sid:当前服务器自己的SID
  • self_zxid:当前服务器自己的ZXID

对比过程如下:

  • 规则1:如果vote_zxid>self_zxid,就认可当前收到投票,并再次将该投票发送出去。
  • 规则2:如果vote_zxid<self_zxid,就坚持自己的投票,不作任何变更。
  • 规则3:如果vote_zxid=self_zxid,就对比两者的SID。如果vote_sid>self_sid,就认可当前收到的投票,并在此将该投票发出去。
  • 规则4:如果vote_zxid=self_zxid,并且vote_sid<self_sid,那么同样坚持自己的投票,不作变更。

确定Leader

经过这第二次投票后,集群中每台机器都会再次受到其他机器的投票,然后开始统计投票。如果一台机器收到了超过半数的相同的投票,那么这个投票对应的SID机器ji'wei即为Leader。

小结

通常哪台服务器上的越新,那么越有可能成为Leader,原因很简单,数据越新,ZXID越大,也就越能够保证数据的恢复。

6.3 Leader选举的实现细节

服务器状态

  • LOOKING
  • FOLLOWING
  • LEADING
  • OBSERVING

投票数据结构

属性 说明
id 被推举的Leader的SID值
zxid 被推举的Leader的事务ID
electionEpoch 逻辑时钟,用来判断多个投票是否在同一轮选举周期中。该值在服务端是一个自增序列。每次进入新一轮投票后,都会对该值进行加一
peerEpoch 被推举的Leader的epoch
state 当前服务器状态

QuorumCnxManager:网络I/O

每台服务器启动的时候,都会启动一个QuorumCnxManager,负责各台服务器之间的底层Leader选举过程中的网络通信。

消息队列

在QuorumCnxManager这个类内部维护了一系列的队列,用于保存接收到的、待发送的消息,以及消息的发送器。

  • recvQueue:消息接收队列
  • queueSendMap:消息发送队列,用于保存那些待发送的消息
  • senderWorkerMap:发送器集合
  • lastMessageSent:最近发送过的消息

建立连接

QuorumCnxManager在启动的时候,会创建一个ServerSocket来监听Leader选举的通信接口(Leader选举的通信端口默认是3888)。开启端口监听后,ZooKeeper就能够不断地接收到来自其他服务器的“创建连接”请求,在收到其他服务器的TCP连接请求时,会交由receiveConnection函数来处理。为了避免两台机器之间重复的创建TCP连接,ZooKeeper设计了建立TCP连接的规则:只允许SID大的服务器主动与其他服务器建立连接,否则断开链接。

一旦建立起连接,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动他们。

消息接收与发送

消息的接收过程是由消息接收器RecvWorker来负责的。ZooKeeper会为每个远程服务器分配一个单独的RecvWorker,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中。

消息发送过程也比较简单,由于ZooKeeper同样已经为每个远程服务器单独分别分配了消息发送器SendWorker,那么每个SendWorker只需要不断地从对应的消息发送队列中取出一个消息来发送即可,同时将这个消息放入lastMessageSent中来作为最近发送过的消息。

FastLeaderElection:选举算法的核心部分

先约定几个概念:

  • 外部投票:其他服务器发来的投票
  • 内部投票:自身当前的投票
  • 选举轮次:ZooKeeper服务器Leader选举的轮次,即logicalclock
  • PK:指对内部投票和外部投票进行一个对比来确定是否需要变更内部投票

选票管理

  • sendqueue:选票发送队列,用于保存待发送的选票
  • recvqueue:选票接收队列,用于保存接收到的外部投票
  • WorkerReceiver:选票接收器。该接收器会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvQueue队列中去。在选票接收过程中,如果发现该外部投票的选举轮次小于当前服务器,就直接忽略这个外部投票,同时立即发出自己的内部投票。当然,如果当前服务器并不是LOOKING状态,即yi'j已经选出了Leader,那么也将忽略这个外部投票,同时将Leader信息已投票信息发送出去。另外,如果接收到的消息来自Observer服务器,那么就直接忽略掉,并将自己当前的投票发送出去。
  • WorkerSender:选票发送器,会不断从sendqueue队列中获取待发送的选票,并将其传递到底层QuorumCnxManager中去。

算法核心

Leader选举算法实现的流程示意图

七、各服务器角色介绍

7.1 Leader

Leader服务器是整个ZooKeeper集群工作机制的核心,其主要工作有以下两个。

  • 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
  • 集群内部各服务器的调度者。

7.2 Follower

Follower服务器是ZooKeeper集群状态的跟随者,主要工作

  • 处理客户端非事务请求,转发事务请求给Leader服务器。
  • 参与事务请求Proposal的投票
  • 参与Leader选举投票

7.3 Observer

工作原理与Follower基本一致,唯一区别在于Observer不参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票。简单的讲,Observer服务器只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

7.4 集群间消息通信

ZooKeeper的消息类型大体上可以分为四类,分别是:数据同步型、服务器初始化型、请求处理型和会话管理型。

数据同步型

是指在Learner和Leader服务器进行数据同步的时候,网络通信所用到的消息,通常有DIFF、TRUNC、SNAP和UPTPDATE四种。

服务器初始化型

是指在整个集群或是某些新机器初始化时,Leader和Learner之间相互通信所使用的消息类型,常见的有OBSEERVERINFO、FOLLOWERINFO、LEADERINFO、ACKEPOCH和NEWLEADER五种。

请求处理型

是指在进行请求处理的过程中,Leader和Learner服务器之间相互通信所使用的消息,常见的有REQUEST、PROPOSAL、ACK、COMMIT、INFORM和SYNC六种。

会话管理型

是指ZooKeeper在进行会话管理的过程中,和Learner服务器之间互相通信所使用的消息,常见的有PING和REVALIDATE两种。

八、请求处理

8.1 会话创建请求

ZooKeeper服务端对于会话创建的处理,大体可以分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应6大环节。

8.2 SetData请求

服务端对于SetData请求的处理,大体可以分为4大步骤,分别是请求的预处理、事务处理、事务应用和请求响应。

8.3 事务请求转发

在事务请求的处理过程中,需要我们注意的一个细节是,为了保证事务请求被顺序执行,从而确保ZooKeeper集群的数据一致性,所有的事务请求必须由Leader服务器来处理。但是,并不是所有的ZooKeeper都和Leader服务器保持连接,那么如何保证所有的事务请求都由Leader来处理呢?

ZooKeeper实现了非常特别的事务请求转发机制:所有非Leader服务器如果接收到了来自客户端的事务请求,那么必须将其转发给Leader服务器来处理。

8.4 GetData请求

服务端对于GetData请求的处理,大体可以分为3大步骤,分别是请求的预处理、非事务处理和请求响应。

九、小结

ZooKeeper以树作为其内存数据模型,树上的每一个节点是最小的数据单元,即ZNode。ZNode具有不同的节点特性,同时每个节点都具有一个递增的版本号,以此可以实现分布式数据的原子性更新。

ZooKeeper的序列化层使用从Hadoop中遗留下来的Jute组件,该组件并不是性能最好的序列化框架,但是在ZooKeeper中已经够用。

ZooKeeper的客户端和服务端之间会建立起TCP长连接来进行网络通信,基于该TCP连接衍生出来的会话概念,是客户端和服务端之间所有请求和响应交互的基石。在会话的生命周期中,会出现连接断开、重连或是会话失效等一系列问题,这些都是ZooKeeper的会话管理器需要处理的问题--Leader服务器会负责管理每个会话的生命周期,包括会话的创建、心跳检测和销毁等。

在服务器启动阶段,会进行磁盘数据的恢复,完成数据恢复后就会进行Leader选举。一旦选举产生Leader服务器后,就立即开始进行集群间的数据同步--在整个过程中,ZooKeeper都处于不可用状态,知道数据同步完毕(集群中绝大部分机器数据和Leader一致),ZooKeeper才可以对外提供正常服务。在运行期间,如果Leader服务器所在的机器挂掉或是和集群中绝大部分服务器断开连接,那么就会触发新一轮的Leader选举。同样,在新的Leader服务器选举产生之前,ZooKeeper无法对外提供服务。

一个正常运行的ZooKeeper集群,其机器通常由Leader、Follower和Observer组成。ZooKeeper对于客户端请求的处理,严格按照ZAB协议规范来进行。每个服务器在启动初始化阶段都会组装一个请求处理链,Leader服务器能够处理所有类型的客户端请求,而对于Follower或是Observer服务器来说,可以正常处理非事务请求,而事务请求则需要转发给Leader服务器来处理,同时,对于每个事务请求,Leader都会为其分配一个全局唯一且递增的ZXID,以此来保证事务处理的顺序性。在事务请求的处理过程中,Leader和Follower服务器都会进行事务日志的记录。

ZooKeeper通过JDK的File接口简单实现了自己的数据存储系统,其底层数据存储包括事务日志和快照数据两部分,这些都是ZooKeeper实现数据一致性非常关键的部分。

推荐阅读更多精彩内容