@Catyee
2020-10-08T16:46:49.000000Z
字数 8085
阅读 455
面试
持久化节点
顺序持久化节点
临时节点(会话级别,会话结束节点结束)
顺序临时节点
容器节点(最后一个子节点被删除(意味着一开始添加过子节点,如果从来没添加过子节点也不会删除),容器节点自动删除(有延迟))
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的过期时间的检查,用了一种时间轮的思想。
老版本都是一次性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中删除掉监听器,如果是持久的监听器,则不会移除。
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。
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节点启动
数据的同步的目的: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)
• 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 还实现了当主节点崩溃后,只有日志最完备的节点才能当选主节点,因为日志最完备的节点包含了所有已经提交的日志,所以这样就能保证提交的日志不会再改变。