前言
在学习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,学习者
算法一致性
- 每个Proposer提出提案时会获取到一个具有全局唯一性、递增的提案编号
- 每个Acceptor在accept提案后,会将该提案编号记录在本地。注意:Acceptor第一次一定要保存,往后只会保存比本地大的提案编号
- 在众多提案中,只有一个提案会被选定
- 一旦某个提案被选定,其它主机会主动同步该提案到本地
算法的两个阶段
算法执行过程分为准备(prepare)阶段和接受(accept)阶段。
prepare阶段
- Proposer提交一个提案的编号,向所有Acceptor通过一个prepare请求发送提案编号,来试探集群是否支持该编号的提案
- 每个Acceptor都保存着自己曾经accept过最大的那个提案编号,当接收到prepare请求时,会把prepare请求中的提案编号和本地保存的提案编号进行比较,有以下几种情况:
- 远程编号小于本地编号,说明该提案已过时,当前Acceptor不回应或者返回Error的方式来拒绝该prepare请求
- 远程编号大于本地编号,说明该提案是可以接受的,当前Acceptor会将提案编号记录下来,并将自己之前最大的提案编号反馈给Proposer
accept阶段
- 当Proposer发出prepare请求后,若收到半数Acceptor的反馈,那么Proposer会将提案发送给所有Acceptor
- 当Acceptor接收到Proposer的提案后,会再次将本地提案编号和请求过来的提案编号进行对比,如果大于等于请求过来的提案编号则接受提案,并反馈给Proposer,否则,就不回复或回应Error来拒绝该提案
- 当Proposer并没有收到超过半数Acceptor的反馈,那么会放弃该提案或者重新进入准备阶段,递增提案号,重新发起prepare请求
- 若提案者接收到反馈数量超过了半数,则其会向外广播两类信息:
- 向曾accept过的Acceptor发送提案+可执行数据的同步信号,即让它们执行其曾接收到的提案
- 向未曾向其发送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 的属性信息,主要包括:
- cZxid / mZxid:ZNode 创建 / 最后更新的事务id
- ctime / mtime:ZNode 创建 / 最后更新的时间(Unix 时间,毫秒)
- dataVersion :ZNode 数据版本,可以充当乐观锁
- cversion:子节点的版本号
- dataLength :ZNode 存储的数据长度
- numChildren :子级 ZNode 的数量
- aclVersion:ACL的版本号
- 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机制实现了发布/订阅模式。具体执行流程:
- 客户端生成Watcher对象,并发到WatcherManger中
- 客户端向服务端注册Watcher
- 服务端发生watcher事件
- 服务端向客户端发送相应的时间通知
- 客户端根据通知从WatcherManager找到watcher对象
- watcher对象执行相应的回调
watcher 事件类型
watcher事件是一次性、有序性的。
客户端状态 | 事件类型 | 触发条件 | 备注 |
---|---|---|---|
SyncConnected | None | 客户端与服务器成功建立会话 | 此时客户端与服务端 处于连接状态 |
NodeCreated | Watcher监听的数据节点被创建 | ||
NodeDeleted | Watcher监听的数据节点被删除 | ||
NodeDataChanged | Watcher监听的数据节点发生变化 | ||
NodeChildrenChanged | Watcher监听的数据节点子节点发生变化 | ||
Disconnected |
| 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 ):
- 若在“Session 超时时间”内,连接重新建立,Session 继续有效,再次进入 CONNECTED;
- 否则,服务端将标记 session 过期(即使连接最终重新建立),进行清理(删除临时ZNode节点),session 最终进入 CLOSE 状态。
Session 是否过期,完全由 ZooKeeper 服务端维护。对于 ZooKeeper 客户端,仅当 Session 过期,才应当重新创建客户端对象。
集群节点四种状态
- looking,选举状态,不可以对外提供服务
- following,Follower正常工作状态,从Leader同步数据
- observing,Observer的正常工作状态,从Leader同步数据
- leading,Leader的正常工作状态,Leader广播数据更新状态
zk使用
Java中使用zk
目前最好用的是Apache的Curator,提供了非常易用的API,封装了很多的功能。【demo待补充】
看了它GitHub给出的案例,已经很全了,而且超多注释,感动~
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的关键配置:
# myid存放在zoo.cfg配置中的datadir目录下的myid文件中,它是一个zk的身份id
# port1是zk的通信端口,port2是选举端口,这个两个端口可以随便填,参考官网填的是2888和3888
# 配置为observer的不参与选举与被选举
# 配置为observer的那台主机zoo.cfg文件中还要加上peerType=observer
server.myid1=ip:port1:port2
server.myid2=ip:port1:port2
server.myid3=ip:port1:port2
server.myid4=ip:port1:port2:observer
zkServer 使用
# 启动
./zkServer.sh start
# 启动
./zkServer.sh restart
# 停止
./zkServer.sh stop
# 查看状态,是否启动,集群中的角色
./zkServer.sh status
# 连接指定zk
./bin/zkCli.sh -server ip:port
# 查看根路径
ls /
# 创建路径
create /路径名称
# 保存数据
set /路径名称
# 删除路径
delete /路径名称
# 创建顺序节点,会生成一个节点为lock000000000
create -s /lock
# 创建临时节点节点,断开就自动删除
create -e /lock/info
# 删除该节点所有节点
rmr /节点名称