[关闭]
@Catyee 2020-10-08T08:46:49.000000Z 字数 8085 阅读 356

分布式专题之Zookeeper

面试


一、节点类型:

持久化节点
顺序持久化节点
临时节点(会话级别,会话结束节点结束)
顺序临时节点
容器节点(最后一个子节点被删除(意味着一开始添加过子节点,如果从来没添加过子节点也不会删除),容器节点自动删除(有延迟))
TTL节点:指定时间没有操作就会被删除(要开启ttl功能)
TTL顺序节点:

xid用于记录请求发起的先后序号,用于确定单个客户端请求的响应顺序。Type代表请求的操作类型,常见的包括创建节点(OpCode.create)、删除节点(OpCode.delete)和获取节点数据(OpCode.getData)等。

二、客户端如何建立与保持服务端的连接

首先与ZooKeeper服务器建立连接,有两层连接要建立:

建立TCP连接之后,客户端发送ConnectRequest请求,申请建立session关联,此时服务器端会为该客户端分配sessionId和密码,同时开启对该session是否超时的检测。

当在sessionTimeout时间内,即还未超时,此时TCP连接断开:服务器端仍然认为该sessionId处于存活状态。此时,客户端会选择下一个ZooKeeper服务器地址进行TCP连接建立,TCP连接建立完成后,拿着之前的sessionId和密码发送ConnectRequest请求,如果还未到该sessionId的超时时间,则表示自动重连成功,对客户端用户是透明的,一切都在背后默默执行,ZooKeeper对象是有效的。

如果重新建立TCP连接后,已经达到该sessionId的超时时间了(服务器端就会清理与该sessionId相关的数据):则返回给客户端的sessionTimeout时间为0,sessionid为0,密码为空字节数组。客户端接收到该数据后,会判断协商后的sessionTimeout时间是否小于等于0,如果小于等于0,则使用eventThread线程先发出一个KeeperState.Expired事件,通知相应的Watcher,然后结束EventThread线程的循环,开始走向结束。此时ZooKeeper对象就是无效的了,必须要重新new一个新的ZooKeeper对象,分配新的sessionId了。

客户端
客户端维护了两个守护线程,一个事件线程即EventThread,一个发送和接收socket数据的线程即SendThread。建立连接时SendThread来完成的:
在SendThread的run方法中,有一个while循环,不断的做着以下几件事:

事件线程EventThread呢就是从一个事件队列(waitingEvents)中不断取出事件并进行处理,一种就是我们注册的watch事件,另一种就是处理异步回调函数:

服务端:
服务器端默认采用NIOServerCnxnFactory来负责socket的处理。每来一个客户端socket请求,为该客户端创建一个NIOServerCnxn。之后与该客户端的交互,就交给了NIOServerCnxn来处理。对于客户端的ConnectRequest请求,处理如下:
首先反序列化出ConnectRequest,然后开始协商sessionTimeout时间。协商完成之后,根据用户传递过来的sessionId是否是0进行不同的处理,sessionId为0,则代表着要创建session。sessionId不为0,则需要对该sessionId进行合法性检查,以及是否已经过期了的检查。如果是0,使用sessionTracker根据sessionTimeout时间创建一个新的session,交给请求链预处理、持久化和提交(顺便提一下,如果是集群版,这里session_id是根据当前时间和集器的sid生成的,生成之后同一机器递增)1. 这里的提交其实就是把Session的ID和Timeout存到一个叫sessionsWithTimeout的Map中去。之后会进行session的过期时间的检查,用了一种时间轮的思想。

二、Watch机制

老版本都是一次性watcher,新版本增加了持久性的watcher和持久性递归的watcher,通过addWatcher()方法添加,可以指定AddWatchMode。
一次性watcher
持久性watcher
持久性递归watcher

客户端三类,服务端两类(通过调用具体操作添加,比如getData、exists方法)
1. dataWatches:表示监听的是某节点的数据变化,比如数据的新增、修改、删除
2. childWathes:表示监听的是某节点的孩子节点的变化,如果某个节点新增或删除了,会触发其父节点上的NodeChildrenChanged事件
3. existWatches(只在客户端):服务端无需单独触发该事件,由客户端接收其他事件自己判断,比如客户端接收到一个NodeCreated事件,客户端如果注册了existWatches,那么existWatches就会被触发。

watch机制实现原理:
1、客户端会向服务端注册watcher,根据watcher类型构造注册请求的packet(并没有把watcher封装到packet,只是封装了一个布尔变量,如果有watcher,就是true),放入到outGoingQueue中,sendThread从这个对列中拿出packet发送给服务端,发出请求后,sendTread将请求放入到pendingQueue。服务端会有一套处理逻辑,等下讲,总之服务端有事件触发之后,会把触发事件发给客户端,客户端接收到响应,收到回复后,sendthread从pendingqueue中取出request,并生成event,并且发现是一个监听事件,把事件放入到waitEvents队列,先把事件注册到客户端维护的三个map中的一个,EventThread线程从这个队列拿出响应进行处理,执行具体的响应的代码。(客户端存放哪些节点绑定了哪些watcher)
2、服务端接收到一个注册请求之后,会放入到请求链,即preRequestQueue、syncRequestQueue、finalRequestQueue,在preRequestQueue预处理,在syncRequestQueue中持久化(如果是集群模式还要经过两阶段提交给follower节点),到了finalRequestQueue中才进行监听,实际上就是一个map,存放了节点和客户端连接(继承了watcher,所以一个连接其实就是一个watcher),如果监听到事件,直接从map中获取到客户端链接,然后返回给客户端(map< path, Set< cnxn>>),另外这里还要看监听器的类型,如果是一次性的,会从map中删除掉监听器,如果是持久的监听器,则不会移除。

三、zookeeper应用场景

1、配置中心:持久节点 watch机制
2、分布式锁:临时顺序节点 watch机制(1、判断自己是否是最小的节点2、监听前一个节点的删除事件)
3、注册中心:临时节点(起一个服务,则创建一个临时节点记录地址,服务挂了临时节点会被删除)
4、集群管理:
5、分布式Job

四、zk server单机启动
1、解析配置
2、基于存储的DataTree快照和日志初始化DataTree
3、监听socket,绑定端口(监听客户端的请求,默认nio):会将请求加入到一个请求链(RequestProcessor),请求链实际上是三个队列,从队列中拿出来请求之后会预处理(验证权限,为持久化做准备),然后持久化(持久化日志(只持久化写请求),并生成快照,第二个队列),然后处理请求,更新DataTree,触发watch并响应客户端(第三个队列)

持久化请求:

如果是一个写请求,写到文件的outputstream中(但是没有flush,也就是没有真正写到磁盘),并把写请求加入到toFlush队列,这时如果又接收到一个读请求,就不会写到outputstream中了(读请求不会持久化),但是要保证读写顺序,所以需要把读请求也加到toFlush队列,如果达到flush条件(请求个数达到一定数量,或者达到了时间限制)就会进行flush,这时候数据真正写入到磁盘,然后会从toFlush中取出请求交给下一个队列去更新DateTree,触发watch以及响应客户端。如果接收到一个读请求的时候toFlush队列是空的,那么读请求是不用放入到toFlush队列的,直接交给下一个队列去更新DateTree,触发watch以及响应客户端。所以读请求处理要分两种情况,而写请求总是先放入到toFlush队列。

三、集群的角色

领导者(Leader) : 负责进行投票的发起和决议,最终更新状态。
跟随者(Follower): Follower用于接收客户请求并返回客户结果。参与Leader发起的投票。
观察者(observer): Oberserver可以接收客户端连接,将写请求转发给leader节点。但是Observer不参加投票过程,只是同步leader的状态。Observer为系统扩展提供了一种方法。
学习者 ( Learner ) : 和leader进行状态同步的server统称Learner,上述Follower和Observer都是Learner。

选举过程:

  1. 个人能力:Zookeeper是一个数据库,集群中节点的数据越新就代表此节点能力越强,而在Zookeeper中可以通事务id(zxid)来表示数据的新旧,一个节点最新的zxid越大则该节点的数据越新。所以Zookeeper选举时会根据zxid的大小来作为投票的基本规则。
  2. 改票:Zookeeper集群中的某一个节点在开始进行选举时,首先认为自己的数据是最新的,会先投自己一票,并且把这张选票发送给其他服务器,这张选票里包含了两个重要信息:zxid和sid,sid表示这张选票投的服务器id,zxid表示这张选票投的服务器上最大的事务id,同时也会接收到其他服务器的选票,接收到其他服务器的选票后,可以根据选票信息中的zxid来与自己当前所投的服务器上的最大zxid来进行比较,如果其他服务器的选票中的zxid较大,则表示自己当前所投的机器数据没有接收到的选票所投的服务器上的数据新,所以本节点需要改票,改成投给和刚刚接收到的选票一样。
  3. 投票箱:Zookeeper集群中会有很多节点,和人类选举不一样,Zookeeper集群并不会单独去维护一个投票箱应用,而是在每个节点内存里利用一个数组来作为投票箱。每个节点里都有一个投票箱,节点会将自己的选票以及从其他服务器接收到的选票放在这个投票箱中。因为集群节点是相互交互的,并且选票的PK规则是一致的,所以每个节点里的这个投票箱所存储的选票都会是一样的,这样也可以达到公用一个投票箱的目的。
  4. 领导者:Zookeeper集群中的每个节点,开始进行领导选举后,会不断的接收其他节点的选票,然后进行选票PK,将自己的选票修改为投给数据最新的节点,这样就保证了,每个节点自己的选票代表的都是自己暂时所认为的数据最新的节点,再因为其他服务器的选票都会存储在投票箱内,所以可以根据投票箱里去统计是否有超过一半的选票和自己选择的是同一个节点,都认为这个节点的数据最新,一旦整个集群里超过一半的节点都认为某一个节点上的数据最新,则该节点就是领导者。

同步原理

zxid
zookeeper采用了全局递增的事务 Id 来标识,所有的 proposal(提议)都在被 提出的时候加上了zxid,zxid 实际上是一个 64 位的数字,高 32 位是 epoch(时期; 纪元; 世; 新时代)用来标识 leader 周期,如果有新的 leader 产生出来,epoch会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。

同步流程
当服务器启动时,完成了领导者选举后,确定了服务器的角色后(比如Leader、Follower、Observer),会先统一Epoch,然后就开始数据同步,最后再构造RequestProcessor,处理客户端的请求。
1. Learner节点向Leader发送LearnerInfo数据(包含了acceptEpoch),然后等待Leader响应
2. Leader不停的从Learner节点接收到发送过来的LearnerInfo数据,比较Epoch,超过过半机制后统一epoch
3. Leader同一Epoch后,向Learner节点,发送LEADERINFO数据(包含了新的epoch),等待接收ACKEPOCH数据
4. Learner节点接收到LEADERINFO数据后,修改自己的epoch,然后发送ACKEPOCH数据给Leader
5. 当Leader节点接收到了大部分的ACKEPOCH数据后,就开始同步数据,Learner节点阻塞等待Leader节点发送数据过来进行同步
6. Leader节点整理要同步的数据,把这些数据先会添加到queuedPackets队列中去,并且往队列中添加了一个NEWLEADER数据
7. Leader节点开启一个线程,从queuedPackets队列中获取数据进行同步
8. Learner节点接收数据进行同步,同步完之后,会接收到一个NEWLEADER数据,并返回给Leader一个ACK数据
9. Leader节点接收到了超过一半的ack后,则运行一个while,负责从Learner接收命令
10.Leader节点启动
11.Follower节点启动

Leader和Learner要同步哪些数据

数据的同步的目的:Learner和Leader上的数据保持一致。那么就有可能:
1. Leader的数据比Learner新,这时Leader要把多出的数据发给Learner。
2. Learner的数据比Leader新,这时Learner要把多出的数据删除掉。
如何判断Learner和Leader上的数据新旧?根据zxid。
如何发送数据给Leader?日志?快照?
在Leader上,数据会保存在几个地方:
1. 日志文件中(txnlog):数据最新
2. 快照中(snapshot):数据新度有延迟
3. CommittedLog队列:保存的是Leader节点最近处理的请求(相当于日志,日志是持久化在文件中的,而CommittedLog是在内存中的)
当Learner节点向Leader节点发起同步数据请求时,Learner会把它目前最大的zxid发给Leader,Leader则会结合自身的信息来进行判断,需要告知Learner如何同步数据

广播原理:
某个ZookeeperServer在处理写请求时,主要分为以下几步:
1. 针对当前请求生成日志(Txn)
2. 持久化日志(持久化Txn)
3. 执行日志,更新内存(根据Txn更新DataBase)
以上是单个ZookeeperServer执行写请求的步骤,那么,集群在处理写请求时只是在这几步之上做了修改。
Zookeeper集群处理写请求时,主要分为以下几步:
1. Leader节点,针对当前请求生成日志(Txn)
2. Leader节点,持久化前请求生成日志(Txn),并向自己发送一个Ack
3. Leader节点,把当前请求生成的日志(Txn)发送给其他所有的参与者节点(非Observer)
4. Leader节点,阻塞等待Follower节点发送Ack过来(超过一半则解阻塞)
5. Follower节点,接收到Leader节点发送过来的Txn
6. Follower节点,持久化当前Txn,并向Leader节点发送一个Ack
7. Leader节点,接收到了超过一半的Ack(加上自己发给自己的Ack),则解阻塞
8. Leader节点,向Follower节点发送commit命令(异步发送的,不会阻塞Leader节点)
9. Leader节点,执行Txn,更新内存(根据Txn更新DataBase)
10. Follower节点,接收到Leader节点发送过来的commit命令
11. Follower节点,执行Txn,更新内存(根据Txn更新DataBase)

Observer节点  

  • Zookeeper需保证高可用和强一致性;
  • 为了支持更多的客户端,需要增加更多Server;
  • Server增多,投票阶段延迟增大,影响性能;
  • 权衡伸缩性和高吞吐率,引入Observer
  • Observer不参与投票;
  • Observers接受客户端的连接,并将写请求转发给leader节点;
  • 加入更多Observer节点,提高伸缩性,同时不影响吞吐率
  

脑裂和基数节点

过半机制

如何保证有序性:

顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。

客户端:
同步调用的时候同一个客户端发送的请求都有一个xid,放送请求的时候会先放入outGoingQueue中,sendThread从outGoingQueue取出请求,并发送给服务端,然后把请求放入到pendingQueue,当sendThread接收到响应之后,从pendingQueue中取出请求,比较响应的xid和请求的xid,如果不相等,会抛出CONNECTIONLOSS的异常。

服务端:
ZAB 实现了 FIFO 队列,保证消息处理的顺序性。
另外,ZAB 还实现了当主节点崩溃后,只有日志最完备的节点才能当选主节点,因为日志最完备的节点包含了所有已经提交的日志,所以这样就能保证提交的日志不会再改变。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注