前言

在学习Zookeeper的过程中,越看越觉得其强大。zk背后有一套强大的理论体系,正是这些理论才成就了zk特有的特性,才担当的起分布式协调,利用zk的特性可以实现很多功能。

官方文档可以锻炼下英文阅读能力。有些知识点网上说法不一,比较困惑,参考官方文档绝对是最佳选择。

ZK能做什么

ZK可以为为分布式系统提供一致性服务,例如配置维护,域名服务,分布式同步,集群管理,一致性服务则是通过Paxos算法的ZAB协议来实现的,利用ZK的特性可以实现很多功能。

ZK的一致性

一致性主要体现在以下几个方面:

  • 顺序一致性:一个客户端发起的多个写操作,会严格按照其发起顺序执行
  • 原子性:事务请求的结果在集群所有主机中是一致的,要么都成功要么都失败
  • 单一视图:客户端无论连接的是哪个集群中的主机,其独到的数据都是一样的
  • 可靠性:一个事务完成写操作,那么其结果会一直保留,直到另一个事务将它改变
  • 最终一致性:一个事务操作成功,最终都会同步,但是无法保证数据实时性

CAP理论

在一个分布式系统中Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)三者不可兼得。

分布式系统中要么是CP,要么是AP,zk遵循的是CP原则,保证了一致性,但是牺牲了可用性。这是因为当Leader宕机后,需要重新进行Leader选举,在选举过程中整个集群是不可用的。

只要是分布式系统,就一定会存在网络问题。因此分布式系统一定要保证分区容错性,而一致性和可用性在只要不是单机环境就一定不能同时满足。例如服务器A和B,如果数据同步时发生网络延迟,那用户就拿不到最新的数据,这就失去了一致性;或者一直阻塞等待数据同步为止,但这又失去了可用性。因此分布式系统中要么是CP、要么是AP。

  • 一致性,分布式系统中多个主机中的数据是否处于一致状态
  • 可用性,系统提供的服务必须一直处于可用的状态,即对于每一个用户的请求,系统总是可以在有限的时间内做出响应
  • 分区容错性,分布式系统中任何一分区出现故障时,仍能对外提供满足一致性和可用性的服务

BASE理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)的缩写。BASE是对CP和AP权衡的结果。

  • 基本可用,当分布式系统出现故障时,允许损失部分可用性
  • 软状态,处于一种过渡状态
  • 最终一致性,系统中的数据在同步一段时间后,数据最终能保持一致性性

zk遵循的是CP原则,可用性体现在选举的时候一般会有30-200ms左右的耗时,在选举时整个集群处于瘫痪状态,不对外提供读写服务。

Paxos算法简介

Paxos算法是莱斯利·兰伯特提出的一种基于消息传递(另一种模式是共享内存)且具有高度容错特性的一致性算法。Paxos算法的前提是不考虑拜占庭将军问题。

算法中三种角色

  • Proposer,提案者
  • Acceptor,表决者
  • Learners,学习者

算法一致性

  1. 每个Proposer提出提案时会获取到一个具有全局唯一性、递增的提案编号
  2. 每个Acceptor在accept提案后,会将该提案编号记录在本地。注意:Acceptor第一次一定要保存,往后只会保存比本地大的提案编号
  3. 在众多提案中,只有一个提案会被选定
  4. 一旦某个提案被选定,其它主机会主动同步该提案到本地

算法的两个阶段

算法执行过程分为准备(prepare)阶段和接受(accept)阶段。

prepare阶段

  1. Proposer提交一个提案的编号,向所有Acceptor通过一个prepare请求发送提案编号,来试探集群是否支持该编号的提案
  2. 每个Acceptor都保存着自己曾经accept过最大的那个提案编号,当接收到prepare请求时,会把prepare请求中的提案编号和本地保存的提案编号进行比较,有以下几种情况:
    1. 远程编号小于本地编号,说明该提案已过时,当前Acceptor不回应或者返回Error的方式来拒绝该prepare请求
    2. 远程编号大于本地编号,说明该提案是可以接受的,当前Acceptor会将提案编号记录下来,并将自己之前最大的提案编号反馈给Proposer

accept阶段

  1. 当Proposer发出prepare请求后,若收到半数Acceptor的反馈,那么Proposer会将提案发送给所有Acceptor
  2. 当Acceptor接收到Proposer的提案后,会再次将本地提案编号和请求过来的提案编号进行对比,如果大于等于请求过来的提案编号则接受提案,并反馈给Proposer,否则,就不回复或回应Error来拒绝该提案
  3. 当Proposer并没有收到超过半数Acceptor的反馈,那么会放弃该提案或者重新进入准备阶段,递增提案号,重新发起prepare请求
  4. 若提案者接收到反馈数量超过了半数,则其会向外广播两类信息:
    1. 向曾accept过的Acceptor发送提案+可执行数据的同步信号,即让它们执行其曾接收到的提案
    2. 向未曾向其发送accept反馈的的Acceptor发送提案+可执行数据同步信号,即让它们接收到该提案后马上执行

ZAB协议简介

Zookeeper Atomic Broadcast,zk原子消息广播协议,是基于Paxos算法专门为Zookeeper设计的一种支持崩溃恢复的原子广播协议,在Zookeeper中,就是靠ZAB来实现一致性的。

ZK简介

Zookeeper使用单一主进程来处理客户端所有事物请求,当服务器数据发生变更后,集群采用ZAB协议,以事务提案Proposal的形式进行广播,为每一个事物分配一个全局递增编号xid。

当Zookeeper客户端连接到集群其中一个节点时,若客户端是读请求,那么从当前节点直接返回数据,如果是写请求且该节点不是Leader,那么会将该写请求转发给Leader,然后以提案的方式进行广播写操作,只要超过半数节点就同意该写操作,然后Leader再次广播给Learner,通知它们同步数据。

zk为了避免单节点的问题,因此zk一般是以集群的方式出现的,zk集群提供了三类角色:

  • Leader:zk集群中事务请求的唯一处理者,并负责进行投票和决议,将事务处理结果同步给集群中其它主机。它是由集群中的主机选举产生
  • Follower:接收和处理读请求,并将事务请求转发给Leader,同步Leader的数据。当Leader挂了,参与Leader的选举
  • Observer:需要在配置文件中配置。和Follower一样,但是它没有选举和被选举权。Observer数量一般小于等于Follower数量。

根据角色的功能可以看出来,Follower和Observer的功能一样,不过它不会参与选举和被选举,Observer的好处在于不会增大投票时的压力,从而降低了Leader写操作效率和选举的效率。参选主机越少选的越快

但Observer也不是越多越好,因为它要从Leader同步数据,这个是需要时间的,这个同步的时间是小于等于Follower同步的时间,Follower一旦同步完成就会结束同步。为了保证数据一致性,Observer只有同步到数据才能对外提供服务,没有同步的Observer主机暂时无法提供读服务,因为它要和Leader同步数据。一般Observer的数量和Follower的数量相等,写操作频繁的场景下Observer也不应该太多

  • Learner=Follower+Observer
  • QuorumServer=Follower+Leader

三个重要的数据

  • zxid

Leader收到事务请求后,会为事务赋予一个全局唯一的64位自增的id,即zxid,通过zxid大小可以进行事务有序性的管理。高32位是epoch,低32位是xid

  • epoch

每个Leader都会有一个不同的epoch,用于区分不同的时期

  • xid

事务id

ZK的三种模式

  • 恢复模式

当集群启动时或Leader挂了, zk就会进入该模式,恢复系统对外提供服务的能力,该模式下会进行Leader选举和
初始化同步。

  • 广播模式

初始化广播和更新广播。

当集群中有过半的Follower完成了初始化状态同步,那么整个zk集群就进入正常工作模式,其它客户端收到事务请求,会转发到Leader服务器。

当集群正在启动或Leader与超过半数的主机断开连接后,集群就会进入恢复模式,恢复时要遵循两个原则:

  • 已经被处理过的消息不能丢
  • 被丢弃的消息不能重现
  • 同步模式

初始化同步和更新同步

Leader选举

这里以三台为举例。投票包含两个参数(myid,zxid),在集群初始化阶段,第1台服务器会给自己投票,然后发布自己的投票结果(1,0),此时该服务器处于looking状态;第2台服务器启动时也给自己投票(2,0),然后将投票结果发送给其它服务器,投票需要判断有效性,即是否来自处于looking状态的服务器;然后对投票结果进行pk,zxid大的优先为Leader,zxid相同则比较myid,大的那个就是Leader,第一台服务器会更新自己的投票结果为Leader的投票结果;然后修改自己的状态,是follower就更改为following,是Leader就更改为leading。第3台启动,发现各个主机状态不是looking,所以以follower的身份加入集群中。

ZK中其它一些概念

  • 数据模型是树形结构。
  • 选举使用的是TCP协议
  • Zookeeper中集群只要超过一半的机器正常,这个集群就能正常使用,例如5台有3台正常就可以,6台需要4台正常就可以。
  • 从容灾能力的角度来看zk节点的奇数和偶数是没有区别的,但是从性能来说越多越好。
  • 集群中每一个节点都有一个唯一标识:myid。
  • 逻辑时钟,在选举时成为逻辑时钟,在选举结束后成为epoch,是不同时期的不同叫法。是一个整型数值。

ZNode

ZNode是zk中的节点,类似文件系统的储存结构,使用绝对路径,ZNode可以使永久的,也可以是临时的,临时ZNode不允许创建子ZNode,ZNode存储数据最大限制为1M。

节点类型

  • 持久节点
  • 持久顺序节点
  • 临时节点,临时ZNode不允许创建子ZNode
  • 临时顺序节点

节点状态

除了存储的数据,ZNode 包含了称为 Stat 的数据结构,用于存储 ZNode 的属性信息,主要包括:

  1. cZxid / mZxid:ZNode 创建 / 最后更新的事务id
  2. ctime / mtime:ZNode 创建 / 最后更新的时间(Unix 时间,毫秒)
  3. dataVersion :ZNode 数据版本,可以充当乐观锁
  4. cversion:子节点的版本号
  5. dataLength :ZNode 存储的数据长度
  6. numChildren :子级 ZNode 的数量
  7. aclVersion:ACL的版本号
  8. ephemeralOwner:是持久节点则为0,临时节点则为sessionId,临时节点通过sessionId来删除

ACL

访问控制列表(Access Control List),zk利用它来进行ZNode节点的权限控制策略。

ZK Watcher

利用这个机制可以实现很多功能。我司用它进行配置维护,分布式数据同步。

对于全部的“读”操作,ZooKeeper 允许客户端于 ZNode 设置 Watch,当 ZNode 变更时,Watch 将被触发并且通知到客户端(即 Watcher)。Watch 是 “一次性” 的,Watch 被触发时即被清除。

Watch“异步地”通知到客户端,“通知内容”不包含 ZNode 变更后的数据,需要由客户端读取。由 ZooKeeper 确保,事件到客户端的通知,严格“按顺序”进行(事务于 ZooKeeper 中的顺序)。此外,当 Watch 被触发时,设置了 Watch 的客户端,接收到通知前,无法获取变更后的数据。

zk通过Watcher机制实现了发布/订阅模式。具体执行流程:

  1. 客户端生成Watcher对象,并发到WatcherManger中
  2. 客户端向服务端注册Watcher
  3. 服务端发生watcher事件
  4. 服务端向客户端发送相应的时间通知
  5. 客户端根据通知从WatcherManager找到watcher对象
  6. watcher对象执行相应的回调

watcher 事件类型

watcher事件是一次性、有序性的。

客户端状态 事件类型 触发条件 备注
SyncConnected None 客户端与服务器成功建立会话 此时客户端与服务端
处于连接状态
NodeCreated Watcher监听的数据节点被创建
NodeDeleted Watcher监听的数据节点被删除
NodeDataChanged Watcher监听的数据节点发生变化
NodeChildrenChanged Watcher监听的数据节点子节点发生变化
Disconnected
  1. | None | 客户端与服务端断开连接 | 此时客户端与服务端<br />断开连接 |

| Expired
| None | 会话失效 | 客户端会话失效 | | AuthFailed | None | 使用错误的 scheme 进行权限检查 | 权限操作异常 |

zk session

ZooKeeper 会话,即为 ZooKeeper 客户端与服务端交互的通道。概述而言,客户端和服务端的 TCP 连接即为 Session,ZooKeeper 抽象了更多的会话状态。

Session 于 ZooKeeper 服务端,以 sessionId 作为唯一标识,同时,ZooKeeper 支持客户端使用 sessionId 进行“session 复用”(需要同时提供 SessionPasswd)。

session 状态维护

ZooKeeper 客户端对象创建时,Session 即进入 CONNECTING 状态,当客户端与服务端(集群的任意节点)完成连接,即进入 CONNECTED 状态。

客户端主动关闭 Session 前,通过“心跳”维护 Session 有效性,若连接中断,ZooKeeper 客户端将尝试重新连接(再次进入 CONNECTING ):

  1. 若在“Session 超时时间”内,连接重新建立,Session 继续有效,再次进入 CONNECTED;
  2. 否则,服务端将标记 session 过期(即使连接最终重新建立),进行清理(删除临时ZNode节点),session 最终进入 CLOSE 状态。

Session 是否过期,完全由 ZooKeeper 服务端维护。对于 ZooKeeper 客户端,仅当 Session 过期,才应当重新创建客户端对象。

集群节点四种状态

  1. looking,选举状态,不可以对外提供服务
  2. following,Follower正常工作状态,从Leader同步数据
  3. observing,Observer的正常工作状态,从Leader同步数据
  4. leading,Leader的正常工作状态,Leader广播数据更新状态

zk使用

Java中使用zk

目前最好用的是Apache的Curator,提供了非常易用的API,封装了很多的功能。【demo待补充】
看了它GitHub给出的案例,已经很全了,而且超多注释,感动~image.png

zk 分布式锁

原理介绍

zk集群

生产环境建议三机房部署,集群中主机数量为N,则三个机房集群数量分别为N1、N2、N3,假设N为15,则三个机房zk数量分别取值为
N1:N1=(N-1)/2,那么N1=7
N2:1< N2 < (N-N1)/2,那么1<=N1<=4
N3: N3=N-N1-N2,那么4<=N3<=7
这样做的好处是上述任一个机房如果断电等突发事故,都不会影响zk集群正常工作,因为仍然有超过半数的节点正常运行

集群脑裂

跟redis类似。zk集群中原来只有一个Leader,如果网络有问题时,原来的Leader没有挂,但网络原因让其它的Follower以为Leader挂了,它们又重新选举出一个Leader,但此时网络又好了,此时的zk集群会出现两个Leader,这就是脑裂问题。脑裂问题会导致数据异常,只能靠重启zk集群来解决问题。

集群搭建

集群搭建非常简单,关键在于zoo.cfg的配置,注意的是zk每次修改配置需要重启,配置才能生效,我在docker中装4台centos环境,配置好一台直接复制,稍微改下配置就行了,很快。这里要注意的是它依赖于JDK,所以需要安装JDK,或者直接在本机搭建伪集群也可以,下面是zoo.cfg的关键配置:

  1. # myid存放在zoo.cfg配置中的datadir目录下的myid文件中,它是一个zk的身份id
  2. # port1是zk的通信端口,port2是选举端口,这个两个端口可以随便填,参考官网填的是2888和3888
  3. # 配置为observer的不参与选举与被选举
  4. # 配置为observer的那台主机zoo.cfg文件中还要加上peerType=observer
  5. server.myid1=ip:port1:port2
  6. server.myid2=ip:port1:port2
  7. server.myid3=ip:port1:port2
  8. server.myid4=ip:port1:port2:observer

zkServer 使用

  1. # 启动
  2. ./zkServer.sh start
  3. # 启动
  4. ./zkServer.sh restart
  5. # 停止
  6. ./zkServer.sh stop
  7. # 查看状态,是否启动,集群中的角色
  8. ./zkServer.sh status
  9. # 连接指定zk
  10. ./bin/zkCli.sh -server ip:port
  11. # 查看根路径
  12. ls /
  13. # 创建路径
  14. create /路径名称
  15. # 保存数据
  16. set /路径名称
  17. # 删除路径
  18. delete /路径名称
  19. # 创建顺序节点,会生成一个节点为lock000000000
  20. create -s /lock
  21. # 创建临时节点节点,断开就自动删除
  22. create -e /lock/info
  23. # 删除该节点所有节点
  24. rmr /节点名称