Java 面试——Zookeeper

Java 面试——Zookeeper

一、Zookeeper 实现机制


文件系统 + 通知机制

二、Zookeeper 高可用实现原理


一个 ZooKeeper 集群如果要对外提供可用的服务,那么集群中必须要有过半的机器正常工作并且彼此之间能够正常通信。如果想搭建一个能够允许 N 台机器 down 掉的集群,那么就要部署一个由 2*N+1 台服务器构成的 ZooKeeper 集群。所以部署3个节点,那么就得至少有2个节点可用则该集群才可用。4个节点同样还是要2个以上。所以 Zookeeper集群部署的节点(非Observer)数一般为奇数。高可用机制其实基于 ZAB协议[链接]

三、Zookeeper文件系统


Zookeeper 提供一个多层级的节点命名空间(节点称为znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。Zookeeper为了保证高吞吐低延迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper不能用于存放大量的数据,每个节点的存放数据上限为1M。

四、四种类型的数据节点 Znode


【1】PERSISTENT 持久节点:除非手动删除,否则节点一直存在于 Zookeeper上;
【2】EPHEMERAL 临时节点:临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与 Zookeeper连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除;
【3】PERSISTENT_SEQUENTIAL 持久顺序节点:基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字;
【4】EPHEMERAL_SEQUENTIAL 临时顺序节点:基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字;

五、Zookeeper Watcher 机制


Zookeeper 客户端向服务端的某个 Znode注册一个 Watcher监听,当服务端对 Znode节点做指定事件[修改,删除]时,触发了这个 Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher通知状态和事件类型做出业务上的改变。

Watcher 特性总结:
【1】一次性:无论是服务端还是客户端,一旦一个 Watcher被触发,Zookeeper都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
【2】客户端串行执行:客户端 Watcher回调的过程是一个串行同步的过程。
【3】轻量:Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。客户端向服务端注册 Watcher的时候,并不会把客户端真实的 Watcher对象实体传递到服务端,仅仅是在客户端请求中使用 boolean类型属性进行了标记。
【4】watcher event异步发送:watcher的通知事件从 server发送到 client是异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用 Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的一致性,而无法保证强一致性。
【5】注册 watcher:getData、exists、getChildren。
【6】触发 watcher:create、delete、setData。
【7】当一个客户端连接到一个新的服务器上时,watch将会被以任意会话事件触发:当与一个服务器失去连接的时候,是无法接收到 watch的。而当 client重新连接时,如果需要的话,所有先前注册过的 watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch可能会丢失:对于一个未创建的 znode的exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 watch事件可能会被丢失。

六、客户端注册 Watcher实现


【1】调用 getData()/getChildren()/exist() 三个API,传入Watcher对象;
【2】标记请求 request,封装 Watcher到 WatchRegistration;
【3】封装成 Packet对象,发服务端发送 request;
【4】收到服务端响应后,将 Watcher注册到 ZKWatcherManager中进行管理;
【5】请求返回,完成注册;

七、服务端处理 Watcher实现


【1】服务端接收 Watcher并存储:接收到客户端请求,处理请求判断是否需要注册Watcher,需要的话将数据节点的节点路径和 ServerCnxn(ServerCnxn代表一个客户端和服务端的连接,实现了 Watcher的 process接口,此时可以看成一个 Watcher对象)存储在 WatcherManager的 WatchTable和 watch2Paths中去。
【2】Watcher 触发:以服务端接收到 setData() 事务请求触发NodeDataChanged事件为例:
    ● 封装 WatchedEvent:将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个 WatchedEvent对象;
    ● 查询 Watcher:从 WatchTable中根据节点路径查找 Watcher;
    ● 没找到,说明没有客户端在该数据节点上注册过 Watcher;
    ● 找到,提取并从 WatchTable和 Watch2Paths中删除对应Watcher(从这里可以看出Watcher在服务端是一次性的,触发一次就失效了);
【3】调用 process方法来触发Watcher:这里 process主要就是通过 ServerCnxn对应的 TCP连接发送 Watcher事件通知。

八、客户端回调 Watcher


客户端 SendThread线程接收事件通知,交由 EventThread线程回调 Watcher。客户端的 Watcher机制同样是一次性的,一旦被触发后,该 Watcher就失效了。

九、ZAB协议


ZAB协议是为分布式协调服务 Zookeeper专门设计的一种支持崩溃恢复原子广播协议。当整个 zookeeper集群刚刚启动或者 Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的 Leader服务器,然后集群中 Follower服务器开始与新的 Leader服务器进行数据同步,当集群中超过半数机器与该 Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。ZAB协议[链接]

十、Leader 选举


详细博文链接

十一、数据同步


整个集群完成 Leader选举后,Learner(Follower和Observer的统称)会向 Leader服务器进行注册。当 Learner服务器向 Leader服务器完成注册后,进入数据同步环节。

Zookeeper 的数据同步通常分为四类
【1】直接差异化同步(DIFF同步)
【2】先回滚再差异化同步(TRUNC+DIFF同步)
【3】仅回滚同步(TRUNC同步)
【4】全量同步(SNAP同步)

进行数据同步前,Leader会完成数据同步初始化:
【1】peerLastZxid:Learner服务器注册时发送的 ACKEPOCH消息中提取 lastZxid(该Learner服务器最后处理的ZXID)
【2】minCommittedLog:Leader服务器 Proposal缓存队列 committedLog中最小ZXID;
【3】maxCommittedLog:Leader服务器 Proposal缓存队列 committedLog中最大ZXID;

直接差异化同步(DIFF同步):peerLastZxid介于 minCommittedLog 和 maxCommittedLog之间;

 

先回滚再差异化同步(TRUNC+DIFF同步):当新的 Leader服务器发现某个 Learner服务器包含了一条自己没有的事务记录,那么就需要让该 Learner服务器进行事务回滚,回滚到 Leader服务器上存在的,同时也是最接近于 peerLastZxid的 ZXID;

仅回滚同步(TRUNC同步):peerLastZxid 大于 maxCommittedLog;

全量同步(SNAP同步):① peerLastZxid 小于 minCommittedLog;Leader服务器上没有 Proposal缓存队列且 peerLastZxid不等于 lastProcessZxid;

十二、Zookeeper是如何保证事务的顺序一致性的?


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

十三、Zookeeper集群管理(文件系统、通知机制)


集群管理包含两点:是否有机器退出和加入、选举 Master。
对于第一点,所有机器约定在父目录下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有机器挂掉,该机器与 Zookeeper的连接断开,其所创建的临时目录节点被删除,所有其它机器都收到通知:某个兄弟目录被删除。新机器加入也类似,所有机器收到通知:新兄弟目录加入节点。对于第二点,我们稍微改变一下,所有机器创建临时顺序编号目录节点,每次选取编号最小的机器作为 Master就好。

十四、Zookeeper分布式锁(文件系统、通知机制)


有了 Zookeeper的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一是保持独占,另一个是控制时序。
对于第一类,我们将 Zookeeper上的一个 znode看作是一把锁,通过 createznode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的distribute_lock 节点就释放出锁。
对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选 Master一样,编号最小的获得锁,用完删除,依次方便。

十五、获取分布式锁的流程


在获取分布式锁的时候在 locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用 createNode方法在 locker下创建临时顺序节点,然后调用 getChildren(“locker”)来获取 locker下面的所有子节点,注意此时不用设置任何 Watcher。客户端获取到所有的子节点 path之后,如果发现自己创建的节点在所有创建的子节点序号最小,那么就认为该客户端获取到了锁。如果发现自己创建的节点并非 locker所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用 exist()方法同时对其注册事件监听器。之后,这个被关注的节点删除,则客户端的 Watcher会收到相应通知,此时再次判断自己创建的节点是否是 locker子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。当前这个过程中还需要许多的逻辑判断。

代码的实现主要是基于互斥锁,获取分布式锁的重点逻辑在于 BaseDistributedLock,实现了基于 Zookeeper实现分布式锁的细节。

十六、Zookeeper 使用那种设计模式?


观察者模式【链接

十七、集群支持动态添加机器吗?


其实就是水平扩容了,Zookeeper在这方面不太好。两种方式:
【1】全部重启:关闭所有 Zookeeper服务,修改配置之后启动。不影响之前客户端的会话。
【2】逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。

3.5版本开始支持动态扩容。

十八、Zookeeper数据复制


Zookeeper 作为一个集群提供一致的数据服务,自然,它要在所有机器间做数据复制。数据复制的好处:
【1】容错一个节点出错,不致于让整个系统停止工作,别的节点可以接管它的工作;
【2】提高系统的扩展能力 :把负载分布到多个节点上,或者增加节点来提高系统的负载能力;
【3】提高性能:让客户端本地访问就近的节点,提高用户访问速度

从客户端读写访问的透明度来看,数据复制集群系统分下面两种:
【1】写主(WriteMaster) :对数据的修改提交给指定的节点。读无此限制,可以读取任何一个节点。这种情况下客户端需要对读与写进行区别,俗称读写分离
【2】写任意(Write Any):对数据的修改可提交给任意的节点,跟读一样。这种情况下,客户端对集群节点的角色与变化透明。

对 Zookeeper来说,它采用的方式是写任意。通过增加机器,它的读吞吐能力和响应能力扩展性非常好,而写,随着机器的增多吞吐能力肯定下降(这也是它建立 observer的原因),而响应能力则取决于具体实现方式,是延迟复制保持最终一致性,还是立即复制快速响应。

本文来源程序猿进阶,由javajgs_com转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处:https://javajgs.com/archives/8210

发表评论