一、概述
特点
- 一个leader多个fllower组成的集群
- 集群中只要有半数以上节点存货,zk集群就能正常服务。所以zk适合安装奇数台服务器
- 全局数据一致性,每隔server保存一份相同的数据副本,client无论连接到哪个Server,数据都是一致的
- 跟新请求顺序执行,来自同一个client的更新请求按其发送顺序一次执行
- 数据更新原子性,一次数据更新要么成功要么失败
- 实时性:在一定时间范围内,client能读取到i最新的数据
数据结构
zookeeper数据结构与unix文件系统很类似,整体上可以看作是一棵树,每隔节点称作一个ZNode。每个ZNode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识。
应用场景
- 统一命名服务:在分布式环境下,经常需要对应用/服务进行统一命名,便于识别‘
- 统一配置管理:在分布式环境下,配置文件同步非常常见
- 统一集群管理:分布式环境中,实时掌握每个节点的状态是必要的;zk可以实现实时监控节点状态变化
- 服务器动态上下线:客户端实时洞察到服务器上下线的变化
- 软负载均衡:zk中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
CAP和BASE理论
一个分布式系统必然会存在一个问题:因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡。这就是著名的CAP定理。
CAP理论中,P(分区容忍性)是必然要满足的,因为毕竟是分布式,不能把所有的应用全放到一个服务器里面,这样服务器是吃不消的。所以,只能从AP(可用性)和CP(一致性)中找平衡。Eureka保证了AP,zookeeper保证了CP。
BASE理论:即使无法做到强一致性,但分布式系统可以根据自己的业务特点,采用适当的方式来使系统达到最终的一致性。BASE理论由:Basically Avaliable
基本可用、Soft state
软状态、Eventually consistent
最终一致性组成。
- 基本可用(Basically Available):基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。例如,电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层在该页面只提供降级服务。
- 软状态(Soft State):软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有多个副本,允许不同节点间副本同步的延时就是软状态的体现。
- 最终一致性(Eventual Consistency):最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
ACID是传统数据库常用的设计理念,追求强一致性模型。BASE支持的是大型分布式系统,通过牺牲强一致性获得高可用性。实现BASE理论的算法由2PC、3PC、Paxos、Raft、ZAB,解决的问题全部都是:在分布式环境下,怎么让系统尽可能的高可用,而且数据最终能达到一致。
二、安装
启动和退出
/bin/zkServer.sh start|stop|status|start-foreground // 服务端管理
/bin/zkCli.sh // 客户端连接
配置
tickTime=2000; 通信心跳时间,zk服务器与客户端心跳时间,单位毫秒
initLimit=10; Leader和Follower之间初始连接时能容忍的最多心跳数量。
syncLimit=5; Leader和Follower之间通信时间如果超过syncLimit*tickTime,则表示掉线,从服务器列表中删除Follower
dataDir=/opt/zk/;数据快照保存目录
clientPort=2181; 客户端连接端口,通常不修改
三、集群
配置
配置文件新增
#server.1=zookeeper1:2888:3888
#server.2=zookeeper2:2888:3888
#server.3=zookeeper3:2888:3888
其中1|2|3表示集群中服务的唯一ID
zookeeper1|2|3是host,也是IP地址
2888是LF之间通信的端口
3888是Leader挂了之后重新选举的端口
选举机制
- SID:服务器ID,用来唯一标识一台zk集群中的机器,和myid一致
- ZXID:事务ID。ZXID用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这和zk服务器对于客户端更新请求的处理逻辑有关。
- Epoch:每个Leader任期的代号。
服务器选举状态
- LOOKING 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
- **FOLLOWING **跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。
- LEADING 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳。
- **OBSERVING **观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。
首次选举
- 服务器1启动,发起一次选举。服务器1投自己一票,此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持未LOOKING
- 服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息;次数服务器1发现服务器2的myid比自己目前投票选举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0,服务器2票数2,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING
- 服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数超过半数,当选leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING
- 服务器4启动,发起一次选举。次数服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING
- 服务器5启动,同4一样更改状态为FOLLOWING
非首次选举
当zk集群中的一台服务器出现一下两种情况之一时,就会开始进入Leader选举:
- 服务器初始化启动
- 服务器运行期间无法和leader保持连接
当一台机器进入Leader选举流程时,当前集群也可能会处于一下两种状态:
- 集群中本来就存在一个leader
- 集群中确实不存在leader
假设zk集群由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器时Leader。某一时刻,3和5服务器出现故障,因此开始进行Leader选举。此时服务器124的(epoch, zxid, sid)分别为(1,8,1)(1,8,2)(1,7,4)。选举规则:
- epoch大的直接胜出
- epoch相同,sxid大的胜出
- sxid相同,sid大的胜出
四、操作命令
节点
节点类型
- 持久化节点PERSISTENT:客户端与zookeeper断开连接后,该节点依旧存在。
- 持久化顺序编号节点PERSISTENT_SEQUENTIAL:客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号。
- 临时节点EPHEMERAL:客户端与zookeeper断开连接后,该节点被删除。
- 临时顺序编号节点EPHEMERAL_SEQUENTIAL:客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号。
节点信息
znode 状态信息 | 解释 |
---|---|
cZxid | create ZXID,即该数据节点被创建时的事务 id |
ctime | create time,znode 被创建的毫秒数(从1970 年开始) |
mZxid | modified ZXID,znode 最后更新的事务 id |
mtime | modified time,znode 最后修改的毫秒数(从1970 年开始) |
pZxid | znode 最后更新子节点列表的事务 id,只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新 |
cversion | znode 子节点变化号,znode 子节点修改次数,子节点每次变化时值增加 1 |
dataVersion | znode 数据变化号,节点创建时为 0,每更新一次节点内容(不管内容有无变化)该版本号的值增加 1 |
aclVersion | znode 访问控制列表(ACL )版本号,表示该节点 ACL 信息变更次数 |
ephemeralOwner | 如果是临时节点,这个是 znode 拥有者的 sessionid。如果不是临时节,则 ephemeralOwner=0 |
dataLength | znode 的数据长度 |
numChildren | znode 子节点数量 |
节点操作
ls /zk 查看节点的子节点
ls2 /zk 查看节点的子节点和该节点的详细信息
get /zk 查询该节点的数据
create /zk/server-1 "s1" 创建节点
create -s /zk/server-2 "s2" 创建带序号的节点
create -e /zk/server-3 "s3" 创建临时节点
set /zk/server-1 "s0" 修改节点的数据
stat /zk 查看节点的状态
rmr /zk/server-3 删除某个节点
delete /zk/server-2 删除某个子节点
监听
Watcher 监听机制是 Zookeeper 中非常重要的特性,我们基于 Zookeeper上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 Zookeeper 实现分布式锁、集群管理等多种功能,它有点类似于订阅的方式,即客户端向服务端 注册 指定的 watcher ,当服务端符合了 watcher 的某些事件或要求则会 向客户端发送事件通知 ,客户端收到通知后找到自己定义的 Watcher 然后 执行相应的回调方法 。
当客户端在Zookeeper上某个节点绑定监听事件后,如果该事件被触发,Zookeeper会通过回调函数的方式通知客户端,但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 Watcher 的客户端不会再次收到消息(Watcher是一次性的操作),可以通过循环监听去达到永久监听效果。
Watcher机制可以分为三个过程:
- 客户端注册Watcher,注册watcher有三种方式:getData、exists、getChildren
- 服务器处理Watcher
- 客户端回调Watcher客户端
写数据原理
每个follower节点都会有一个先进先出的队列用来存放收到的事务请求,保证执行事务的顺序。所以:
- 可靠提交由ZAB的事务一致性协议保证
- 全局有序由TCP协议保证
- 因果有序由follower的历史队列保证
通过Leader写数据
- leader从客户端收到一个写请求
- leader生成一个新的事务并为这个事务生成一个唯一的ZXID
- leader将这个事务发送给所有的follows节点,将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower。
- follower节点将收到的事务请求加入到历史队列(history queue)中,当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK
- 当leader收到大多数follower(超过一半)的ack消息,leader会向follower发送commit请求(leader自身也要提交这个事务)
- 当follower收到commit请求时,会判断该事务的ZXID是不是比历史队列中的任何事务的ZXID都小,如果是则提交事务,如果不是则等待比它更小的事务的commit(保证顺序性)
- Leader将处理结果返回给客户端
注意:
- leader并不需要得到observer的ack,即observer无投票权
- leader不需要得到所有follower的ack,只要收到过半的ack即可,同时leader本身对自己有一个ack
- observer虽然无投票权,但仍需同步leader的数据从而在处理读请求时可以返回尽可能新的数据
通过follower/observer写数据
follower/observer接受写请求以后,不能直接处理,需要将写请求转发到leader处理
- Follower/Observer接受写请求以后,不能直接处理,而需要将写请求转发给Leader处理
- 除了多了一步请求转发,其它流程与直接写Leader无任何区别
- Leader处理写请求是通过上面的消息广播模式,实质上最后所有的zkServer都要执行写操作,这样数据才会一致
五、应用
分布式锁
a、基于临时节点方案
- AB等多线程都创建/lock节点
- 如果A创建成功,则A持有锁
- B创建节点失败阻塞,同事对/lock节点设置监听
- A线程执行完操作,删除/lock节点释放锁
- B线程此时收到监听回调,解除阻塞,重新去创建/lock节点获取锁
缺点:当线程数庞大时会发生"惊群"现象,zk节点可能会运行缓慢甚至宕机。每次的争抢锁都会消耗资源,且性能大打折扣
b、基于临时顺序节点方案
- AB等多线程在/locks路径下创建一个带序号的临时节点
- 判断自己创建的节点是不是/locks路径下序号最小的节点
- 如果是,则获取锁
- 如果不是,则监听自己的前一个节点
- 获取锁的线程处理完业务逻辑,删除自己创建的节点
- 监听它的后一个节点收到通知后循环步骤5
服务器动态上下线
- 服务器上线后去注册信息(创建的都是临时节点)
- 客户端获取到当前在线服务器列表并注册监听(监听父级节点子节点变动)
- 服务器节点下载(临时节点被删除)
- 服务器上下线回调客户端通知