4.1、概述

Zookeeper 是一个分布式协调服务,可用于服务发现,分布式锁,分布式领导选举,配置管理等。Zookeeper 提供了一个类似于 Linux 文件系统的树形结构(可认为是轻量级的内存文件系统,但只适合存少量信息,完全不适合存储大量文件或者大文件),同时提供了对于每个节点的监控与通知机制。
构建一个分布式应用是一个很复杂的事情,主要的原因是我们需要合理有效的处理分布式集群中的部分失败的问题。例如,集群中的节点在相互通信时,A节点向B节点发送消息。A节点如果想知道消息是否发送成功,只能由B节点告诉A节点。那么如果B节点关机或者由于其他的原因脱离集群网络,问题就出现了。A节点不断的向B发送消息,并且无法获得B的响应。B也没有办法通知A节点已经离线或者关机。集群中其他的节点完全不知道B发生了什么情况,还在不断的向B发送消息。这时,你的整个集群就发生了部分失败的故障。
Zookeeper不能让部分失败的问题彻底消失,但是它提供了一些工具能够让你的分布式应用安全合理的处理部分失败的问题,从设计模式角度理解,zookeeper是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理数据,然后接收观察者的注册,一旦这些数据发生变化,zookeeper负责通知已经在zookeeper上注册的那些观察者,然后再做出相应的处理

4.2、设计目标

4.2.1、简单的数据模型

ZooKeeper 允许分布式进程通过共享的分层命名空间相互协调,该命名空间的组织类似于标准文件系统,命名空间由数据寄存器组成——在 ZooKeeper 中称为 znodes——这些类似于文件和目录。与为存储而设计的典型文件系统不同,ZooKeeper 数据保存在内存中,这意味着 ZooKeeper 可以实现高吞吐量和低延迟数量
四-Zookeeper - 图1

4.2.2、高性能

它在 “以读为主 “的工作负载中特别快。ZooKeeper应用程序在数以千计的机器上运行,它在读比写更常见的情况下表现最好,比例约为10:1。因此zk不适合使用多写少读的场景。

4.2.3、集群模式

就像它协调的分布式进程一样,zk本身旨在通过一组称为集合的主机进行复制,这些主机之间必须相互了解,它们会将内存中的状态进行持久化存储以及事务日志,因此只要大多数服务器可用,那么zk服务就能正常服务。当客户端连接到其中一个 ZooKeeper 服务器时,客户端会维护一个 TCP 连接,通过它发送请求、获取响应、获取监视事件和发送心跳。如果发现到服务器的 TCP 连接中断,那么客户端就会连接到其他的服务器上
四-Zookeeper - 图2

4.2.4、顺序访问

ZooKeeper在每次更新时都会标上一个数字,反映出所有ZooKeeper事务的顺序。随后的操作可以使用这个顺序来实现更高层次的抽象,如同步原语

4.3、角色介绍

四-Zookeeper - 图3
如上图所示,客户端向Zk集群发起请求,会先经过请求协调器进行处理,然后以原子语义进行处理,并将其中涉及到的状态和操作落入到内存数据库中,该数据库是一个包含整个数据树的内存数据库。同时更新会被记录到磁盘,以便于故障恢复,而写入的内容在应用到内存数据库之前被序列化到磁盘。
每个 ZooKeeper 服务器都会为客户端提供服务。客户端只需要连接到一台服务器来提交请求,读取请求由每个服务器数据库的本地副本提供服务。 改变服务状态的请求即写请求,由Leader来处理。
作为协议的一部分,所有来自客户端的写请求都被转发到一个服务器,称为领导者Leader。其余的ZooKeeper服务器,称为追随者Follower,从领导者那里接收消息建议并同意开始消息传递。
信息传递层负责更换失败的领导者,并将追随者与领导者之间的信息进行同步更新。
ZooKeeper使用一个自定义的原子信息传递协议。由于消息传递层是原子性的,所以ZooKeeper可以保证本地副本永远不会出现分歧。当领导者收到一个写请求时,它会计算出要写的时候系统的状态是什么,并将其转化为捕捉这个新状态的事务。

4.3.1、Leader

1、Leader为客户端提供读写服务,其他角色提供读服务。
2、所有的写操作必须要通过Leader完成再由Leader将写操作广播给其他服务器,只要超过半数节点(不包括Observer)节点写入成功,该请求就会被提交(类2PC协议);
3、一个集群中同一时间只能有一个实际工作的Leader,它会发起并维护与各Follower以及Observer之间的心跳

4.3.2、Follower

1、Follower提供读服务,可以直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理;
2、负责在Leader处理写请求时对请求进行投票;
3、集群中可能同时存在多个Follower,它会响应Leader的心跳;

4.3.3、Observer

Zookeeper 需保证高可用和强一致性,为了支持更多的客户端,需要增加更多 Server;Server 增多,投票阶段延迟增大,影响性能 ;因此出于该目的,增加了Observer的角色,该角色具有以下几个职责:
1、角色与 Follower 类似,但是无投票权。
2、引入 Observer,Observer 不参与投票;
3、Observers 接受客户端的连接,并将写请求转发给 leader 节点
4、加入更多 Observer 节点,提高伸缩性,同时不影响吞吐率

4.4、安装部署

4.4.1、单机部署

4.4.1.1、下载包

http://zookeeper.apache.org/releases.html

4.4.1.2、解压缩、配置环境变量

  1. tar -zxvf zookeeper-x.y.z.tar.gz
  2. % export ZOOKEEPER_HOME=~/sw/zookeeper-x.y.z
  3. % export PATH=$PATH:$ZOOKEEPER_HOME/bin

4.4.1.3、配置调整

运行ZooKeeper之前我们需要编写配置文件。配置文件一般在安装目录下的conf/zoo.cfg。我们可以把这个文件放在/etc/zookeeper下,或者放到其他目录下,并在环境变量设置ZOOCFGDIR指向这个个目录。下面是配置文件的内容:

  1. vi zoo.cfg
  2. tickTime=2000
  3. dataDir=/Users/tom/zookeeper
  4. clientPort=2181

tickTime是zookeeper中的基本时间单元,单位是毫秒。datadir是zookeeper持久化数据存放的目录。clientPort是zookeeper监听客户端连接的端口,默认是2181.

4.4.1.4、服务启动

  1. zkServer.sh start

4.4.2、分布式部署

4.4.2.1、解压缩并分发

  1. tar -zxvf zookeeper-x.y.z.tar.gz

4.4.2.2、修改配置文件

  1. vi zoo.cfg
  2. # 添加如下配置:
  3. server.2=hadoop102:2888:3888
  4. server.3=hadoop103:2888:3888
  5. server.4=hadoop104:2888:3888
  6. #配置解读
  7. server.A=B:C:D
  8. A:是一个数字,表示是第几号服务器
  9. B:服务器地址
  10. C:这个服务器Follower与集群中的Leader服务器交换信息的端口
  11. D:进行执行选举Leader互相通信的端口

4.4.2.3、配置服务器编号

  1. # 在/Users/tom/zookeeper目录下,添加myid文件,其他机器做同样的配置
  2. cd /Users/tom/zookeeper
  3. vi myid
  4. # 添加与Server对应的编号
  5. 2

4.4.2.4、服务启动

  1. #启动服务
  2. zkServer.sh start
  3. #查看状态
  4. zkServer.sh status

4.5、命令使用

基本语法 功能描述
ls path [watch] 使用ls命令查看当前znode中所包含的内容
ls2 path [watch] 查看当前节点数据并能看到更新次数等数据
create 普通创建
-s 含有序列 create -s /zk “myData”
-e 临时(重启或者超时就会丢失) create -e /zk “myData”
get path [watch] 获得节点的值
set 设置节点的具体值
stat 查看节点状态信息
delete 删除节点
rmr 递归删除节点,不管有多少子znode节点,统统删除
zkCli.sh -server host:2181 连接服务其器

4.6、源码编译

4.6.1、下载源码包

git clone clone -b branch-3.5.5 https://github.com/apache/zookeeper.git

4.6.2、下载Ant

1.下载Ant

https://downloads.apache.org/ant/binaries/apache-ant-1.10.8-bin.zip

2.配置环境变量

2.1 新增用户变量ANT_HOME ,值为安装目录
# 2.2 将bin目录增加到系统变量Path中

3.测试

ant -version
四-Zookeeper - 图4四-Zookeeper - 图5

4.6.3、编译

4.6.3.1、开始编译

进入zookeeper下载目录,启动cmd,输入编译命令
ant eclipse
#等待编译成功
问题:编译时可能会 下载ant-eclipse-1.0.bin.tar.bz2失败
四-Zookeeper - 图6
解决方案:
将bulild.xml中的
get src=”http://downloads.sourceforge.net/project/ant-eclipse/ant-eclipse/1.0/ant-eclipse-1.0.bin.tar.bz2“%22” \t “_blank)
替换成如下地址
get src=”http://ufpr.dl.sourceforge.net/project/ant-eclipse/ant-eclipse/1.0/ant-eclipse-1.0.bin.tar.bz2“%22” \t “_blank)
四-Zookeeper - 图7

4.6.3.2、 编译成功

四-Zookeeper - 图8
将编译好的代码导入Idea即可。

4.6.3.3、服务启动

4.6.3.3.1、编辑zoo.cfg文件

将conf/zoo_sample.cfg修改为zoo.cfg,并增加dataDir和dataDirLog参数配置
四-Zookeeper - 图9

4.6.3.3.2、启动org.apache.zookeeper.server.ZooKeeperServerMain
注意:这里启动的是单机版的服务端,从org.apache.zookeeper.server.quorum.QuorumPeerMain入口启动也是一样的

四-Zookeeper - 图10
注意:
1.记得要配置下log4j配置文件,否则会抛出Usage: ZooKeeperServerMain configfile | port datadir [ticktime] [maxcnxns]错误
-Dlog4j.configuration=file:D:\GitCode\zookeeper\zookeeper-release-3.5.5\zookeeper-server\src\test\resources\log4j.properties
2.指定zoo.cfg文件路径,否则会抛出java.lang.IllegalArgumentException: Invalid number of arguments:[]
四-Zookeeper - 图11

4.7、一致性算法

4.7.1、背景

20世纪60年代大型主机被发明出来以后,集中式的计算机系统架构成为了主流,其单机处理能力方面的优势非常明显,但从20世纪80年代之后,传统的集中式处理模式越来越不能满足人们的需求,同时集中式系统有明显的单点故障问题,从2008年开始,阿里开启了“去IOE”计划,后来逐渐的往分布式系统的方向发展。分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。因此分布式系统具有以下几个特点:
1.分布性:多台计算机在空间上随意分布,同时分布情况也会随时变动
2.对等性:集群中的每个节点角色都是一样的,注意副本的概念
3.并发性:多个机器可能会同时操作一个数据库或存储系统,可能会引起数据不一致问题
4.缺乏全局时钟:分布式系统中的多个主机事件先后顺序很难界定
5.故障总发生:服务器宕机,网络拥堵和延迟
同时和分布式系统进行对比发现集中式系统具有以下几个特点:

  • 部署结构简单
  • 成本高
  • 单点故障
  • 大型主机的性能扩展受限于摩尔定律

注意:这里要区分集群和分布式的概念,集群是指大量的机器做同一件事情;分布式是指每台机器都有不同的角色,做不同的事情

4.7.2、分布式异常问题

分布式系统体系结构从出现到现在仍有很多的问题,这里列出一些典型的问题
1.通信异常:从集中式向分布式演变,必然会引入网络因素,由于网络的不可靠性,必然会在分布式节点之间出现网络 异常的情况
2.网络分区:网络分区也就是常说的脑裂,即出现多个局部小集群,每个局部网络可以互相通信
3.三态:三态指的是三种状态,即成功、失败、超时;在集中式系统中只有成功和失败,而超时是由于分布式系统中会出现网络异常才会有的状态
4.节点故障:分布式节点总会出现宕机或者僵死现象
5.数据丢失:对于有状态的节点,数据丢失意味着状态丢失,通常只能从其他节点读取,恢复存储的状态;通常针对这种问题,通过引入副本机制就可以解决

4.7.3、衡量分布式系统的性能

  • 性能:即系统吞吐能力、响应延迟、QPS等
  • 可用性
  • 可扩展性
  • 一致性

    4.7.4、一致性模型

    分布式环境下,一致性指的是数据在多个副本之间是否能够保持一致的特性,对于一个将数据副本分布在不同节点的系统上来说,如果对一个节点的数据进行了更新操作,却没有使得第二个节点上的数据得到响应的更新,那么此时在读取第二个节点的数据时,将会出现脏读现象(即数据不一致).那么一致性又分为以下几种:
    1、强一致性
    即写操作完成后,读操作一定能够读到最新的数据,在分布式场景下,很难实现;Paxos、Quorum机制、ZAB协议能够实现,后面会对这几种协议算法进行讲解。
    2、弱一致性
    不承诺立即可以读到写入的值,也不承诺多久后能达到数据一致,但会尽可能保证某个时间级别后,数据达到一致状态
    3、读写一致性
    用户读取自己写入结果的一致性,保证用户能够第一时间看到自己更新的内容;这种实现的解决方案有:一种方案是对于特定的内容,我们去主库查询
    设置一个更新时间窗口,在刚更新的一段时间内,默认去主库读取,过了这个窗口后,挑选最近更新的从库进行读取
    直接记录用户更新的时间戳,在请求的时候把这个时间戳带上,凡是最后更新时间小于这个时间戳的从库都不响应
    4、单调读一致性
    本地读到的数据不比上次读到的旧,多次刷新返回旧数据会出现灵异事件,对于这种情况可以通过hash映射到同一台机器上
    5、因果一致性
    如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改基于A更新的值。此时,和节点A无因果关系的节点C的数据访问则没有这样的限制
    6、最终一致性
    所有分布式一致性模型中最弱的。不考虑中间的任何状态,只保证经过一段时间之后,最终系统内数据正确,最大程度上保证了系统的并发能力,因此在高并发场景下,也是使用最广的一致性模型
    4.7.4.1、事务
    事务是可以保证一致性的方法,在集中式系统架构中可以通过ACID的方式实现,在早期分布式架构,是通过CAP/BASE理论来实现的,后来又演化出了2pc/3pc,以及Paxos、Raft等算法来保证一致性。
    事务是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。(概念性的东西直接略过~)

  • ACID

  • Atomicity原子性简单说就是一个操作序列要么全部成功执行,要么全部不执行
  • Consistency一致性也就是说数据从一个一致性状态转变到另一个一致性状态,中间不能存在不一致的状态
  • Isolation隔离性在并发环境下,一个事务的执行不能被其他事务所干扰,每个事务都有各自独立完整的数据空间,也就是说一个事务内部的操作以及数据的使用对其他并发的事务都是隔离的。根据隔离性的强度分为4个级别:
    • 未授权读取(读未提交)这是隔离级别最低的。比如说事务A和事务B同时进行,事务A在整个执行的过程中,会将某个数据值从1开始做加法操作直到变成10之后提交事务,而此时事务B能够看到事务A操作这个数据的所有变化(即从1到10的变化),而这一系列中间值的读取就是未授权读取
    • 授权读取(读已提交)只允许读取已经被提交的数据而且不可重复读。比如说事务A和事务B同时进行,同样进行加法操作(从1到10),那么此时事务B是无法看到所有的中间值,只能看到最终的10.
    • 可重复读取在保证事务处理的过程中,多次读取同一个数据时候,它的值和事务开始时刻是一致的,可能会出现幻影数据(即同一个事务操作,在前后两个时间段内执行对同一个数据项进行读取,可能会得到不同的结果),还是拿上面的例子,事务B在第一次事务读取的时候,始终读到的是1,但是到第二次事务读取的时候就会变成了10(因为这个时候事务A已经完成了)
    • 串行化即所有的事务串行执行
  • Durability持久性
  • 即一个事务一旦提交,对应数据的状态变更就是永久性的

4.7.4.2、CAP

  • Consistency一致性数据在多个副本之间是否能够保持一致的特性。
  • Avaliabilty可用性指系统提供的服务必须一直处于可用的状态,对于用户的操作总能给在有限的时间内返回结果,这里要注意下是在有限的时间内哦
  • Partition tolerance分区容错性即分布式系统在遇到网络分区的时候,仍然能够保证对外提供满足一致性和可用性的服务,除非整个网络发生了故障注意:分布式系统无法满足上面三个特性,但是一定要有分布容错性,即P,C和A根据需求进行衡量

4.7.4.3、BASE

  • Basically Avaliable 基本可用即分布式系统中在出现故障的时候,允许损失部分可用性,比如响应时间上的损失或者功能上的损失,例如双11的时候,可以将一些无关紧要的服务进行降级
  • Soft state软状态即允许系统中的数据存在中间状态,且中间状态的存在不会影响系统的整个可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程出现延时
  • Eventually consistent最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终达到一个一致的状态

    4.7.5、分布式一致性算法

    4.7.5.1、2pc

    即二阶段提交,绝大部分的关系型数据库都是采用二阶段提交协议来完成分布式事务处理。即将事务的提交过程分为了两个阶段来处理
    四-Zookeeper - 图12

  • 阶段一:请求/表决阶段

    • 1.事务询问协调者向参与者发起事务内容,询问是否可以执行事务操作,并等待响应
    • 2.执行事务各参与者执行事务操作,并将undo和redo信息记录到事务日志中
    • 3.各参与者向协调者反馈事务询问的响应协调者根据所有的参与者是否都响应了yes或者no来进行表决事务是否执行或者不执行
  • 阶段二:提交/执行/回滚阶段
    • 情况1.执行事务提交
    • 1.发起提交请求协调者向参与者发起commit请求
    • 2.事务提交参与者收到commit请求后,正式执行事务提交,完成之后释放资源
    • 3.反馈事务提交结果参与者完成事务之后,向协调者发送ack消息
    • 4.完成事务协调者收到所有参与者返回的ack消息,完成事务
    • 情况2.中断事务
    • 1.发送回滚请求协调者向参与者发起rollback请求
    • 2.事务回滚参与者收到rollback请求后,利用undo信息执行事务回滚操作,完成之后释放资源
    • 3.反馈事务回滚结果参与者完成事务之后,向协调者发送ack
    • 4.中断事务协调者接收到所有参与者反馈的ack,完成事务中断
  • 二阶段提交的问题

    • 1.性能问题从流程上看,在执行过程中节点都处于阻塞状态。各个操作数据库的节点都占用数据库资源,只有当所有节点准备完毕后,事务协调者才会通知进行全局commit/rollback,参与者进行本地事务commit/rollback之后才会释放资源,对性能影响较大
    • 2.单点故障问题事务协调者是整个分布式事务的核心,一旦事务协调者出现故障,会导致参与者收不到commit/rollback的通知,从而导致参与者节点一直处于事务无法完成的中间状态
    • 3.数据不一致在第二阶段的时候,如果发生局部网络问题,一部分事务参与者收不到commit/rollback消息,就会导致节点间数据不一致
    • 4.太多保守必须 收到所有参与的正反馈才提交事务如果有任意一个事务参与者的响应没有收到,则整个事务失败回滚

      4.7.5.2、3pc

      基于2pc出现的一些问题,3pc进行了改进,也就是三阶段提交,将2pc的第二个阶段进行了一分为二,形成了CanCommit(提交询问)、Precommit(预提交)、doCommit阶段(最终提交)三个阶段;其实3pc和2pc的不同点在于3pc增加了超时机制。
      四-Zookeeper - 图13
  • 阶段1:CanCommit(提交询问)协调者向所有参与者发送一个请求,询问是否可以执行事务提交操作,然后开始等待所有响应者的响应正常情况下,所有参与者会反馈yes,然后进入预备状态,否则反馈No

    • 2.各参与者向协调者反馈事务询问的响应
    • 1.事务询问
  • 阶段2:PreCommit(预提交)
    • 情况1:执行事务预提交(即所有参与者都响应yes)
    • 1.发起预提交请求协调者向所有参与者发出preCommit请求,并进入准备阶段
    • 2.事务预提交参与者接收到preCommit请求后,执行事务操作,然后将undo和redo信息记录到事务日志中
    • 3.各参与者向协调者反馈事务执行的响应
    • 情况2:中断事务(任何一个参与者反馈no或者超时就会中断事务)
    • 1.发送中断请求
    • 2.中断事务
  • 阶段3:doCommit(最终提交)
    • 情况1:执行提交
    • 1.发送提交请求
    • 2.事务提交
    • 3.反馈事务提交结果
    • 4.完成事务
    • 情况2:中断事务
    • 1.发送中断请求
    • 2.事务回滚
    • 3.反馈事务回滚结果
    • 4.中断事务
  • 3pc特点

    • 1.降低了参与者阻塞范围,增加了超时自动处理的机制
    • 2.能够在出现单点故障后继续保持一致
    • 3.网络分区会出现不一致的问题即参与者接收到了preCommit消息后,出现了网络分区,即协调者和参与者无法进行正常通信,这个时候该参与者依然会进行事务的提交,就会出现数据不一致的情况

      4.7.5.3、Paxos

      Paxos算法要解决的问题就是如何保证数据一致性,这是一种基于消息传递且具有高度容错特的一致性算法
      4.7.5.3.1、首先要引入拜占庭问题
      拜占庭问题:即不同军队的将军之间必须制定一个统一的行动计划,但是在地理上都是分隔开来的,只能依靠通讯员进行通信,但是通讯员可能会存在叛徒,对消息进行篡改,从而欺骗将军。
      这种消息篡改的情况在同一个局域网内几乎不会出现,或者简单通过校验算法进行避免。但是在实际工程实践中,可以假设不存在拜占庭问题,基于这种假设如何保证一致性呢?这个时候又引入Paxos的故事
      4.7.5.3.2、Paxos故事
      古希腊有一个Paxos小岛,岛上采用会议的形式来通过法令,议会上的议员通过信使来传递消息,注意信使和议员都是兼职的,有可能随时会离开议会,而且信使有可能会重复传递消息,也有可能一去不返。那么在这种情况下议会协议也要保证法令能够正确选举出来,而且不会产生冲突。
      根据这个故事也就衍生出了Paxos算法,该算法有3个角色:
      1.Proposer(提议者,用来发起提案的,相当于zk中的leader角色)
      2.Acceptor(接受者,可以接受或拒绝提案,相当于zk中的follower角色)
      3.Learner(学习者,学习被选定的提案,相当于zk中的observer角色)
      注意这里讲解的是Basic Paxos,基于Baisc Paxos演化出了Multi Paxos,这里不做过多讲解,有兴趣的同学可自行查阅
      大致流程就是首先选举出一个Leader,也就是Proposer用来发起提案,然后发送给所有的Acceptor来进行投票,当超过一半投通过票的时候,该提案也就通过了,那么这个时候Proposer会将该提案进行同步所有机器进行学习
      也就是说Paxos是基于议会制,以少数服从多数的核心思想来保证一致性的

      4.7.5.4、Raft

      该算法不做过多讲解,想要了解,请查看http://thesecretlivesofdata.com/raft/网址
      Raft算法是一个分布式共识算法,有三个角色
      1.Leader
      2.follower:如果follower接收不到leader的心跳时,会变为candidate(这里会有150s~300s的等待时间),发起投票是否成为新的leader
      3.candidate(候选人)
      核心思想:少数服从多数!

      4.7.5.5、ZAB协议

      zookeeper是基于该协议(zookeeper原子广播协议)实现的。这里只做简单介绍,该协议比较重要,后面会和zookeeper相关文章结合单独进行讲解!
      该协议有两种模式:
      1.崩溃恢复
      2.消息广播
      它和Paxos的区别联系在于:
      1.都存在类似于Leader角色,负责协调多个Follower进程的运行
      2.Leader进程都会等待超过半数的Follower做出正确反馈后,才会将一个提案进行提交
      3.zab协议中,每个proposal都包含一个epoch值,用了代表当前Leader周期;Paxos中也存在同样的标识,名字为Ballot
      4.zab协议主要用于构建一个高可用的分布式数据主备系统
      5.Paxos算法主要用于构建一个分布式的一致性状态机系统

      4.8、数据模型

      4.8.1、Znode

      ZooKeeper包含一个树形的数据模型,我们叫做znode,类似于Linux文件系统,但不同的是Linux文件系统有目录和文件的区别,而zk统一叫做znode。而且一个znode中包含了存储的数据和ACL(Access Control List),一个znode节点下可能会包含子znode。
      我们可以通过path来定位znode,就像Unix系统定位文件一样,使用斜杠来表示路径。但是,znode的路径只能使用绝对路径,而不能想Unix系统一样使用相对路径,即Zookeeper不能识别../和./这样的路径。
      节点的名称是由Unicode字符组成的,除了zookeeper这个字符串,我们可以任意命名节点。为什么不能使用zookeeper命名节点呢?因为ZooKeeper已经默认使用zookeeper来命名了一个根节点,用来存储一些管理数据。
      ZooKeeper的设计适合存储少量的数据,并不适合存储大量数据,所以znode的存储限制最大不超过1M。

      4.8.1.1、Znode类型

      四-Zookeeper - 图14
      4.8.1.1.1、按照生命周期划分
      znode按照生命周期来区别话,分为ephemeral和persistent两种类型。分别介绍下这两种类型:
  • ephemeral:该类型的znode是随着会话结束而自动删除的,数据不会永久保留;需要注意的是该类型的节点是不能有子节点的,只能成为叶子节点

  • persistent:默认的存储类型,该类型的znode数据存储不会随着会话断开而删除,除非程序人为删除。

    4.8.1.1.2、按照是否自带序列编号划分

    默认情况下,在创建znode节点的时候是不带序号的,也就是非sequential类型,但是如果客户端重复发起同一个znode节点创建请求时,如果我们使用排序标志的话,Zk会在我们指定的znode名字后面增加一个数字,对于这种加数字的类型称之为sequential类型。我们继续加入相同名字的znode时,这个数字会不断增加。这个序号的计数器是由这些排序znode的父节点来维护的,该顺序号是可以被用于为所有的事件进行全局排序,这样的话客户端就可以通过顺序号来推断事件发生的顺序。
    示例:客户端发起znode创建请求,指定名称为/a/d-,那么zk就会为我们创建一个名字为a/d-1的znode,如果我们再次发起/a/d-的创建请求,那么zk就会为我们创建一个/a/b-2的znode,这样zk给我们生成的序号是不断增长的。
    根据上述的两种分类,可以进行随意组合,即得到了如下几种具体类型:

  • PERSISTENT: 一旦创建这个znode节点,存储的数据不会主动丢失,除非是客户端主动delete

  • PERSISTENT_SEQUENTIAL:任意一个client去创建znode都是保证得到的znode编号是递增的,而且是唯一的znode节点且不会主动丢失的
    EPHEMERAL:临时znode节点,Client连接到zk service的时候会建立一个session,之后用这个zk连接实例在该session期间创建该类型的node,一旦client关闭了连接,服务器就会清除session,然后这个session建立的znode节点都会从命名空间消息。
  • EPHEMERAL_SEQUENTIAL:临时自动编号节点,znode节点编号会自动增加,但是会随session消失而消失

    4.8.1.2、Znode属性

    | | 字段信息 | 描述 | | —- | —- | —- | | 节点状态信息 | czxid | 该节点被创建时的事务ID | | | mzxid | 该节点最后一次更新时的事务ID | | | ctime | 该节点被创建的时间 | | | mtime | 该节点被修改的时间 | | | version | 数据版本 | | | cversion | 子节点的数据版本 | | | aversion | 节点的acl控制 | | | ephemeralOwner | 创建该临时节点的会话sessionId,如果创建的是持久节点,那么这个属性值为0 | | | dataLength | 数据内容长度 | | | numChildren | 子节点个数 | | | pzxid | 该节点的子节点列表最后一次被修改时的事务ID,只有子节点列表变更时才会变更pzxid | | 版本
    (通常用于锁控制) | version | 当前节点数据内容的版本号 | | | cversion | 当前节点子节点的版本号 | | | aversion | 当前节点ACL变更的版本号 |

4.8.2、Zxid

在zab(Zookeeper Automic Broadcast,Zookeeper原子广播协议)协议的事务编号Zxid设计中,Zxid是一个64位的数字,类似于关系型数据库中的事务ID,用于标识一次更新操作的Proposal(提议)ID,为了保证顺序性,该zkid必须单调递增。

  • 其中低32位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器加1
  • 而高32位则代表Leader周期epoch的编号,每个当选产生一个新的Leader服务器,就会从这个Leader服务器上取出其本地日志中最大事务的ZXID,并从中读取epoch值,然后加1,以此作为新的epoch,并将低32位从0开始计数

    4.9、服务端启动源码追踪

    4.9.1、服务端启动流程图

    四-Zookeeper - 图15

    4.9.2、整体代码解读

    Zookeeper启动类是QuorumPeerMain ,其参数是配置文件zoo.cfg
    四-Zookeeper - 图16

    4.9.2.1、参数解析

    QuorumPeerConfig config = new QuorumPeerConfig();

    4.9.2.2、创建快照日志截断定时调度器

    这里启动DatadirCleanupManager线程,zk的任何一个变更操作都会记录到transaction log中,由于内存数据易丢失,所以必须要刷写到磁盘上。当写操作达到一定量或者一定时间间隔后,会进行一次刷写,其主要目的是为了缩短启动时加载数据的时间从而加快系统的启动,另一方面是为了避免transaction log日志数量过大,所以要定期清理。
    1. /**
    2. * 2.创建快照日志截断定时调度器
    3. * snapRetainCount:清理后保留的snapshot的个数,对应配置:autopurge.snapRetainCount,大于等于3,默认3
    4. * purgeInterval:清理任务TimeTask执行周期,即几个小时清理一次,对应配置:autopurge.purgeInterval,单位:小时
    5. */
    6. DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config.getDataDir(), config.getDataLogDir(), config.getSnapRetainCount(), config.getPurgeInterval());
    7. purgeMgr.start();

    4.9.2.3、判断启动模式,启动单机版服务或者分布式集群模式服务

    1. /**
    2. * 3.判断启动模式
    3. */
    4. if (args.length == 1 && config.isDistributed()) {
    5. /**集群模式*/
    6. runFromConfig(config);
    7. } else {
    8. /**单机模式*/
    9. ZooKeeperServerMain.main(args);
    10. }

    4.9.3、集群模式启动服务(runFromConfig方法)

    4.9.3.1、注册JMX log4j 控制器

    ManagedUtil.registerLog4jMBeans();

    4.9.3.2、实例化两种ServerCnxnFactory(带有SSL和无SSL)

    ServerCnxnFactory从名字就可以看出其是一个工厂类,负责管理ServerCnxn,ServerCnxn这个类代表了一个客户端与一个server的连接,每个客户端连接过来都会被封装成一个ServerCnxn实例用来维护了服务器与客户端之间的Socket通道。首先要有监听端口,客户端连接才能过来,ServerCnxnFactory.configure()方法的核心就是启动监听端口供客户端连接进来,端口号由配置文件中clientPort属性进行配置,默认是2181 。
    1. /**
    2. * 2.未开启SSL的ServerCnxnFactory
    3. */
    4. if (config.getClientPortAddress() != null) {
    5. cnxnFactory = ServerCnxnFactory.createFactory();
    6. cnxnFactory.configure(config.getClientPortAddress(),
    7. config.getMaxClientCnxns(),
    8. false);
    9. }
    10. /**
    11. * 2.开启SSL的ServerCnxnFactory
    12. */
    13. if (config.getSecureClientPortAddress() != null) {
    14. secureCnxnFactory = ServerCnxnFactory.createFactory();
    15. secureCnxnFactory.configure(config.getSecureClientPortAddress(),
    16. config.getMaxClientCnxns(),
    17. true);
    18. }

    4.9.3.3、创建QuorumPeer并初始化

    Quorum在Zookeeper中代表集群中大多数节点的意思,即一半以上节点,Peer是端、节点的意思,Zookeeper集群中一半以上的节点其实就可以代表整个集群的状态,QuorumPeer就是管理维护的整个集群的一个核心类,这一步主要是创建一个QuorumPeer实例,并进行各种初始化工作 。
    1. quorumPeer = getQuorumPeer(); //创建实例
    2. quorumPeer.setTxnFactory(new FileTxnSnapLog(
    3. config.getDataLogDir(),
    4. config.getDataDir()));
    5. quorumPeer.enableLocalSessions(config.areLocalSessionsEnabled());
    6. quorumPeer.enableLocalSessionsUpgrading(
    7. config.isLocalSessionsUpgradingEnabled());
    8. //quorumPeer.setQuorumPeers(config.getAllMembers());
    9. quorumPeer.setElectionType(config.getElectionAlg());;//选举类型,用于确定选举算法
    10. quorumPeer.setMyid(config.getServerId());
    11. quorumPeer.setTickTime(config.getTickTime());
    12. quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
    13. quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
    14. quorumPeer.setInitLimit(config.getInitLimit());
    15. quorumPeer.setSyncLimit(config.getSyncLimit());
    16. quorumPeer.setConfigFileName(config.getConfigFilename());
    17. quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
    18. quorumPeer.setQuorumVerifier(config.getQuorumVerifier(), false);
    19. if (config.getLastSeenQuorumVerifier() != null) {
    20. quorumPeer.setLastSeenQuorumVerifier(config.getLastSeenQuorumVerifier(), false);
    21. }
    22. quorumPeer.initConfigInZKDatabase();
    23. quorumPeer.setCnxnFactory(cnxnFactory); //ServerCnxnFactory客户端请求管理工厂类
    24. quorumPeer.setSecureCnxnFactory(secureCnxnFactory);
    25. quorumPeer.setSslQuorum(config.isSslQuorum());
    26. quorumPeer.setUsePortUnification(config.shouldUsePortUnification());
    27. quorumPeer.setLearnerType(config.getPeerType());
    28. quorumPeer.setSyncEnabled(config.getSyncEnabled());
    29. quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
    30. if (config.sslQuorumReloadCertFiles) {
    31. quorumPeer.getX509Util().enableCertFileReloading();
    32. }
    33. // sets quorum sasl authentication configurations
    34. quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);
    35. if (quorumPeer.isQuorumSaslAuthEnabled()) {
    36. quorumPeer.setQuorumServerSaslRequired(config.quorumServerRequireSasl);
    37. quorumPeer.setQuorumLearnerSaslRequired(config.quorumLearnerRequireSasl);
    38. quorumPeer.setQuorumServicePrincipal(config.quorumServicePrincipal);
    39. quorumPeer.setQuorumServerLoginContext(config.quorumServerLoginContext);
    40. quorumPeer.setQuorumLearnerLoginContext(config.quorumLearnerLoginContext);
    41. }
    42. quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
    43. quorumPeer.initialize();

    4.9.3.4、启动(QuorumPeer中的start方法)

    四-Zookeeper - 图17
    4.9.3.4.1、加载快照、事务文件到内存生成DataTree模型
    涉及到的核心类是ZKDatabase,并借助于FileTxnSnapLog工具类将snap和transaction log反序列化到内存中,最终构建出内存数据结构DataTree ,其核心就是从文件流中读取数据,转换成DataTree对象,放入zkDb中。
    先了解ZKDatabase、DataTree、DataNode三者之间的关系:
    四-Zookeeper - 图18
    通过QuorumPeer.loadDataBase方法入口开始恢复加载数据到内存,然后调用zkDb.loadDatabase方法,底层调用snapLog.restore方法,然后最底层调用FileTxnSnapLog.restore方法从磁盘中读取文件
    四-Zookeeper - 图19
    4.9.3.4.2、Socket服务启动
    之前介绍过ServerCnxnFactory作用,ServerCnxnFactory本身也可以作为一个线程,其run方法实现的大致逻辑是:构建reactor模型的EventLoop,Selector每隔1秒执行一次select方法来处理IO请求,并分发到对应的代表该客户端的ServerCnxn中并利用doIO进行处理 。
    1. /**启动ServerCnxn,建立Socket通道*/
    2. startServerCnxnFactory();
    4.9.3.4.3、Leader选举初始化
    初始化一些Leader选举工作
    1. 创建一个QuorumCnxManager实例,负责集群中各节点的网络IO
    2. QuorumCnxManager实例内部有一个listener监听器,会启动一个线程,主要是用来监听选举端口并处理连接进来的socket
    3. 选择使用FastLeaderElection算法(一共有4个选举算法,其他三个都已经过时废弃了).
    1. /**创建出选举算法*/
    2. startLeaderElection();
    4.9.3.4.4、启动QuorumPeer,单独开启一个线程用于Leader选举(发现,选举,广播,同步)
    这里调用的是supper.start()方法,由于QuorumPeer继承自ZooKeeperThread,所以自身也是一个线程,进入run()方法后可以发现这里进入到一个无限循环的模式,不停的通过getPeerState方法获取当前的状态,然后执行相应分支的逻辑:
    系统刚启动的时候serverState默认是LOOKING状态,需要进行选举,这时会调用 FastLeaderElection.lookForLeader方法 ,该方法内部有一个循环的逻辑,直到选举出leader后才会跳出,如果选举出的Leader节点就是自身,那么就会将serverState变更为LEADING,否则设置成FOLLOWING或OBSERVING
    然后进入下一轮的循环,根据自身的状态角色,执行对应的逻辑,如果是Leader
    注意: 进入分支路程会一直阻塞在其分支中,直到角色转变才会重新进行下一轮次循环,比如Follower监控到无法与Leader保持通信了,会将serverState赋值为LOOKING,跳出分支并进行下一轮次循环,这时就会进入LOOKING分支中重新进行Leader选举
    1. /*
    2. * 进入无限循环,不停的通过getPeerState方法获取当前节点状态
    3. * 注意:进入分支路程会一直阻塞在其分支中,直到角色转变才会重新进行下一轮次循环
    4. */
    5. while (running) {
    6. switch (getPeerState()) {
    7. case LOOKING:
    8. if (Boolean.getBoolean("readonlymode.enabled")) {
    9. // Create read-only server but don't start it immediately
    10. final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(logFactory, this, this.zkDb);
    11. Thread roZkMgr = new Thread() {
    12. public void run() {
    13. try {
    14. sleep(Math.max(2000, tickTime));
    15. if (ServerState.LOOKING.equals(getPeerState())) {
    16. roZk.startup();
    17. }
    18. } catch (InterruptedException e) {
    19. LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
    20. } catch (Exception e) {
    21. LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
    22. }
    23. }
    24. };
    25. try {
    26. roZkMgr.start();
    27. reconfigFlagClear();
    28. if (shuttingDownLE) {
    29. shuttingDownLE = false;
    30. startLeaderElection();
    31. }
    32. setCurrentVote(makeLEStrategy().lookForLeader());
    33. } catch (Exception e) {
    34. LOG.warn("Unexpected exception", e);
    35. setPeerState(ServerState.LOOKING);
    36. } finally {
    37. roZkMgr.interrupt();
    38. roZk.shutdown();
    39. }
    40. } else {
    41. try {
    42. reconfigFlagClear();
    43. if (shuttingDownLE) {
    44. shuttingDownLE = false;
    45. /**
    46. * 选举leader
    47. */
    48. startLeaderElection();
    49. }
    50. //调用FastLeaderElection.lookForLeader()
    51. setCurrentVote(makeLEStrategy().lookForLeader());
    52. } catch (Exception e) {
    53. LOG.warn("Unexpected exception", e);
    54. setPeerState(ServerState.LOOKING);
    55. }
    56. }
    57. break;
    58. case OBSERVING:
    59. try {
    60. /**创建Observer实例并调用observerLeader*/
    61. setObserver(makeObserver(logFactory));
    62. observer.observeLeader();
    63. } catch (Exception e) {
    64. LOG.warn("Unexpected exception", e);
    65. } finally {
    66. observer.shutdown();
    67. setObserver(null);
    68. updateServerState();
    69. }
    70. break;
    71. case FOLLOWING:
    72. try {
    73. /**创建Follower实例并调用followLeader*/
    74. setFollower(makeFollower(logFactory));
    75. follower.followLeader();
    76. } catch (Exception e) {
    77. LOG.warn("Unexpected exception", e);
    78. } finally {
    79. follower.shutdown();
    80. setFollower(null);
    81. updateServerState();
    82. }
    83. break;
    84. case LEADING:
    85. /**创建Leader实例并调用lead*/
    86. try {
    87. setLeader(makeLeader(logFactory));
    88. leader.lead();
    89. setLeader(null);
    90. } catch (Exception e) {
    91. LOG.warn("Unexpected exception", e);
    92. } finally {
    93. if (leader != null) {
    94. leader.shutdown("Forcing shutdown");
    95. setLeader(null);
    96. }
    97. updateServerState();
    98. }
    99. break;
    100. }
    101. start_fle = Time.currentElapsedTime();
    102. }
    4.9.3.4.5、阻塞,直到服务停止

    4.9.4、服务启动之-加载快照、事务文件到内存生成DataTree模型

    4.9.4.1、应用快照文件

    QuorumPeer在实例化的时候,会创建ZKDatabase实例,该类有以下几个属性:
    四-Zookeeper - 图20

    4.9.4.2、获取最近一次提交的事务id

    4.9.4.3、从zxid中解析获取epoch任期,以及从文件中解析当前epoch

    4.9.5、服务启动之-Socket服务启动

    服务器从快照中恢复数据构建出DataTree模型后,开始启动serverCnxn,建立socket通道,这里启动主要对应两个实现:NIO服务和Netty服务,具体选择哪一种服务,是在初始化quorumPeer时进行配置的

    4.9.5.1、创建ServerCnxnFactory

    调用ServerCnxnFactory.createFactory()方法实例化一个cnxnFactory,而具体选择哪种服务,已经被封装起来,具体细节可见createFactory方法:
    可以看到通过反射的方式生成ServerCnxnFactory,默认使用的是NIO,可以通过zookeeper.serverCnxnFactory参数进行配置,如果选用Netty,则配置成 NettyServerCnxnFactory.class.getName()
    可以看到通过反射的方式生成ServerCnxnFactory,默认使用的是NIO,可以通过zookeeper.serverCnxnFactory参数进行配置,如果选用Netty,则配置成 NettyServerCnxnFactory.class.getName()
    这里通过NettyServerCnxnFactory来看一下内部细节
    构造函数中,初始化ServerBootstrap对象,设置TCP参数,设置channelHandler
    CnxnChannelHandler继承自ChannelDuplexHandler, 它被标注为@Sharable,还是一个共享的处理器。 主要 处理各种IO事件。比如客户端连接、断开连接、可读消息
    四-Zookeeper - 图21四-Zookeeper - 图22
    四-Zookeeper - 图23

    4.9.5.2、调用ServerCnxnFactory.start方法来启动Netty服务

    通过调用quorumPeer.start方法来调用cnxnFactory.start方法来启动Netty服务
    quorumPeer.start方法内部封装了一个startServerCnxnFactory方法,而startServerCnxnFactory方法底层调用了cnxnFactory.start方法,即quorumPeer.start—->startServerCnxnFactory—->cnxnFactory.start
    四-Zookeeper - 图24四-Zookeeper - 图25

    4.9.6、服务启动之-Leader选举初始化

    4.9.6.1、选举初始化

    Leader选举入口: QuorumPeer.startLeaderElection() ,大致流程如下:
    判断当前服务器状态是否为LOOKING,如果是则创建Vote投票实例,服务刚启动时会把票投给自己
    创建QuorumCnxManager实例,主要负责集群中各个节点的网络IO
    QuorumCnxManager有一个内部类Listener,会启动一个线程用于监听选举端口并处理连接进来的Socket
    构建一种选举算法FastLeaderElection,早期Zookeeper实现了四种选举算法,但是后面废弃了三种,最新版本只保留FastLeaderElection这一种选举算法
    四-Zookeeper - 图26

    4.9.6.2、网络IO

    四-Zookeeper - 图27
    Leader选举涉及到两个核心类:QuorumCnxManager和FastLeaderElection ,红线之上是 QuorumCnxManager工作区域,红色线之下的是FastLeaderElection工作区域 。 选举算法逻辑被封装在FastLeaderElection类中 ; 而QuorumCnxManager则用于管理维护选举期间的网络IO。
    网络IO大致流程:
    初始化阶段,会实例化QuorumCnxManager,而且会创建内部listener线程,创建ServerSocket,然后循环等待客户端连接(注意:这里的客户端指的是集群其他节点)
    当有客户端连接进来后,会把该客户端socket封装成 RecvWorker和SendWorker,它们都是线程,分别负责和该Socket所代表的客户端进行读写;RecvWorker和SendWorker是成对出现的,每对负责维护和集群中的一台服务器进行网络IO通信 (注意:这里为了避免资源浪费,只需建立一条通道,即当接收到到一个请求时会进行比对myid,如果对端myid大于自己,则认为连接有效,也就是myid小的一方作为服务端)
    FastLeaderElection负责Leader选举核心规则算法实现,注意FastLeaderElection类中也包含了两个内部类WorkerSender和WorkerReceiver,类似于QuorumCnxManager中的SendWorker和RecvWorker,也是用于发送和接收线程
    FastLeaderElection中进行选举时广播投票信息时,将投票信息写入到对端服务器大致流程如下:
    将数据封装成ToSend格式放入到sendqueue;
    WorkerSender线程会一直轮询提取sendqueue中的数据,当提取到ToSend数据后,会获取到集群中所有参与Leader选举节点(除Observer节点外的节点)的sid,如果sid即为本机节点,则转成Notification直接放入到recvqueue中,因为本机不再需要走网络IO;否则放入到queueSendMap中,key是要发送给哪个服务器节点的sid,ByteBuffer即为ToSend的内容,queueSendMap维护的着当前节点要发送的网络数据信息,由于发送到同一个sid服务器可能存在多条数据,所以queueSendMap的value是一个queue类型;
    QuorumCnxManager中的SendWorkder线程不停轮询queueSendMap中是否存在自己要发送的数据,每个SendWorkder线程都会绑定一个sid用于标记该SendWorkder线程和哪个对端服务器进行通信,因此,queueSendMap.get(sid)即可获取该线程要发送数据的queue,然后通过queue.poll()即可提取该线程要发送的数据内容;
    然后通过调用SendWorkder内部维护的socket输出流即可将数据写入到对端服务器
    FastLeaderElection中进行选举时广播投票信息时,从对端服务器读取投票信息的大致流程如下:
    QuorumCnxManager中的RecvWorker线程会一直从Socket的输入流中读取数据,当读取到对端发送过来的数据时,转成Message格式并放入到recvQueue中;
    FastLeaderElection.WorkerReceiver线程会轮询方式从recvQueue提取数据并转成Notification格式放入到recvqueue中;
    FastLeaderElection从recvqueue提取所有的投票信息进行比较 最终选出一个Leader
    四-Zookeeper - 图28
    四-Zookeeper - 图29
    四-Zookeeper - 图30

    4.9.6.3、Leader选举

    Leader选举策略使用的是FastLeaderElection ,当检测到serverState状态为LOOKING时进入到LOOKING分支中调用lookForLeader方法开始选举,具体流程如下:
    调用 FastLeaderElection.lookForLeader方法 ,开始选举
    更新自己期望投票信息,即自己期望选哪个服务器作为Leader(用sid代替期望服务器节点)以及该服务器zxid、epoch等信息,第一次投票默认都是投自己当选Leader,然后调用sendNotifications方法广播该投票到集群中所有可以参与投票服务器 (注意:调用 updateProposal 方法来更新投票信息,有三个参数: a.期望投票给哪个服务器(sid)、b.该服务器的zxid、c.该服务器的epoch
    然后等待其他服务器发送给自己投票信息
    将接收到的投票state进行判断确定接下来执行哪个逻辑接下来首先比较epoch,如果接收到的epoch比自己的要大,此时要清空之前获取的所有投票, 因为之前获取的投票轮次落后于当前则代表之前的投票已经无效了 ,然后开始根据epoch,zxid,sid进行PK,最后将pk结果同步出去;当接收到的epoch小于自己时,则直接省略;当接收到的epoch和自己的一致时,则将投票放入到投票列表集合中,然后进行PK,看是否能够选举出Leader,如果能选举出,则选举结束,并更改自身状态,否则需要继续接收投票
    当接收到的状态为LOOKING时
    当接收到的状态为OBSERING时,则忽略投票信息
    当接收到的状态为LEADING时,这时只需要验证是否有效,如果有效则选举结束,否则继续接收投票信息
    四-Zookeeper - 图31
    四-Zookeeper - 图32
    四-Zookeeper - 图33

    4.9.7、服务启动之-Leader选举/数据同步/提供服务/状态检测

    在之前的流程中,经过了选举初始化,涉及到网络io,以及到最后选举leader之后,每台机器都有了各自的角色,但这时仍不能对外提供服务,还需要再进行数据同步阶段,当保证集群中的数据状态一致时,这时候的leader才能对外提供服务
  • 对于Leader来说创建Leader实例并调用其lead方法
  • 对于Follower来说创建Follower实例并调用followLeader方法
  • 对于Observer来说创建Observer实例并调用observeLeader方法

    4.9.7.1、Leader方调用lead方法逻辑

    整体流程如下:
    调用LeaderZooKeeperServer实例的loadData方法进行快照和失效会话清理
    接收Learner端的请求,并为每个请求创建一个LearnerHandler处理线程
    计算epoch,并进行同步,等待Learner应答
    当有过半节点应答了epoch,这时进行数据同步
    启动会话管理器,初始化处理器责任链,正式对外提供服务
    进入循环,进行心跳检测以及会话管理流程
    这里要说明一下zookeeper中三个主要的端口:
    客户端请求端口,默认是2181,也就是客户端进行增删改查请求的端口
    集群选举端口,涉及到的网络IO使用的端口,比如”server.1=zk1:2888:3888”,这里3888就是选举端口
    集群同步端口,也就是leader和Learner之间的同步用到的端口,对应的端口是2888

    4.9.7.1.1、Leader调用lead方法内部逻辑

    四-Zookeeper - 图34

    4.9.7.1.2、Leader端第一步流程:快照和失效会话清理

    加载zkDatabase,设置事务id(如果已经进行了初始化则直接从DataTree模型中解析,否则需要进行初始化操作)
    遍历会话,清理失效会话
    生成快照
    四-Zookeeper - 图35

    4.9.7.1.3、实例化LearnerCnxAcceptor,创建LearnerHandler线程

    Leader服务器会创建LearnerCnxAcceptor,用来处理Learner的连接请求,且为每个连接创建一个handler线程用于通信
    四-Zookeeper - 图36

    4.9.7.1.4、计算epoch,同步epoch

    leader选举完成后,会重新计算epoch(也就是所谓的任期),然后将新计算的epoch值同步给所有的learner,当有过半的节点应答了epoch,那么此时leader也就算是上任了,否则的话需要重新进行选举,直到新的leader产生。
    具体epoch计算规则就是获取所有Learner中最大的epoch,在此基础上加1即得到最新的epoch;
    四-Zookeeper - 图37

    4.9.7.1.5、数据同步

    leader获取到过半以上节点的epoch应答后,这时算是正式成为leader了,但此时还不能正常工作,还需要进行数据的同步,保证整个集群数据一致后才能对外提供服务。Leader和Learner在进行数据同步时会通过peerLastZxid(Learner服务器上最后处理的ZXID) maxCommittedLog(Leader服务器上提议缓存队列commitedLog中最大ZXID), minCommittedLog(Leader服务器上提议缓存队列commitedLog中最小ZXID) 两个值比较最终决定数据同步方式
    DIFF(差异化同步)
    情况一:当learner的peerLastZxid等于Leader的peerLastZxid,此时说明follower和leader数据一致,采用diff方式同步,即无需同步,但仍会发送一个空包
    情况二:当learner的peerLastZxid介于minCommitedLog和maxCommitedLog之间,说明Learner和Leader之间存在数据差异,需要对差异的部分进行同步,这个时候Leader会发送一个DIFF报文以及同步方式,随后再发送差异提案以及提案提交报文
    例如: 假设 leader 节点的提案缓存队列对应的 zxid 依次是: 0x500000001, 0x500000002, 0x500000003, 0x500000004, 0x500000005 ; 而 follower 节点的 peerLastZxid 为 0x500000003,则需要将 0x500000004, 0x500000005 两个提案进行同步;那么数据包发送过程如下表:
    四-Zookeeper - 图38
    TRUNC+DIFF(回滚+差异化同步)
    在DIFF差异化同步中会存在一个特殊化的场景,即数据不一致的情况,虽然Learner的peerLastZxid介于minCommitedLog和maxCommitedLog之间,但是Learner的peerLastZxid在Leader节点上不存在,此时Leader需要告知Learner先回滚到peerLastZxid的前一个zxid,回滚后再进行差异化同步。
    例如: 假设集群中三台节点 A, B, C 某一时刻 A 为 Leader 选举周期为 5, zxid 包括: (0x500000004, 0x500000005, 0x500000006); 假设某一时刻 leader A 节点在处理完事务为 0x500000007 的请求进行广播时 leader A 节点服务器宕机导致 0x500000007 该事物没有被同步出去;在集群进行下一轮选举之后 B 节点成为新的 leader,选举周期为 6 对外提供服务处理了新的事务请求包括 0x600000001, 0x600000002;
    四-Zookeeper - 图39
    此时节点 A 在重启加入集群后,在与 leader B 节点进行数据同步时会发现事务 0x500000007 在 leader 节点中并不存在,此时 leader 告知 A 需先回滚事务到 0x500000006,在差异同步事务 0x600000001,0x600000002;那么数据包发送过程如下表:
    四-Zookeeper - 图40
    TRUNC(回滚同步)
    当Learner的peerLastZxid大于Leader的maxCommitedLog时,需要告知Learner回滚到maxCommitedLog;
    SNAP(全量同步)
    当Learner的peerLastZxid小于Leader的minCommitedLog或者Leader节点上不存在提案缓存队列时,将采用SNAP全量同步方式。该模式下Leader首先会向Learner发送SNAP通知,然后从内存数据库中获取全量数据序列化传输给Learner,Learner接收到全量数据后反序列化加载到内存数据库中
    四-Zookeeper - 图41

    4.9.7.1.6、Leader通知Learner正式对外提供服务

    Leader完成数据同步后,集群数据达到一致时,这时会调用LeaderZooKeeperServer.startup方法,开启会话管理器,设置自身状态为RUNNING,并初始化处理器责任链正式对外提供服务
    四-Zookeeper - 图42

    4.10、适用场景

    4.10.1、发布/订阅

    对于发布订阅系统有2种设计模式:推push / 拉pull
    在push模式下,服务端将所有事件更新发给订阅的客户端;在pull模式下,由客户端主动发起请求获取最新数据。客户端通常采用轮询方式来获取数据。
    对于zk来说采用的是推拉组合的模式,当客户端向服务端注册自己需要关注的节点后,一旦该节点数据发生变更,服务器向客户端发送Watcher事件通知,收到消息后客户端会主动向服务器获取最新数据。

    4.10.2、命名服务

    命名服务是分布式系统中常见的一类场景。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或者远程对象等。通过命名服务,客户端可以根据指定名字来获取资源的实体、服务地址和提供者信息。
    zk也可以帮助应用系统通过资源引用的方式来实现对资源的定位和使用,广义上的命名服务的资源定位都不是真正意义上的实体资源,在分布式环境中,上层应用仅仅需要一个全局唯一的名字。在zk中可以实现一套分布式全局唯一ID的分配机制。
    由于zk可以创建顺序节点,保证额同一节点下子节点是唯一的,所以直接按照存放文件的方法,设置节点,比如一个路径下不可能存在两个相同的文件名,这种定义创建节点,就是全局唯一ID

    4.10.3、配置管理

    在实际的生产环境中,特别是在分布式系统环境下,如果我们想要调整配置信息,手动去逐个改配置就变得有点笨拙。
    但如果把这些配置全部放到zookeeper上去,保存在zookeeper的某个目录节点上,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到zk的通知,然后从zk获取新的配置信息应用到系统中就好。这样就会可以实现自动变动。

    4.10.4、集群管理

    在日常集群维护管理中,机器的扩展和主从的选择是比较常见的两种情况。
    对于机器的扩展这种情况,所有机器约定在父目录GroupMembers下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器和zk的连接断开,其所创建的代表该节点存活状态的临时目录节点也被删除,所有其他机器都将收到通知。机器加入也是同样。
    通过利用zk的强一致性,能够保证在分布式高并发的情况下节点创建的全局唯一性。即:同时有多个客户端请求创建/currentMaster节点,最终一定只有一个客户端请求能够创建成功。利用这一特性,就能很轻易在分布式环境中进行集群选取了。

    4.10.5、分布式锁

    对于锁的类型可以分为两三类:

  • 一种是写锁,即对写进行加锁,并且保持独占,这种锁也叫排他锁或者独占锁。

  • 一种是读锁,即对读进行加锁,可以共享访问,当锁释放之后才能进行事务操作,这种锁也叫共享锁。
  • 一种是控制时序,叫做时序锁。

对于第一类锁,可以将Zk上的一个znode看作一把锁,通过createznode()的方式来实现。当所有的客户端都去创建同一个znode节点时,那么只有最终成功创建的那个客户端也拥有这把锁,用完并删掉自己创建的节点后就会释放掉锁。
对于第二类锁,举例来说当znode节点已经存在,所以的客户端在该节点下面创建临时性具有顺序编号的目录节点,和选举master一样,编号最小的就会获得锁,用完就删,依次有序。

4.10.6、队列管理

队列的类型可以分为两种:

  • 同步队列/分布式屏障:当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。
  • 先进先出队列:队列按照FIFO方式进行入列和出列操作

对于第一种类型的队列,zk层面的实现可以在约定的目录下创建临时目录节点,并监听节点数目是否是我们要求的数目
对于第二种类型的队列,zk层面的实现和分布式锁服务中的控制时序场景基本原理一致,即入列有编号,出列按编号

4.10.7、负载均衡

zk在负载均衡的实现上可以借鉴时序锁的实现机制