浅谈Zookeeper

51 阅读10分钟

一、Zookeepr数据模型

zookeeper数据存储结构与标准的Lunix文件系统非常相似,都是在根节点下挂很多子节点(树形)。但是zookeeper中没有文件系统中目录与文件的概念,而是使用了znode作为数据节点。znode是zookeeper中的最小数据单元,每个znode上都可以保证数据,同时还可以挂载子节点,形成一个树形化命名空间。

image.png

每个znode都有自己所属的节点类型节点状态。其中节点类型可以分为持久节点、持久顺序节点、临时节点和临时顺序节点。

持久节点:一旦创建就一直存在,直到将其删除。

持久顺序节点:一个父节点可以为其子节点维护一个创建的先后顺序,这个顺序提现在节点名称上,是节点名称后自动添加一个由10位数字组成的数字串,从0开始计数。

临时节点:临时节点的生命周期是与客户端会话绑定的,会话消失则节点消失。临时节点只能做叶子节点,不能创建子节点。

临时顺序节点:父节点可以创建一个维持了顺序的临时节点(和持久顺序节点类似)。

节点状态包含了很多节点的属性,比如:czxid、mzxid等等,在zookeeper中使用stat这个类来维护的。

czxid:create zxid,该数据节点被创建时的事务ID。

mzxid:modifed zxid,节点最后一次被更新时的事务ID。

ctime:create time,该节点被创建的时间。

mtime:modified time,该节点最后一次被修改的时间。

version:节点的版本号。

cversion:子节点的版本号。

aversion:节点的ACL版本号。

ephemeralOwner:创建该节点的会话的sessionID,如果该节点为持久节点,该值为0。

dataLength:节点数据内容的长度。

numChildre:该节点的子节点个数,如果为临时节点为0。

pzxid:该节点子节点列表最后一次被修改时的事务ID,注意是子节点的列表,不是内容。

二、会话

zookeeper客户端和服务端是通过TCP长连接维持的会话机制,其实对于会话来说,可以简单理解为保持连接状态

在zookeeper中,会话还有对应的事件,比如:CONNECTION_LOSS 连接丢失事件、SESSION_MOVED 会话转移事件、SESSION_EXPIRED 会话超时失效事件。

三、ACL

ACL为access control lists,它是一种权限控制。在zookeeper中定义了5种权限,它们分别为:

create:创建子节点的权限。

read:获取节点数据和子节点列表的权限。

write:更新节点数据的权限。

delete:删除子节点的权限。

admin:设置节点acl的权限。

四、Watcher机制

Watcher为事件监听器,是zookeeper非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式:

1、client(客户端)向Server(服务端)注册指定的watcher;

2、当服务端符合了watcher的某些事件或要求则会向客户端发送事件通知;

3、客户端收到通知后找到自己定义的watcher然后执行相应的回调方法。

image.png

五、ZAB协议(ZooKeeper Atomic Broadcast)

1、ZAB中的三个角色

ZAB中的三个主要的角色:leader 领导者、follower 跟随者、observer 观察者。

leader:集群中唯一的写请求处理者,能够发起投票。

follower:客户端读请求,自己处理,如果是写请求则要转发给leader。在选举过程中会参与投票,有选举权和被选举权。

observer:没有选举权和被选举权的follower。

在ZAB协议中对zkServer还有两种模式的定义,分别是消息广播崩溃恢复

2、消息广播模式

如何在整个集群中保持数据的一致性 需要leader将写请求广播出去,让leader询问followers是否同意更新,如果超过半数以上的同意那就进行follower和observer的更新。

image.png

1、leader通过followerqueue发送写请求到follower(提案);

2、半数以上的follower返回是否同意本次请求结果于leader;

3、同意,则leader发送commit请求给follower。

4、同意,则leader通过observerqueues 发送commit请求给observer。

为啥需要两个queue?

ZAB需要让follower和observer保证顺序性,比如:现有一个写请求A,此时leader将请求A广播出去,因为只需要半数同意,所以可能这个时候,有一个follower F1因为网络原因没有收到,而leader又广播了一个请求B,因为网络原因,F1竟然先收到请求B,然后才收到请求A,这个时候请求处理顺序不同就会导致数据的不同,从而产生数据不一致性问题。

所以在Leader端,它为每个其它的zkServer准备了一个队列,采用先进先出的方式发送消息。由于协议是通过TCP协议来进行网络通信的,保证了消息的发送顺序性,接收顺序性也得到了保证。

除此之外,在ZAB中还定义了一个全局单调递增的事务ID ZXID,它是一个64位long型,其中高32位表示epoch年代,低32位表示事务Id。

epoch是会根据leader的变化而变化的,当一个leader挂了,新的leader上位的时候,epoch年代就变了,二低32位可以简单理解为递增的事务Id。

定义这个的原因也是为了顺序性,每个proposal(提议)在leader中生成后需要通过其ZXID来进行排序,才能得到处理。

3、崩溃恢复模式

当leader出现崩溃,则需要重新选举leader。

leader选举可以分为两个不同阶段,第一个是我们提到的leader宕机,第二个则是zookeeper启动时需要进行系统leader初始化选举。

初始化选举:比如,三台机器,启动server1时,它首先投票给自己,投票内容为服务器的myid和ZXID(全局单调递增事务ID),因为初始化,所以ZXID都为0,此时server1发出的投票为(1,0)。但此时server1的投票仅为1,所以不能作为leader,此时还在选举阶段,所以整个集群处于LOOKING状态。

接着server2启动,它首先选举自己(2,0),并将投票信息广播出去,server1收到server2的投票信息后会将投票信息与自己作比较。首先它会比较ZXID,ZXID大的优先为Leader,如果相同则比较myID,myID大的优先作为leader

所以此时server1发现server2更适合做leader,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后server2收到之后,发现和自己的一样无需做更改,并且自己的投票已经超过半数,则确定server2位leader,server1也会将自己服务器设置为following变成follower。整个服务器从LOOKING变为了正常状态。

当server3启动发现集群没有处于LOOKING状态时,它直接以Follower的身份加入集群。

leader宕机:在整个集群运行的过程中server2挂了,进行重新选举,首先两个follower会将自己的状态从following变为LOOKING状态,然后每个server会向初始化投票一样首先给自己投票,不过这里的ZXID可能不是0了。

假设server1给自己投票(1,99),然后广播给其它server,server3首先也会给自己投票(3,95),然后广播给其它server。这个时候server1收到server3的投票发现没自己合适故不变,server3收到server1的投票信息结果发现server1更适合,则把自己的投票改成(1,99),然后广播出去,最后server1变成leader,server3变成follower。

崩溃恢复:当集群中有机器挂了,我们整个集群如何保证数据一致性?

1、如果只是Follower挂了,而且挂的没超过半数的时候,因为我们一开始讲了Leader中会维护队列,所以不用担心后面的数据没有接收到导致数据不一致性。

2、如果Leader挂了那就比较麻烦啦,我们肯定需要先暂停服务变为LOOKING状态,然后进行Leader的重新选举,但是这个要分两种情况,分别是确保已经被leader提交的提案最终能够被所有的follower提交跳过那些已经被丢弃的提案

a、确保已经被Leader提交的提案最终能够被所有的Follower提交是什么意思呢? 假设leader(server2)发送commit请求,它发送给server3,然后要发给server1的时候突然挂了。这个时候重新选举的时候我们如果把server1作为leader的话,那么肯定会产生数据不一致性,因为server3肯定提交刚刚server2发送的commit请求的提案,而server1根本没收到,所以会丢弃。

b、跳过那些已经被丢弃的提案是什么意思? 假设leader(server2)此时同意了提案N1,自身提交了这个事务并且发送给所有follower要commit的请求,却在这个时候挂了,此时肯定要重新进行leader的选举,比如:此时选server1为leader,但是过了一会,这个挂掉的leader又重新恢复了,此时它作为follower的身份进入集群中,需要注意的是刚刚server2已经同意提交了提案N1,但其它server并没有收到它的commit信息,所以其它server不可能再提交这个提案N1了,这样就会出现数据不一致性问题,所以该提案N1最终需要被抛弃掉。

六、zookeeper分布式锁与redis分布式锁

1、zookeeper分布式锁

image.png zookeeper加锁步骤:

1、现如今有两个客户端A与B,客户端A,B依次在zookeeper中创建临时顺序节点。

2、判断能否加锁,比如:判断clientA的节点是否是第一个,是则加锁成功,不是则给上一个节点添加监听器,监听上一个节点变化。

3、clientA,释放锁,删除顺序节点;

4、zk通知clientB监听器:ClientA的节点删除啦!

5、监听到事件,触发再次给clientB加锁操作。

6、加锁成功,执行逻辑。

原理:

1、多个客户端都是一上来直接创建一个锁节点下的一个接一个的临时顺序节点。

2、如果自己不是第一个节点,就对自己上一个节点加监听器。

3、只要上一个节点释放锁,自己就会去获取锁(相当于排队机制,队列机制),用临时节点的另外一个用意是:如果某个客户端创建临时顺序及下单之后,不小心自己宕机啦,zk感知到那个客户端宕机啦,就会删除临时节点,相当于释放锁。

2、redis分布式锁

image.png

步骤:

1、加锁机制:客户端面对的是一个redis集群,它需要根据hash节点去选择一台机器,然后发送一个lua脚本(可以保证原子性,有三个重要的参数:加锁的key,key存活时间[默认30s],加锁的客户端ID,还有两个判断,第一个判断:判断加锁key是否存在,不存在则加锁; 第二个判断:判断key的hash数据结构中是否存在目前客户端的ID,如果不存在,则会获取这个key的剩余时间,此时客户端会进入while循环,直到加锁成功)。

2、只要客户端加锁成功,就会启动watch dog机制,它是一个后台线程,会每隔10s检查一下,如果客户端还持有锁,则延长时间。

3、可重入加锁机制:方法内有多把锁,会对key的hash数据结构中Id对应着加锁的次数。

4、释放锁的机制:如果发现加锁次数是0,就删除key,其它客户端就可以进行加锁了。

缺陷:如果master宕机了,因为主从复制,可能导致多个客户端同时完成加锁,导致各种脏数据出现。

引用:

1、javaguide.cn/distributed…

2、blog.51cto.com/u_15346267/…