学习zookeeper

1.zookeeper是什么

ZooKeeper 是一个高可用的分布式数据管理系统协调框架。基于ZAB(Zookeeper Atomic Broadcast)协议实现,ZAB 协议是为ZooKeeper 专门设计的一种支持崩溃恢复的原子消息广播协议。在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性,同时其崩溃恢复过程也确保看zk集群的高可用(HA)。
对于操作来说,其实很简单,就是对zk节点的写入和删除。但是要保证数据的正确性,其内部原理远不止这么简单。

2.zookeeper要点

zk的核心任务就是解决在操作过程中数据的一致性问题,其中zk的几个关键点就是zab协议、节点属性、数据写入、watcher机制、崩溃恢复等

2.1 节点属性

zk中节点分为两种:服务器节点、znode节点。

2.1.1 服务器节点

zk对集群中的服务器节点定义了3种角色:
leader:领导者
follower:跟随者,同步leader,参与节点选举。
observer:特殊的follower,不参与节点选举,只会同步leader。设计此角色是为了当节点过多时,进行选举时能够最快选举完成,避免所有节点参与投票。

2.1.2 znode节点

上述是对服务器节点的说明,zk对数据结构以及节点也定义了如下属性:
zk的数据结构是一个树状结构,每个节点称做一个ZNode。每一个ZNode默认能够存储1M B的数据,每个ZNode都可以通过其路径唯一标识。
zk数据结构
(1)临时节点:客户端与Zookeeper断开连接后,该节点被删除,
基于zk的分布式锁,就是依赖了zk临时节点的属性实现的。
(2)临时有序节点:Zookeeper给该临时节点名称进行顺序编号
(3)持久节点:客户端与Zookeeper断开连接后,该节点依旧存在
(4)持久有序节点:Zookeeper给该持久节点名称进行顺序编号
有序节点
创建有序znode时,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护,在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序

2.2 ZAB协议

zab协议是基于Paxos算法实现,但并不是完全复制paxos算法,zab协议定义了4个阶段:选举、发现、同步、广播。

2.2.1 选举

节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。此阶段的目的是为了选出一个准 leader,只有完成第3步同步,准 leader 才会成为真正的 leader。选举操作发生在两种情况下进行,同时在选举过程中对不同程度下的服务器定义了状态:

  1. leading:当前服务器节点是 leader,负责协调事务,选举完成后为leadeing
  2. following:当前服务器节点是跟随者,服从 leader 节点的命令,选举完成后为fllowering
  3. election/looking:当前服务器节点处于选举状态
2.2.1.1 服务器启动时的leader选举

选举机制

  1. 服务器 1 启动,发起一次选举。服务器 1 投自己一票。此时服务器 1 票数一票,不够半数以上(3 票),选举无法完成,服务器 1 状态保持为 LookIng;
  2. 服务器 3 启动,再发起一次选举。服务器 1 和 3 各投自己一票,并且将信息发送给其他服务器及接受其他服务器的信息,每次投票信息会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,由于是初始状态,所以此时Server1的投票为(1, 0),Server3的投票为(3, 0),服务器都需要将别人的投票和自己的投票进行PK,此时服务器 1 发现服务器 3 的 ID 比自己目前投票推举的(服务器 1)大,更改选票为(3,0)推举服务器 3,对于Server3而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。此时服务器 1 票数 0 票,服务器3票数 2 票,没有半数以上结果,选举无法完成,服务器 1,3 状态保持 LookIng
  3. 服务器 2 启动,发起一次选举。同样的,发现id没有服务器3的大,则服务器2将选票投给服务器 3。此次投票结果:服务器 1 为 0 票,服务器 2 为 0 票,服务器 3 为 3 票。此时服务器 3 的票数已经超过半数,服务器 3 当选Leader。服务器 1,2 更改状态为 FollowIng,服务器 3 更改状态为LeadIng;
  4. 服务器 4 启动,发起一次选举。此时服务器 1,2,3 已经不是 LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器 3 为 3 票,服务器 4 为 1 票。此时服务器 4服从多数,更改选票信息为服务器 3,并更改状态为 FollowIng;
  5. 服务器 5 启动,同 4 一样当小弟。
说明:上述过程描述故意将服务器2和3的启动程序变动,目的是为了说明
选举是根据现有id顺序来进行投票的。那个节点先半数以上,那么该节点就位准Leader

2.2.1.2 服务器运行时的leader选举

在Zookeeper运行期间,即便当有非Leader服务器宕机或新加入,也不会影响Leader,但是一旦Leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮Leader选举,其过程和启动时期的Leader选举过程基本一致。
选举过程如下:

  1. 变更状态。Leader挂了之后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
  2. 每个server都投自己一票,发送和接收投票信息并PK,剩下的步骤就一样了
成为 leader 的条件:
 选epoch最大的,epoch相等,
 选 zxid 最大的,epoch和zxid都相等,
 选择server id最大的(就是我们配置zoo.cfg中的myid)
 节点在选举开始都默认投票给自己,当接收其他节点的选票时,会根据上面
 的条件更改自己的选票并重新发送选票给其他节点,当有一个节点的得票超
 过半数,该节点会设置自己的状态为 leading,其他节点会设置自己的状态
 为 following。

选举这一步只是为了选出leader,要想成为真正的leader,需要完成同步工作

2.2.2 发现

在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准 leader 生成新的 epoch,让 followers 接受,更新它们的Epoch。

在 ZAB 协议的事务编号 Zxid 设计中,Zxid 是一个 64 位的数字,其中低 
32 位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器
加 1;而高 32 位则代表 Leader 周期 epoch 的编号,每个当选产生一个新
的 Leader 服务器,就会从这个 Leader 服务器上取出其本地日志中最大事务
的ZXID,并从中读取 epoch 值,然后加 1,以此作为新的 epoch,并将
低 32 位从 0 开始计数。epoch:可以理解为当前集群所处的年代或者周期,
每个 leader 就像皇帝,都有自己的年号,所以每次改朝换代,leader 变更之后,
都会在前一个年代的基础上加 1。这样就算旧的 leader 崩溃恢复之后,
也没有人听他的了,因为 follower 只听从当前年代的 leader 的命令。

2.2.3 同步

同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。只有当半数以上节点都同步完成,准 leader 才会成为真正的 leader。follower 只会接收 zxid 比自己的 lastZxid 大的提议。

2.2.4 广播

到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。值得注意的是,ZAB 提交事务并不像 2PC 一样需要全部 follower 都 成功,只需要得到半数以上节点成功就可以了。

2.2.5 基于ZAB协议的崩溃恢复

2.2.5.1 leader崩溃
2.2.5.1.1 选举(服务器运行时的leader选举)

lead崩溃比较复杂,在leader崩溃后至重新选举的leader开始服务,这期间zk是不对外提供服务的,所以zk为了保证其高可用性,实现了一个高效且可靠的leader选举算法:FastLeaderElection算法,就是2.2.1 选举章节提到的两种选举方式。leader崩溃重新选举应用了其中之一的选举方式:服务器运行时的lead选举。

2.2.5.1.2 数据同步

当完成Leader选举后,进行故障恢复的第二步就是数据同步: Leader服务器会为每一个Follower服务器准备一个队列,并将那些没有被各个Follower服务器同步的事务以Proposal的形式逐条发给各个Follower服务器,并在每一个Proposal后都紧跟一个commit消息,表示该事务已经被提交,当follower服务器将所有尚未同步的事务proposal都从leader服务器同步过来并成功应用到本地后,leader服务器就会将该follower加入到真正可用的follower列表中。(新选举周期,epoch已经更新了)
在同步过程中,想要leader达到能够再次使用的条件,zk对消息的处理具体需要从以下两方面入手:

  1. 已经被leader提交的事务需要最终被所有的机器提交(已经发出commit了)
    场景介绍: 我们知道,当leader收到半数以上flowwer的ACK后,leader会先提交本地事务、然后广播commit提交指令并且向客户端返回成功。
    倘若leader和部分follwer上执行了commit指令,但是其他follwer还没有收到commit命令前,leader挂了,此时还没有同步完,但客户端可能已经收到成功反馈,经过恢复模式后需要保证所有机器都执行了此消息。
    在这里插入图片描述
    解决策略:为了实现已经被处理的消息不能丢这个目的,Zab 的恢复模式使用了以下的策略
    (1):选举拥有 zxid最大值的节点作为新的 leader
    (2):新的 leader 将自己事务日志中 proposal 但未 COMMIT 的消息处理
    (3):新的 leader 与 follower 建立先进先出的队列, 先将自身有而 follower 没有的消息发送给 follower并发送commit提交指令
    保证所有的follwer都处理了消息
  2. 保证丢弃那些只在leader上提出的事务。(leader已将将proposa提案广播出去,还没有收到follwer的回应,还没有进行提交)
    场景介绍:当 leader 接收到消息请求生成 proposal 后就挂了,其他 follower 并没有收到此 proposal,因此经过恢复模式重新选了 leader 后,这条消息是被跳过的。 此时,之前挂了的 leader 重新启动并注册成了 follower,他保留了被跳过消息的 proposal 状态,与整个系统的状态是不一致的,需要将其删除。
    解决策略:这是follwer的一个恢复阶段,涉及到zab事务id的一个概念,Zab 通过设计 zxid 来实现这一目的。一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 leader 选举产生一个新的 leader,新 leader 会将 epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新 leader 选举后这个值重置为 0。这样设计的好处是旧的 leader 挂了后重启,它不会被选举为 leader,因为此时它的 zxid 肯定小于当前的新 leader。当旧的 leader 作为 follower 接入新的 leader 后,新的 leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除。
2.2.5.1 follwer崩溃和加入

follwer崩溃,比较简单,直接同步leader数据即可,follower 发送它们的 lastZixd 给 leader,leader 根据 lastZixd 决定如何同步数据。

2.3 数据写入

zk集群中的节点收到写入请求,如果当前服务器不是leader,那么服务器会把写入请求进一步转发给leader,如果你想问,高并发的时候,写入请求怎么处理—这样想没错,但是你要知道leader在收到请求后,尽管怎么个高并发,在那一刻也有个先后顺序吧。
leader会将写入请求广播到各个节点,直到半数以上响应leader成功,此时写入成功。
在这里插入图片描述

  1. leader收到写请求后,将Proposal提案(就是要被写入的数据)广播给follwer。
  2. follwer记入待写日志中,响应leader ACK
  3. leader收到半数以上follwer ACK后,提交本地事务,广播commit指令,反馈客户端成功。
  4. follwer收到提交指令

2.4 watcher监听机制

zk的watcher事件,是一次性触发的,就是设置一次watcher,当节点更新被触发后,在下次更新前若没有设置watcher,是没有反应的,哪怕是连续的两次操作,也只会在第一次操作是触发,效果类似于异步获得数据的操作。
Zookeeper的Watcher机制主要包括客户端线程(实际中的体现就是应用服务器,因为你的代码就是写在应用中)、客户端WatchManager和Zookeeper服务器三部分。在具体的流程上,客户端向Zookeeper服务器注册Watcher事件监听的同时,会将Watcher对象存储在 客户端WatchManager中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象执行回调逻辑。
在这里插入图片描述
watcher监听zk有两种类型进行监听:

2.4.1 znode节点事件类型

  1. EventType.NodeCreated:节点创建,是的,你没看错,zk支持提前监听,就是目前节点还没有被创建,你就可以通过zk.exists接口或者其他api设置监听机制进行监听,当节点创建就被回调process方法。
  2. EventType.NodeDateChanged:节点值修改
  3. EventType.NodeChildrenChanged:子节点发生变化,注意,zk不支持隔代监听,比如A/B/C结构,你对B节点设置了监听,当C节点发生变化的时候,返回NodeChildrenChanged类型,虽然因为是子节点改变而触发,但是你应该清楚,这是对B节点的监听。
  4. EventType.NodeDeleted 节点删除

2.4.2 状态类型

  1. KeeperStatus.Disconnected:未连接
  2. KeeperStatus.SyncConnected:已连接,连接zk服务器,连接成功后调用process方法
  3. KeeperStatus.AuthFailed:验证失败,zk有安全认证机制,其实就是加上登录名密码进行连接
  4. KeeperStatus.Expired:已过期

当发生以上事件类型、状态类型两种类型的时候进行回调process方法,不同的是状态类型是自动触发,不需要在节点上设置监听,通过查看源码在创建连接是设置监听:
在这里插入图片描述
需要说明的是,当状态类型被触发时,所记录的事件类型都是:EventType.None

2.5 事务zxid

在上面你应该看到了好几处提到了zxid概念,这里再次单独说明:
在 ZAB 协议的事务编号 Zxid 设计中,Zxid 是一个 64 位的数字,其中低 32 位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器加 1;而高 32 位则代表 Leader 周期 epoch 的编号,每个当选产生一个新的 Leader 服务器,就会从这个 Leader 服务器上取出其本地日志中最大事务的ZXID,并从中读取 epoch 值,然后加 1,以此作为新的 epoch,并将低 32 位从 0 开始计数。epoch:可以理解为当前集群所处的年代或者周期,每个 leader 就像皇帝,都有自己的年号,所以每次改朝换代,leader 变更之后,都会在前一个年代的基础上加 1。这样就算旧的 leader 崩溃恢复之后,也没有人听他的了,因为 follower 只听从当前年代的 leader 的命令。

2.6 数据存储

Zookeeper是将全量数据存储在内存中,但他是怎样进行容错的呢?当节点崩溃后或重新初始化时,是怎么会发到宕机之前的数据呢?这就需要Zookeeper的数据存储实现。zk的数据存储机制也是采用的数据快照+事务日志实现,为什么用也呢,比如redis的rdb、aof不就是快照、日志,生产环境中就是采用两种模式配合的方式使用。

2.6.1 事务日志

配置dataLogDir目录,其用来存储事务日志文件,在运行过程中会在该目录下建立一个名字为version-2的子目录,该目录确定了当前Zookeeper使用的事务日志格式版本号,当下次某个Zookeeper版本对事务日志格式进行变更时,此目录也会变更,即在version-2子目录下会生成一系列文件大小一致(64MB)的文件。
事务日志记录了对Zookeeper的操作,命名为log.ZXID,后缀是一个事务ID,并且是写入该事务日志文件第一条事务记录的ZXID。使用ZXID作为事务日志后缀的优势可以帮助我们迅速定位到某一个事务操作所在的事务日志,以及可以清楚地看出当前运行时的zookeeper的leader周期。
事务日志的写入是采用了磁盘预分配的策略,事务写入可被看做是一个磁盘IO过程,为了提高性能,避免磁盘寻址seek所带来的性能下降,所以zk在创建事务日志的时候就会进行文件空间“预分配”,即:在文件创建之初就想操作系统预分配一个很大的磁盘块,默认是64M,而一旦已分配的文件空间不足4KB时,那么将会再次进行预分配,再申请64M空间。

2.6.1 数据快照

数据快照是Zookeeper数据存储中非常核心的运行机制,数据快照用来记录Zookeeper服务器上某一时刻的全量内存数据内容,并将其写入指定的磁盘文件中,也是使用ZXID来作为文件 后缀名,并没有采用磁盘预分配的策略
与事务文件类似,通过dataDir属性来配置文件路径,与事务日志类似,在运行过程中会在该目录下创建version-2的目录,该目录确定了当前Zookeeper使用的快照数据格式版本号。在Zookeeper运行时,会生成一系列文件。针对客户端的每一次事务操作,Zookeeper都会将他们记录到事务日志中,同时也会将数据变更应用到内存数据库中,Zookeeper在进行若干次(snapCount)事务日志记录后,将内存数据库的全量数据Dump到本地文件中,这就是数据快照。
每进行一次事务记录后,Zookeeper都会检测当前是否需要进行数据快照。理论上进行snapCount次事务操作就会开始数据快照,但是考虑到数据快照对于Zookeeper所在机器的整体性能的影响,需要避免Zookeeper集群中所有机器在同一时刻进行数据快照。因此zk采用“过半随机”的策略,来判断是否需要进行数据快照。即:符合如下条件就可进行数据快照:
logCount > (snapCount / 2 + randRoll) randRoll位1~snapCount / 2之间的随机数。这种策略避免了zk集群的所有机器在同一时刻都进行数据快照,影响整体性能。
进行快照:开始快照时,首先关闭当前日志文件(已经到了该快照的数了),重新创建一个新的日志文件,创建单独的异步线程来进行数据快照以避免影响Zookeeper主流程,从内存中获取zookeeper的全量数据和校验信息,并序列化写入到本地磁盘文件中,以本次写入的第一个事务ZXID作为后缀。

2.6.1 数据恢复

在Zookeeper服务器启动期间,首先会进行数据初始化工作,用于将存储在磁盘上的数据文件加载到Zookeeper服务器内存中。
数据恢复时,会加载最近100个快照文件(如果没有100个,就加载全部的快照文件),之所以要加载100个,是因为防止最近的那个快照文件不能通过校验。在逐个解析过程中,如果正确性校验通过之后,那么通常就只会解析最新的那个快照文件,但是如果校验和发现最先的那个快照文件不可用,那么就会逐个进行解析,直到将这100个文件全部解析完。如果将所有的快照文件都解析后还是无法恢复出一个完整的“DataTree”和“sessionWithTimeouts”,则认为无法从磁盘中加载数据,服务器启动失败。当基于快照文件构建了一个完整的DataTree实例和sessionWithTimeouts集合了,此时根据这个快照文件的文件名就可以解析出最新的ZXID,该ZXID代表了zookeeper开始进行数据快照的时刻,然后利用此ZXID定位到具体事务文件从哪一个开始,然后执行事务日志对应的事务,恢复到最新的状态,并得到最新的ZXID。

3.zookeeper能做什么

请点击

4.zookeeper怎么玩

zk有多个客户端框架,最常用的就是zkclient和curator框架。

5.参考链接

https://juejin.im/post/5b924b0de51d450e9a2de615
https://blog.csdn.net/u013679744/article/details/79230418
https://blog.csdn.net/u013679744/article/details/79240249

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值