暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Zookeeper#深入学习、使用(四)

小怂读书 2021-11-23
399

《zk:分布式一致性原理》学习,完结。

前面已经整理了zk的使用以及一些基本的概念,对于如何部署集群、zk在企业级的应用场景应该有所了解了,出去水一波应该差不多大致足够了吧。但是,仔细想一下,前面一直说zk是分布式数据一致的解决方案,它究竟是如何实现的这些唯一ID、命名服务、分布式锁等等功能的呢?


1、zk系统模型

1)数据模型

zk的视图结构和标准的Unix文件系统类似,都是通过路径来表示数据存储,我们成为ZNode。ZNode是zk中数据的最小单元,每个ZNode上保存数据,同时还可以挂载子节点,构成一个层次化的空间,我们称之为树。如下图所示。

zk中事务是指能够改变zk服务器状态的操作,我们也称之为事务操作或者更新操作,一般包括数据节点创建和删除、数据节点内容更新和客户端会话创建与失效等。对于每一个事务请求,zk会为其分配一个全局唯一的事务ID,用ZXID来表示(64位)。

2)节点特性

zk的命名空间是由一系列数据节点组成的。

在zk中每个数据节点都是有生命周期的,生命周期的长短取决于数据节点的节点类型:持久节点、临时节点、顺序节点。

  • 持久节点。zk中最常见的一种节点,该数据节点被创建后会一直存在于zk服务器上,直到删除操作来主动清除

  • 持久顺序节点。持久节点+顺序性。zk中每个父节点会为它的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。创建节点时,zk会自动为给定节点名称加上一个后缀数字(int最大值)。

  • 临时节点。临时节点的生命周期和客户端的会话(Session)绑定。zk规定不能基于临时节点来创建节点,即临时节点只能作为叶子节点

  • 临时顺序节点

zk的数据节点除了存储数据内容之外,还存储了数据节点自身的状态信息,如下图所示。

这些状态信息实际上是数据节点Stat对象的格式化输出,Stat对象状态属性说明如下。

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

zk中在数据节点中引入了版本的概念,每个数据节点都有三个类型的版本信息,如上图所示:version、cversion、aversion,对数据节点的任何更新操作都会引起版本号的变化

注:version表示的是对数据节点数据内容的变更次数,强调变更次数,即使两次变更没有使得数据内容值发生变化,version值依然会变更
我们通常在分布式系统中通过版本来实现
乐观锁控制(数据读取、写入校验、数据写入)。

zk中version属性是用来实现乐观锁机制中的“写入校验”的。在zk内部PrepRequestProcessor处理器类中处理每一个数据更新请求时(setDataRequest)会检查当前请求的version版本,如果是-1说明客户端不要求使用乐观锁;如果不是-1比较version和currentVersion。

4)Watcher-数据变更的通知

zk中引入了Watcher机制来实现分布式的通知功能,允许客户端向服务端注册Watcher监听。

zk的监听机制包括客户端线程、客户端WatchManager、zk服务器三部分。简单来说,客户端向zk服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中;当zk服务端触发Watcher时间后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调

5)ACL-保障数据的安全

ACL访问控制列表包括三个方面:权限模式(Scheme)、授权对象(ID)、权限(Permission),通常使用scheme:id:permission来标识一个有效的ACL信息。

scheme:IP、Digest、World、Super

permission:Create、Delete、Read、Write、Admin


2、序列化协议

zk的客户端和服务端会进行一系列的网络通信从而实现数据的传输,在进行网络传输时首先要解决的问题就是对数据的序列化和反序列化。zk使用Jute这一序列化组件来进行数据的序列化和反序列化。(种种原因,Jute这个老的序列化组件一直没有被替换掉,现在还在zk中使用)


3、一次会话的创建过程(客户端)

1)初始化阶段(白底框图)

  • 初始化zk对象:调用zk构造方法实例化对象、创建客户端Watcher管理器:ClientWatchManager

  • 设置会话默认Watcher:如果构造方法传入Watcher对象,客户端将其作为默认Watcher保存到ClientWatchManager中

  • 构造zk服务器地址列表管理器:HostProvider,构造方法中传入的服务器地址会存放在HostProvider中

  • 创建并初始化客户端网络连接器:ClientCnxn,用来管理客户端和服务端的网络交互,同时初始化客户端两个核心队列:outgoingQueue(请求发送队列)和pendingQueue(服务端响应等待队列)

  • 初始化SendThread和EventThread,SendThread用于管理客户端和服务端之间所有的网络IO,EventThread用于进行客户端的事件处理

2)会话创建阶段(斜线底纹)

  • 启动SendThread和EventThread,SendThread会先判断当前客户端状态,进行一系列清理性工作,为客户端发送“会话创建”请求准备

  • 获取一个服务器地址:开始创建TCP连接前,SendThread需要获取一个zk服务器的目标地址,通常是从HostProvider中随机获取一个,然后委托给ClientCnxnSocket去创建与zk服务器之间的TCP连接

  • 创建TCP连接:与zk服务器创建一个TCP长连接

  • 构造ConnectRequest请求,TCP连接创建完成后,发送请求

3)响应处理阶段(点状底纹)

  • 接收服务端响应、处理Response

  • 连接成功、生成事件、处理事件等


4、zk服务器地址选择

创建Zookeeper构造函数的时候我们会传入一系列的ip:port地址,这是zk服务器的地址列表,Q:zk客户端在连接服务器的过程中,如何选择要连接的ip端口呢

客户端对传入的地址通过HostProvider存放(地址列表管理器),类似于ArraysList的结构,元素就是ip和端口的结构。客户端通过next()方法获取一个可用的服务器地址,next()方法并不是简单从地址列表中获取一个,而是先将服务器地址随机打散(CollectionUtils的shuffle()方法),然后打散的地址列表组成一个环形循环队列,如下图所示。

(注:这个随机过程是一次性的,之后使用过程都是按照这样的顺序来获取)

假如客户端传入这样一个地址列表:“host1,host2,host3,host4,host5”。经过一轮随机打散后,可能的一种顺序变成了“host2,host4,host1,host5,host3”。HostProvider还会为该循环队列创建两个游标:currentIndex 和 lastIndex。currentIndex 表示循环队列中当前遍历位置,lastIndex 则表示当前正在使用的服务器地址位置。初始化的时候,currentIndex和lastIndex的值都为-1。

在每次尝试获取一个服务器地址的时候,都会首先将 currentIndex 游标向前移动 1位,如果发现游标移动超过了整个地址列表的长度,那么就重置为 0,回到开始的位置重新开始,这样一来,就实现了循环队列。当然,对于那些服务器地址列表提供得比较少的场景,StaticHostProvider中做了一个小技巧,就是如果发现当前游标的位置和上次已经使用过的地址位置一样,即当 currentIndex 和 lastIndex 游标值相同时,就进行spinDelay毫秒时间的等待。

整个过程非常类似于“Round Robin”的调度策略。

服务器地址可以:代码中硬编码、配置文件、动态变更地址列表管理器等方式。


5、会话(Session)

客户端和服务端之间的任何交互操作都与会话息息相关,包括临时节点的生命周期、客户端请求的顺序执行以及Watcher通知机制等。zk客户端和服务端完成连接创建后,就建立了一个会话。在运行期间,会话的状态会在CONNECTING、CONNECTED、RECONNECTION、RECONNECTED和CLOSE之间切换。

  • 客户端给出服务器的地址list,创建zk对象 -> CONNECTING

  • 客户端从地址列表中逐个选取IP尝试连接,直到连上服务器 -> CONNECTED

网络中断等情况导致Client和Server之前连接断开,zk客户端会重连。重连过程重复上面状态。出现会话超时、权限检查失败或客户端主动退出等情况,客户端状态 -> CLOSE

1)会话创建

Session有四个属性:

  • sessionID:唯一标识一个会话,每次客户端创建新会话时,zk分配全局唯一。

  • TimeOut:超时时间,服务器根据自己的超时时间确定最终会话的超时时间。

  • TickTime:下次会话超时时间,zk为每个会话标记一个下次会话超时时间点,13位long型数据(≈now()+timeOut值),方便对会话实行分桶策略管理。

  • isClosing:标记一个会话是否被关闭。

2)SessionID

  • 获取当前时间毫秒数 -> 64位二进制数据 t

  • t左移24位(高24位移出,低位补0) -> 右移8位(高位补0)

<< 24:将高位1移出,剩下最高位0,防止负数出现(不行啊,会出现负数,时间戳够大的情况)。

  • 添加SID(myId文件中的机器ID值,最大255,所以只需要8位),SID左移56位 s

  • t | s => sessionID

3)会话管理

zk服务端如何进行会话管理?

zk的会话管理主要是由SessionTracker负责的,采用“分桶策略”。分桶策略是指将类似的会话放在同一个区块中进行管理,以便于zk对会话进行不同区块的隔离处理和统一处理。

zk将所有的会话分配在不同的区块中,分配的原则是每个会话的TickTime(下次超时时间点)。

4)重连

客户端和服务器断开连接时,zk客户端会反复重连直到成功连接上zk的一台机器。这种情况下的客户端可能处于以下两种状态之一:

  • CONNECTED:会话超时时间内连上了,重连成功

  • EXPIRED:会话超时时间已过,服务端已经对该会话进行了清理,这时连上视为非法会话

zk的客户端和服务器之前的会话维持的是一个TCP的长连接。超时后再连上的情况,用户需要重新实例化zk对象。


6、zk启动

首先看下zk的整体架构,如下图。

zk的启动主要分为以下几步:(集群启动多了选举这一步,所有机器之间互相投票,比较ZXID,ZXID一致比较SID,找到Leader)

  • 配置文件解析

解析zoo.cfg文件,基本参数:TickTime、dataDir、clientPort等;判断是集群模式还是单机模式(根据服务器的地址个数);创建服务器实例ZooKeeperServer实例。

  • 初始化数据管理器

数据管理器FileTxnSnapLog,上层服务器和底层数据之间的对接层,提供一系列操作数据文件的接口,包括事务日志文件快照数据文件

  • 初始化网络IO管理器

  • 数据恢复

每次zk启动时都需要从本地快照数据文件和事务日志文件中进行数据恢复。创建会话管理器SessionTracker。初始化zk请求处理链(责任链模式),一个客户端请求会经过多个请求处理器。

  • 对外服务


7、日志

zk事务日志特点:

  • 每个大小基本统一64M

ZooKeeper 的事务日志文件会采取“磁盘空间预分配”的策略。当检测到当前事务日志文件剩余空间不足4096字节(4KB)时,就会开始进行文件空间扩容。文件空间扩容的过程其实非常简单,就是在现有文件大小的基础上,将文件大小增加 65536KB(64MB),然后使用“0”(\0)填充这些被扩容的文件空间。

那么 ZooKeeper 为什么要进行事务日志文件的磁盘空间预分配呢?事务日志的写入性能直接决定了 ZooKeeper 服务器对事务请求的响应,也就是说,事务写入近似可以被看作是一个磁盘 I/O 的过程。严格地讲,文件的不断追加写入操作会触发底层磁盘 I/O 为文件开辟新的磁盘块,即磁盘Seek。因此,为了避免磁盘Seek的频率,提高磁盘I/O的效率,ZooKeeper在创建事务日志的时候就会进行文件空间“预分配”——在文件创建之初就向操作系统预分配一个很大的磁盘块,默认是64MB,而一旦已分配的文件空间不足4KB时,那么将会再次“预分配”,以避免随着每次事务的写入过程中文件大小增长带来的Seek开销,直至创建新的事务日志。(每个事务一个日志,这样事务日志文件很多啊,所以需要定时的清理这部分日志文件)

  • 文件名后缀十六进制数字(ZXID事务ID)

数据快照日志的后缀也是ZXID,标识生成快照这一时刻最新的事务ID,这是数据恢复的起点


8、数据同步

zk集群的数据同步分为四类:直接差异化同步(DIFF同步)、先回滚再差异化同步(TRUNC+DIFF同步)、仅回滚同步(TRUNC同步)、全量同步(SNAP同步)。

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

minCommittedLog:Leader服务器提案缓存队列(从内存数据库中提取出事务请求对应的提议缓存队列Proposal队列)中最小的zxid

maxCommittedLog:最大的zxid

1)DIFF 同步(peerLastZxid在min和max之间)

Leader发送DIFF指令,Learner进入差异化同步阶段,Leader将差异化的提案Proposal+Commit两条信息发送Learner。发送完后,Leader进入过半ACK等待。
2)TRUNC+DIFF同步

当Leader发现某个Learner包含一条自己都没有的事务记录(上一个epoch的领导遗留在本地的),让这个Learner先进行事务回滚,然后再差异化同步。

3)TRUNC同步(peerLastZxid>max) 让跟随者都回滚,以Leader为准,虽然Leader比较Low

4)SNAP同步(peerLastZxid<min || Leader没有提议缓存队列)

Leader将本机全量内存数据同步给Learner。


文章转载自小怂读书,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论