一篇文章带你走近zookeeper

1.zk是什么

zk是apache下的一个开源项目,官方介绍为:"zooKeeper是一种集中式服务,用于维护配置信息,命名,提供分布式同步和提供组服务"。

 

2.zk中的角色

2.1 Leader

zk集群中同一个时间只有一个leader,它会和各个follower和observer发起进行心跳,来保证集群中各个节点是否是可用的。进行写操作,并且将写操作的结果同步到各个follower和observer上。

2.2 Follower

zk集群中可能会有多个follower,它会响应leader的心跳。对客户端提供读数据的功能,将写请求转发给leader,并且参与写请求的半数投票,在leader挂掉后,可以参与新的leader的选举。

2.3 Observer

和follower类似,但是没有投票权,增加集群读数据的能力。

image.png

 

3.原子广播(ZAB协议)

为了保证写操作的一致性,zk专门设计了一种名为原子广播(ZAB)的支持崩溃恢复的协议。基于该协议,zk实现了一种主从模式的系统架构来保持集群中各个副本的数据一致性。

ZAB协议中,所有的写操作都由leader完成,leader写入本地日志后,再将数据同步给follower和observer。一旦leader无法工作,ZAB协议会自动从follower中选举一个节点成为新的leader(即领导选举),重新对外提供服务。

3.1 写操作

image.png

 

 

如图所示,写操作会有5步

1.客户端向leader发起写请求,也可以将follower和observer发起写请求,follower和observer会将写请求转发给leader

2.leader将写请求以proposal的形式发送给各个follower,并等待ACK

3.follower收到leader的propose请求后,返回ACK给leader

4.leader得到半数的ACK后(这里的ACK其实就是投票,半数指的是(leader+follower)/2,leader默认会给自己投一票),向所有的follower和observer发送commit,将写数据同步给各个节点。

5.从接受client请求的那个节点把response返回给客户端

3.2 读操作

leader,follower,observer都可直接处理读请求,从本地内存读取数据并返回给客户端

image.png

由于读请求不需要节点之间的交互,所以follower,observer越多,整体可处理的读请求越大,即读性能越好。

 

4.FastLeaderElection(快速领导选举算法)

当集群中的leader挂掉时,需要重新选举一个follower作为leader,这时就需要使用fastleaderElection来进行领导的选举了。

4.1 myId

每个zk节点,都需要在数据文件下创建一个myid文件,该文件包含整个集群唯一的Id(整数)。例如,某ZooKeeper集群包含三个节点,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其ID与hostname必须一一对应,如下所示。在该配置文件中,server.后面的数据即为myid

server.1=zoo1:2888:3888

server.2=zoo2:2888:3888

server.3=zoo3:2888:3888

4.2 zxid

类似于RDBMS中的事务ID,用于标识一次更新操作的proposal ID,为了保证顺序性,zxid单调递增。zk使用一个64数来表示zxid,高32位为leader的epoch(用来表示这是第几次的选举),从1开始,每次选举出一个新的leader,epoch加1。低32位为该epoch的序列号,每次epoch变化,都将低32位进行重置,这样保证了zxid的全局递增性。高32位越大,说明参与成功选举的次数越多。低32位越大,说明在当前epoch内,收到leader的proposal(写提议)的次数越多。

4.3 节点状态

Leading: 领导者状态,表示当前节点是leader节点,它会维护follower,obsever之间的心跳

Following: 跟随者状态,表示当前节点是follower,它知道当前集群内leader是谁

Observing: 观察者状态,表示当前节点是obsderver,和following相似,只是它不参与选举投票和写操作投票

Looking: 不确定leader状态,该状态下认为当前集群内没有leader,会发起一次leader选举投票

4.4 选票的数据结构

每个节点进行领导选举时,会发送如下关键信息:

logicClock: 每个节点会维护一个自增的整数,名为logicClock,它表示这是当前节点时第多少轮发起的投票选举。

state: 当前节点的状态

self_id: 当前节点的myid

self_zxid: 当前节点上所保存的最大的zxid

vote_id: 当前节点投票的节点的myid

vote_zxid: 当前节点投票的节点的最大的zxid

4.5 投票流程

自增选举轮次:zk规定所有投票都必须在同一轮次中,每个节点在开始新一轮的投票时,都会对logicClock进行自增操作。

初始化选票:每个节点在广播自己的选票前,会将自己的投票箱清空,该投票箱记录了由其他节点广播来的选票。例:节点2投票给节点3,节点3投票给节点1,则节点1的投票箱为(2, 3), (3, 1), (1, 1)。票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,则其它节点收到该新选票后会在自己票箱中更新该节点的选票。

发送初始化选票:每个节点开始都是通过广播把票投给自己,初始化投票投的都是自己。如节点1的vote_id就是self_id,vote_zixd都是self_zxid。

接受外部选票:每个节点会尝试从其它节点获取选票,并记录到自己的投票箱内。如果无法获取到外部选票,则会确认自己是否和其他节点保持着有效连接。如果是,则重新发送自己的选票。如果否,则马上与之建立连接。

判断选举轮次:收到选举投票后,首先会根据投票信息中的logicClock来进行不同的处理

1.外部投票的logicClock大于自己的logicClock,说明当前节点投票轮次落后于其他节点的投票轮次,清空自己的投票箱,并且把自己的logicClock更新为外部投票的logicClock,然后再对比自己的投票和收到的投票,确定是否需要变更自己的投票,最终再将自己的投票广播出去。

2.外部投票的logicClock小于自己的logicClock,当前节点的轮次大于收到的外部选票,直接忽略。

3.外部投票和自己的logicClock相等,则进行选票PK

选票PK:选票PK是基于self_id,self_zxid和vote_id,vote_zxid的对比来进行的

1.外部投票的logicClock大于自己的logicClock,将自己的logicClock以及自己选票的logicClock更新为收到的外部投票的logicClock

2.若logicClock一致,则对比两个的vote_zxid,若外部投票的vote_zxid大,则将自己选票的vote_zxid和vote_id更新为外部投票的vote_zxid和vote_id并广播出去,另外将收到的票以及自己更新后的票放进自己的投票箱,如果箱内已存在已存在相同的选票(self_id,self_zxid),则直接覆盖

3.若两者的vote_zxid一致,则比较vote_id,若外部投票的vote_id比较大,则将自己票中的vote_id更新成外部投票的vote_id并广播出去,另外将收到的票以及自己更新过后的选票放进自己的投票箱(其实有点不公平,myid成为领导的可能性就越大)

统计选票:如果已经确定有过半节点投出了认可自己的投票(可能是更新后的投票),则中止投票,否则接受其他节点的投票,继续进行投票选举

更新节点状态:投票中止后,各个节点会更新自己的状态,若过半的节点都将票投给了自己,则将自己的状态改成leading,否则将自己的状态改成following。

 

5.选举场景

5.1 集群启动领导选举

初始投票投给自己:集群刚启动时,所有节点的logiticClock都为1,zxid都为0,各节点初始化后,都投票给自己,并将自己的投票放入自己的票箱,如下图所示。

在上图中,(1,1,0)第一位数字代表投出选票的节点的logicClock,第二位则是vote_id,第三位是vote_zxid,由于是初始步骤,vote_id其实就是self_id,vote_zxid就是self_zxid。此时,各个节点的票箱中,只有自己投给自己的一票。

更新选票:节点收到外部投票后,会进行选票PK,相应更新自己的选票并广播出去,并将合适的选票放入自己的票箱。

如上图所示,节点1收到节点2(1,2,0)和节点3(1,3,0)的选票后,由于logicClock都相等,所有vote_zxid也都相等,这时就判断vote_id的大小,节点3的选票(1,3,0)最大。这时将自己的选票更新为(1,3,0),并将自己的票箱清空,将(1,3),(3,3)投入自己的票箱内,然后将更新后的选票(1,3,0)广播出去。

同理,节点2接受节点3的选票后,将自己的投票更新为(1,3,0),清空自己的票箱,将(2,3),(3,3)投入自己的票箱内。

节点3根据上述规则,不需要更新自己的选票,自身的票箱内的选票依然是(3,3)

节点1和节点2将自己更新过后的选票广播出去之后每个节点内的投票箱内的情况都是一致的。(1,3),(2,3),(3,3)

根据选票确定角色:根据上述选票,三个节点一致认为节点3应该是leader,因为节点1和节点2进入following状态,节点3进入leading状态。之后leader(节点3)负责维护和follower(节点1,节点2)之间的心跳连接。

 

5.2 Follower重启选举

Follower重启投票给自己:Follower重启,或者发生网络分区后找不到leader,会进入looking状态并发起新一轮的选举

 

发现已有leader后成为follower:节点3收到节点1的投票后,将自己的状态Leading以及选票返回给节点1,节点2收到节点1的投票后,将自己的状态following以及选票返回给节点1。此时节点1知道节点3是leader,并且通过节点2和节点3返回的选票可以确定节点3确实得到了半数以上的投票,因此节点1进入following状态

5.3 Leader重启选举

Follower发起新投票:leader(节点3)宕机后,follower(节点1,节点2)长时间没有收到心跳连接的请求后,知道leader宕掉了,因此进入looking状态,发起新一轮的投票,并且都将票投给自己

广播更新选票:节点1和节点2各自收到对方的投票后,判断是否要更新自己的选票,这里有两种情况

1.节点1和节点2的zxid相同,例如在节点3宕机前节点1和节点2完全与之同步,此时比较myid即可,节点2的myid大于节点1,节点1会更新自己的选票为节点2的选票。

2.节点1和节点2的zxid不同,在旧leader宕机之前,其主导的写操作,只需要半数节点确认即可,而不需要所有节点确认。换句话说,节点1和节点2可能一个与旧leader同步(即zxid相同),另一个与旧leader不同步(即zxid比leader小)。此时选票的更新取决于谁的zxid大。

在上图中,节点1的zxid为11,节点2的zxid为10。因此,节点2更新自己的选票为(3,1,11),并将自己的投票箱清空,放入(1,1),(2,1)到自己的投票箱。

选出新leader:经过上述的选票后,节点1和节点2的选票都投给了节点1。因此,节点1进入leading状态,节点2进入following状态,节点1成为leader后维护与节点2的心跳连接

旧leader恢复后重新发起选举:旧的leader恢复后,进入looking状态并发起新一轮的选举,并将票投给自己。节点1会将自己的leading状态和选票(3,1,11)返回给节点3,节点2会将自己的following状态和选票(3,1,11)返回给节点3。

旧Leader成为Follower:节点3收到返回的选票后,确认节点1是leader,并且节点1确实收到了半数的投票。因此,自己进入following状态。

 

6.Commit机制

6.1 Commit过的数据不丢失

FailOver前状态:为更好演示leader failover过程,为更好演示Leader Failover过程,本例中共使用5个ZooKeeper节点。A作为Leader,共收到P1、P2、P3三条消息,并且Commit了1和2,且总体顺序为P1、P2、C1、P3、C2。根据顺序性原则,其它Follower收到的消息的顺序肯定与之相同。其中B与A完全同步,C收到P1、P2、C1,D收到P1、P2,E收到P1,如下图所示。

这里要注意:

由于A没有C3,意味着收到P3的节点的总个数不会超过一半,也即包含A在内的最多只有两个节点收到P3,其他节点均未收到P3

由于A已经写入了C1,C2。说明它已经commit了P1,P2,因此整个集群有超过一半的节点,即最少三个节点收到P1、P2。在这里所有节点都收到了P1,除E外其它节点也都收到了P2

选出新leader:旧Leader也即A宕机后,其它节点根据上述FastLeaderElection算法选出B作为新的Leader。C、D和E成为Follower且以B为Leader后,会主动将自己最大的zxid发送给B,B会将Follower的zxid与自身zxid间的所有被Commit过的消息同步给Follower,如下图所示。

在上图中

P1和P2都被A commit,因此B会通过同步保证P1,P2,C1,C2都存在与C,D,E中

P3由于未被A commit,同时P3未存在于大数据幸存的节点中,因此它不会被同步到其他follower中。

通知Follower可对外服务:同步完数据后,B会向C,D,E发送NEWLEADER命令并等待大多数节点的ACK(下图中D和E已返回ACK,加上B自身,已经占集群的大多数),然后向所有节点发送UPTODATE命令,收到该命令的节点即可对外提供服务。

 

6.2 未Commit过的消息对客户端不可见

在上例中,P3未被A commit,同时因为没有过半节点收到P3,因此B也未commit P3(如果有过半节点收到P3,即使A没有 commit P3,B也会commit P3,即C3),所以B不会将P3广播出去

具体做法是,B成为leader后,先判断自身未commit的消息(即P3)是否存在于大多数的其他节点中,从而决定是否要将其commit。然后B可得出自身所包含的commit过的消息中的最小zxid(记作min_zxid)与最大zxid(max_zxid),C,D,E会向B发送自身commit过的最大消息的zxid(记为max_zxid)以及未被commit过的所有消息(记为zxid_sets),B根据这些消息进行如下操作

1.如果follower的max_zxid和leader的max_zxid完全相同,说明该follower和leader完全同步,无须同步任何数据

2.如果follower的max_zxid在leader的(min_zxid,max_zxid)范围内,leader会通过TRANC命令通知follower将其zxid_sets中大于follower中max_zxid的所有消息全部删除

(未解决的疑惑,如果存在一个follower节点的max_zxid和leader相同,但是存在未commit的比max_zxid大的消息,这种情况下这个未commit的消息不会被删除掉)

上述操作保证了未被Commit过的消息不会被Commit从而对外不可见。

 

上述例子中Follower上并不存在未被Commit的消息。但可考虑这种情况,如果将上述例子中的节点数量从五增加到七,节点F包含P1、P2、C1、P3,节点G包含P1、P2。此时节点F、A和B都包含P3,但是因为票数未过半,因此B作为Leader不会Commit P3,而会通过TRUNC命令通知F删除P3。如下图所示。

 

7.节点类型

zk提供了一个类似于linux文件系统的树形结构,该树形内每个节点被称为znode,可按照如下两个维度分类。

从节点创建后保存的角度

  • Persist节点:一旦被创建,便不会意外丢失,即使服务器全部重启也依然存在。每个Persist节点即可包含数据,也可包含子节点。
  • Ephemeral节点:在创建它的客户端和服务器的session结束时自动删除。服务器重启会导致session结束,对应的Ephemeral节点也会被自动删除。

从节点是否重复的角度

  • Non-sequence节点:多个客户端端同时创建同一个Non-sequence节点时,只有一个可创建成功,其他均失败。并且创建出来的节点名称和创建时指定的节点名称完全一致。
  • Sequence节点:创建出来的节点名称在指定的名称后带有10位10进制的序号。多个客户端创建同一节点时,都能创建成功,只是序号不一样。

 

8.语义保证

ZooKeeper简单高效,同时提供如下语义保证,从而使得我们可以利用这些特性提供复杂的服务。

顺序性:客户端发起的更新会按发送顺序被应用到 ZooKeeper 上

原子性:更新操作要么成功要么失败,不会出现中间状态

单一系统镜像:一个客户端无论连接到哪一个服务器都能看到完全一样的系统镜像(即完全一样的树形结构)。注:根据上文《ZooKeeper架构及FastLeaderElection机制》介绍的 ZAB 协议,写操作并不保证更新被所有的 Follower 立即确认,因此通过部分 Follower 读取数据并不能保证读到最新的数据,而部分 Follwer 及 Leader 可读到最新数据。如果一定要保证单一系统镜像,可在读操作前使用 sync 方法。

可靠性:一个更新操作一旦被接受即不会意外丢失,除非被其它更新操作覆盖

最终一致性:写操作最终(而非立即)会对客户端可见

 

9.Watch机制

对于zk的读操作而言,都可附带一个Watch,一旦相应的数据发生变法,则该watch被触发,类似于监听者机制。

Watch的特性

主动推送:Watch被触发时,由 zk服务器主动将更新推送给客户端,而不需要客户端轮询。

一次性:数据变化时,Watch 只会被触发一次。如果客户端想得到后续更新的通知,必须要在 Watch 被触发后重新注册一个 Watch。

可见性:如果一个客户端在读请求中附带 Watch,Watch 被触发的同时再次读取数据,客户端在得到 Watch 消息之前肯定不可能看到更新后的数据。换句话说,更新通知先于更新结果。

顺序性:如果多个更新触发了多个 Watch ,那 Watch 被触发的顺序与更新顺序一致。

 

10.分布式锁机制

10.1 最多一个获取锁

对于分布式锁(这里特指排它锁)而言,任意时刻,最多只有一个进程(对于单进程内的锁而言是单线程)可以获得锁。

10.2 锁重入

对于分布式锁,需要保证获得锁的进程在释放锁之前可再次获得锁,即锁的可重入性。

10.3 释放锁

锁的获得者应该能够正确释放已经获得的锁,并且当获得锁的进程宕机时,锁应该自动释放,从而使得其它竞争方可以获得该锁,从而避免出现死锁的状态。

10.4 感知锁释放

当获得锁的一方释放锁时,其它对于锁的竞争方需要能够感知到锁的释放,并再次尝试获取锁。

 

11.zk实现分布式锁

11.1 非公平锁

  • 最多一个获取锁:假设有三个zk的客户端,同时获取锁资源。这三个客户端同时向zk集群注册Ephemeral且Non-sequence类型的节点,路径都为 /zkroot/leader(工程实践中,路径名可自定义)。由于是Non-sequence节点,这三个客户端只会有一个创建成功,其它客户端均创建失败。此时,创建成功的客户端获取到锁 ,创建失败的客户端等待锁。
  • 锁重入:创建节点成功的客户端获取到锁,只需要判断创建的节点是否由本身客户端创建即可。
  • 释放锁:如果客户端打算主动释放锁,直接删除 /zkroot/leader 节点即可。如果客户端进程意外宕机,其与 ZooKeeper 间的 Session 也结束,该节点由于是Ephemeral类型的节点,因此也会自动被删除。此时 /zkroot/leader 节点不复存在,对于其它参与竞争的客户端而言,可以重新参与锁的竞争。
  • 感知锁释放:创建节点失败的客户端,会向 /zkroot/leader 注册一个 Watch ,一旦锁释放,也即该节点被删除,所有的创建失败的客户端会收到通知。
  • 重新竞争锁:感知到锁释放后,所有的客户端重新进行锁的竞争,即创建/zkroot/leader节点。新一轮的锁竞争方式和第一次一样,都是发起节点创建请求,创建成功即获取到锁,否则为等待锁,且会Watch该节点。新一轮的选举结果,无法预测,与它们在第一轮选举中的顺序无关。这也是该方案被称为非公平模式的原因

总结:

  1. 非公平模式实现简单,每一轮竞争方法都完全一样
  2. 竞争参与方不多的情况下,效率高。每个客户端通过 Watch 感知到节点被删除的时间不完全一样,只要有一个 客户端得到通知即可竞争锁。
  3. 给zk集群造成的负载大,因此扩展性差。如果有上万个客户端都参与竞争,意味着同时会有上万个写请求发送给 Zookeper。如《ZooKeeper架构》一文所述,ZooKeeper 存在单点写的问题,写性能不高。同时一旦客户端放释放锁,ZooKeeper 需要同时通知上万个其他客户端,负载较大。

11.2 公平锁

  • 最多一个获取锁:公平锁竞争中,各客户端均创建 /zkroot/leader 节点,且其类型为Ephemeral与Sequence。由于是Sequence类型节点,故上图中三个客户端均创建成功,只是序号不一样。此时,每个客户端都会判断自己创建成功的节点的序号是不是当前最小的。如果是,则该客户端获取到锁,否则等待锁。
  • 锁重入:判断当前客户端创建的节点序号是否是最小的即可。
  • 释放锁:客户端如果需要释放锁,直接删除其创建的节点即可。如果客户端所在进程意外宕机,其与 ZooKeeper 间的 Session 结束,由于其创建的节点为Ephemeral类型,故该节点自动被删除。
  • 感知锁释放:与非公平模式不同,每个客户端并非都 Watch 由获取资源客户端创建出来的节点,而是 Watch 序号刚好比自己序号小的节点。如果有 1、2、3 共三个节点,Client1获取到锁,创建节点 /zkroot/leader1。因此Client 2 Watch /zkroot/leader1,Client 3 Watch /zkroot/leader2。(注:序号应该是10位数字,而非一位数字,这里为了方便,以一位数字代替)。一旦 /zkroot/leader1 被删除,Client 2可得到通知。此时Client 3由于 Watch 的是 /zkroot/leader2 ,故不会得到通知。
  • 重新竞争锁:Client 2得到 /zkroot/leader1 被删除的通知后,不会立即成为新的 Leader 。而是先判断自己的序号 2 是不是当前最小的序号。在该场景下,其序号确为最小。因此Client 2成为新的 Leader。这里要注意,如果在Client 1释放锁之前,Client 2就宕机了,Client 3会收到通知。此时Client 3不会立即获取到锁,而是要先判断自己的序号 3 是否为当前最小序号。很显然,由于Client 1创建的 /zkroot/leader1 还在,因此Client 3不会获取到锁,并向Client 2序号 2 前面的序号,也即 1 创建 Watch。

总结:

  1. 实现相对复杂。
  2. 扩展性好,每个客户端都只 Watch 一个节点且每次节点被删除只须通知一个客户端。
  3. 客户端释放锁时,其它客户端根据竞争的先后顺序(也即节点序号)依次获取到锁,这也是公平模式的由来。
  4. 延迟相对非公平模式要高,因为它必须等待特定节点得到通知才能获取到锁。

 

12 应用场景 

12.1 分布式协调

A系统发送消息到mq,B系统从mq中获取消息进行处理。那A系统如何知道B系统的处理结果呢?通过zk就是可以分布式系统之前的协调工作,A系统发送完消息到mq中,就可以在zk上对某个节点的值设置一个监听器,一旦B系统将消息消费成功之后,就修改节点的值,A系统马上就知道B系统是否消费成功了。

image.png

 

例如: 订单中心创建订单的时候,需要进行库存的锁定。订单中心创建完订单,就可以向mq发送一条消息,告诉商品中心需要进行库存的锁定。订单中心在zk中创建一个带有orderId的node,并注册监听。商品中心在收到mq的消息,锁定库存成功之后,就修改对应的orderId的node的值,订单中心就知道商品中心的库存锁定成功了。

12.2 分布式锁

见11章

12.3 元数据/配置信息管理

zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper 么?

12.4 HA高可用性

这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。


本文参考:

1.郭俊前辈的文章《实例详解ZooKeeper ZAB协议、分布式锁与领导选举》https://dbaplus.cn/news-141-1875-1.html

2.Github博主yanglbme 的advanced-java项目中的文章Zookeeper 都有哪些应用场景?》 https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/zookeeper-application-scenarios.md

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值