竹笋

注册

 

发新话题 回复该主题

横趟面试中遇到的ZooKeeper问题 [复制链接]

1#

作者:HelloGitHub-老荀

本文是HelloZooKeeper系列的最后一篇文章,接下来主要聊聊面试中如果被问到ZooKeeper的问题如何回答,也可以当作学完本系列的测试。

准备好了吗?面试开始喽~

一、模拟面试

终于来到重头戏了,本小节我会从网上找到一些关于ZK的面试题进行剖析讲解,并且站在面试官的基础上分析考点,相信看完这一节,出去面试再碰到ZK相关的问题你便能披荆斩棘、所向披靡!

我先给大家模拟一个面试的场景:

面试官:我看你简历上用过ZK,能给我介绍下吗?你是怎么理解ZK的作用呢?

(如果你把百度百科中的定义背给他听,我只能说,千万别这样,会被别人当成傻子。)

我:我的理解ZK是一个脱离于应用的第三方进程,类似的数据库,消息队列,Redis等都是扮演这个角色,拥有一定的数据存储和查询能力,可以让我们在现在都是分布式部署的应用之间“传递”数据,其次ZK支持的回调通知,让应用可以在一些业务场景中感知到数据的变化并及时作出相应的反应。最后,ZK本身也支持集群部署具有高可用的特点,是一个可靠的第三方中间件。

面试官:嗯,你刚刚提到了回调通知,能仔细跟我聊聊ZK是怎么去实现的吗?

我:各种编程语言的客户端都会对这个回调通知进行抽象,通常需要开发者声明一个callback的对象,在Java的客户端中这个接口是Watcher,ZK服务端提供了一些方法,比如getData、exists或者最新版本中的addWatch都可以用来向ZK注册回调通知,而向服务端发送的回调通知,只会告诉服务端我当前的这个路径需要被通知,服务端得知后,会在内存中记录下来,路径和客户端之间的关系,客户端自己也需要记录下来,路径和具体回调的关系。当被订阅的路径发生事件的时候,各种增删改吧,服务端就会从内存中的记录去查看有没有需要通知的客户端,有的话会发送一个通知的请求给客户端,客户端收到通知后,就会从本地的记录中取出对应的回调对象去执行callback方法!

(实际情况,我觉得面试官可能不会让你一直说下去,应该是互相聊的一个状态)

面试官:嗯,说得挺详细,那你刚刚提到的getData、exists、addWatch三种注册有什么区别吗?

我:getData、exists以及getChildren注册的通知都是一次性的,当服务端通知过一次后,就会删除内存中的记录,之后如果仍然需要通知的话,客户端就要去继续注册,而addWatch注册的回调通知是永久性的,只需要注册一次可以一直被通知。

面试官:嗯好,你刚刚还提到了ZK有一定的数据存储能力,你能说说ZK是怎么保存和整理数据的吗?

我:ZK的数据体现在两部分。

面试官:哦?哪两部分?

我:内存中和磁盘上。

面试官:那你先说说内存里ZK是怎么存储数据

我:从逻辑上来讲,ZK内存中的数据其实是一个树形结构,从/根节点开始,逐级向下用/分割,每一个节点下面还可以有多个子节点,就类似于Unix中的目录结构,但在实际中,ZK是使用一个HashMap去存储整个树形结构的数据的,key是对应的全路径字符串,value则是一个节点对象,包含了节点的各种信息。

面试官:能说说你觉得为什么要这么设计吗?

(其实我觉得一般面试官不会这么问,以下回答也是我个人的猜想)

我:首先HashMap查询速度很快,是Java标准库中一个非常重要的数据结构,在许多地方都有用到。ZK本身并不需要排序或者是范围求值的操作,所以HashMap完全可以满足查询的需求。至于为什么逻辑上要设计成树形结构,父子节点,这个可能是因为这个结构和Unix文件系统很像,非常便于理解以及基于路径进行数据的分类,而且最新的ZK中有一些功能是依赖了父子递归这个特性的(比如addWatch),如果是普通的key-value是无法满足的。

面试官:嗯好,那你再说说磁盘上ZK是怎么存储数据的呢?

我:ZK在磁盘上规定了两种文件类型,一种是log文件,一种是snapshot。log文件是增量记录,负责对每一个写请求进行保存,snapshot文件是全量记录,是对内存的快照。

面试官:ZK是怎么保证内存中的数据和磁盘中的数据的一致性呢?

我:真正的强一致性,ZK无法保证。对于每一次的写请求,ZK是采取先记录磁盘再修改内存的,所以保证了如果出现意外的话,优先记录磁盘可以尽可能的保证数据的完整。如果ZK是正常退出的话,也会强制刷磁盘文件和生成snapshot,保证了一致性,但如果是非正常退出的话,极端情况下的一部分数据是会丢失的。

面试官:你刚刚也提到了ZK本身也可以集群部署的?能多聊一点吗?

我:ZK的配置文件zoo.cfg中可以配置其他节点的信息,各个节点通过dataDir目录下的myid文件进行区分,不同节点之间可以相互通信,客户端连上集群中的任意一个节点都可以进行通信。

面试官:ZK集群中有几种不同的角色?你知道吗?

我:有Leader、Follower、Observer三种角色。

面试官:说说他们之间的区别吧

我:集群中有且只能有一个Leader,Leader负责对整个集群的写请求事务进行提交,在一个集群选出Leader之前是无法对外提供服务的。Follower和Observer都只能处理读请求,区别是Follower有投票权可以参与Leader的竞选,Observer无法参与Leader的竞选。

面试官:那你可以跟我讲讲,选举Leader依靠哪些信息吗?

我:每一个节点都会维护三个最重要的信息:epoch、zxid、myid。epoch代表选举的轮次,优先比较,如果相同则继续比较下一级。zxid代表本节点处理过的最大事务ID,越大代表当前节点经手的写请求越多,知道的也就越多,第二优先级比较,如果还相同则比较myid,myid整个集群中不能重复,所以最终一定能分出胜负。胜利的节点当选Leader。

(准确地说,epoch和zxid是一个字段,一个记录在高32位,一个记录在低32位)

面试官:不同节点之间怎么通信呢?怎么去进行选举?

我:每一个ZK节点在启动的时候,会通过读取配置文件中的集群信息,与其他节点建立Socket连接,集群间的通信就是通过这个Socket。每个节点选举的时候都把自己认为的候选人信息广播出去,同时也接收来自其他节点的候选人信息,通过比较后,失败的一方会更改自己的候选人信息并重新进行广播,反复直到某一个节点得到半数以上投票,选举就完成了。

面试官:不同的节点角色,在处理读写请求上有什么不同吗?你先聊聊Leader吧

我:好滴,Leader作为集群中的老大,负责对收到的写请求发起提案PROPOSAL,告诉其他节点当前收到一个写请求,其他节点收到后,会在本地进行归档,其实就是写入文件输出流,完毕后会发送一个ACK给Leader,Leader统计到半数以上的ACK之后会再次发送给其他节点一个COMMIT,其他节点收到COMMIT之后就可以修改内存数据了。读请求的话不需要提案直接查询内存中的数据返回即可。

面试官:那Follower或Observer呢?

我:他们收到读请求是一样的,直接返回本地的内存数据即可。但是写请求的话,会将当前请求转发给Leader,然后由Leader去处理,就和之前的流程是一样的。

面试官:不同的请求ZK是如何保证顺序呢?

我:这个顺序的保证最终是落实在一个先进先出的队列,优先进该队列的请求会被先处理,所以能保证顺序。

面试官:不同的客户端的请求怎么保证顺序呢?A先发送了一个创建节点,在该请求返回之前,B发送了一个查询该节点,B会阻塞到A执行完毕再查询吗?还是直接返回查询不到节点?

我:B会直接返回查不到。不同的客户端之间的顺序ZK不保证,原因是在底层ZK是通过一个Map去分别放置不同的客户端的请求的,不同的客户端的key是不一样的,而这个Map的value则是我刚刚提到的先进先出的队列。所以只有同一个客户端的请求能被顺序执行,不同的客户端是无法保证的。

面试官:能说说不同的客户端的key是什么吗?怎么保证不同。

我:每一个客户端在连接至ZK后会被分配一个sessionId,这个sessionId是通过当前时间戳、节点的myid和一个递增特性生成的一个long类型字段,可以保证不会重复。

面试官:说到session,你知道ZK的会话是怎么维持的吗?

我:你问的是客户端和服务端之间的会话吗?

面试官:是的,你能跟我说说吗?

我:每一个客户端在连接ZK的时候会同时上报自己的超时时间,加上刚刚的sessionId,ZK的服务端会在本地维护一个映射关系,通过计算可以计算出该sessionId的超时时间,并且ZK自己也有一个tickTime的配置,通过一个算法可以将不同客户端不同超时间都映射到相同间隔的时间点上,再将这个超时时间和sessionId关系存起来。

面试官:映射到相同的时间点上有什么好处吗?

我:这样服务端在启动后,后台会有一个线程,通过这个统一的时间间隔,取出session过期的客户端,向他们发送会话过期的消息,极大地节约了性能。

面试官:客户端是怎么去更新会话的超时时间呢?

我:首先客户端的每次操作都会刷新这个超时时间,其次客户端必须设计一个PING的操作,用于在客户端空闲的时候主动去刷新会话超时时间,防止过期。

面试官:除了客户端和服务端之间的会话,还有别的吗?

我:服务端和服务端之间也有心跳,而且服务端的心跳是由Leader主动发起的,向其他节点发送PING请求,而其他节点收到PING后,需要把本地的会话信息一并发送给Leader。

编不下去了,上面一些题具有我强烈的主观偏好性,我觉得如果面试官是个菜鸡的话,这些问题大部分都问不出来,所以重点是不在于我怎么回答,而是当你对背后的原理了然于胸时,自然是神挡杀神,佛挡杀佛。

我说说我认为比较重要的几个特性:

回调通知,ZK其他原理可以不懂,但是怎么用回调是肯定要知道的。选举,ZK最具特色的一个属性,基本都会问一下。持久化,说清楚两种文件的区别。会话,会话的概念,以及怎么维持。最后通过一个模拟面试回答了一下我认为ZK中比较有特点的面试问题,如果大家对面试问题还有什么疑问记得留言给我噢~必须给你们安排上!

二、网上真题

我大部分题目是网上直接搜的,网址在这里ZooKeeper面试题(最新版)但是过滤了一些太low的题目。

4.ZooKeeper怎么保证主从节点的状态同步?

我上面说了Leader在接受到写请求后,会发起提案,然后等待其他节点的ACK,这个ACK是要求半数以上通过才能继续下去的,所以能收到半数以上的ACK说明集群中的一半以上都已经完成了本地磁盘的归档,自然是保证了主从之间的数据同步。

5.四种类型的数据节点Znode

我之前的文章中有介绍现在ZK中有7种节点类型,关于新节点的原理我还没来得及讲,所以他如果这么问了你可以很官方的回答他:

持久节点持久顺序节点临时节点临时顺序节点他一般后面会接着问两者的区别,临时节点会随着客户端的会话断开而自动删除,原理就是在创建临时节点的时候,服务端会维护一个sessionId和它对应的临时节点路径列表,当关闭会话时,把这个列表里的路径都拿出来一一删除即可。而顺序节点的区别就在于ZK会自动为路径加上数字的后缀,仅此而已。

并发创建时,顺序节点怎么保证后缀数字唯一呢?

ZK的请求是放入队列里一个个处理的,所以其实并没有所谓的并发,前一个请求处理完再处理下一个请求,自然就能保证后缀数字的唯一性了。

10.ACL权限控制机制

ZK将权限分为两大类,两大类又能继续细分:

客户端的角色权限IP用户名密码world,最宽泛的权限,也就是没有权限super,特殊的用户名密码,相当于管理员权限节点的数据权限Create,创建Delete,删除Read,读Write,写ACL,读写权限11.Chroot特性

chroot是ZK设计给客户端的命名空间隔离,作为不同客户端的根节点,由客户端去维护,总的来说就是发送请求之前把chroot的路径拼接上,再去请求服务端。chroot对于服务端是透明的,完全不知道的。

15.数据同步

Learner和Leader之间同步数据是一个比较漫长和复杂的过程,总的来说可以大致分为以下步骤:

Learner上报自己的信息给LeaderLeader根据Learner信息决定使用何种同步方法DIFF,直接从最近的个提案中恢复数据,直接发送提案即可TRUNC,通常出现于Learner是前Leader,需要降级自己的数据达到和Leader一致SNAP,Leader直接发送整个内存快照给FollowerLeader和Learner开始同步同步完成后开始对外提供服务

三、配置大全

托大家的福,我把ZK的源码全部(爆肝)浏览了一遍,找到了至少99%的配置选项,ZK的配置大致可以分为3种:

启动命令行传入的参数zoo.cfg配置文件中的参数当前环境变量中的参数

3.1命令行参数

命令行参数很少,而且没有对应的配置名称,这里我简单介绍下:

单机版只支持两种形式的命令行传参

客户端监听端口加data目录,上一节源码调试中用的就是这一个形式,例如:/your/zk/data/path或者只传一个参数,zoo.cfg的路径,例如:/your/zoocfg/path集群版更简单只支持zoo.cfg的路径一个参数

3.2zoo.cfg文件中的配置

我仔细查看源码的时候发现有些配置实际作用时需要计算又或者是一鱼两吃,被多个地方使用,所以很难一步到位地讲清楚,所以下面的介绍仅供参考,配置项加星号(*)的是我未来打算开篇讲解的。

以上就是3.6.2中zoo.cfg所有的官方配置选项了(由于编辑器问题,只放了一部分)

3.3环境变量配置

Java程序想要指定环境变量有两种方法:

只需要在启动的时候在后面加上-DpropertyKey=propertyValue即可ZK还支持一种简单的方式就是在zoo.cfg中直接指定(指定时不需要写zookeeper.的前缀)。只要不是在上面2.2中ZK自己定义的配置项里,ZK启动的时候读取这些配置会自动帮他们添加zookeeper.前缀并加入当前环境变量中如果该配置是follower.nodelay,就只能用第一种方式添加环境变量了。

让我们也来看看ZK自己定义了哪些环境变量配置吧

ZK的配置还是很多的,有些我这里TODO了,以后有机会和大家详细介绍下~而且相当一部分的配置ZK官方的文档中已经给出了解释,可以查看ZK3.6.2配置文档。

我这里还要吐槽下,ZK中有些配置是用true或者false,有些使用yes或者no,明显是两个(波)人开发的,这种不应该做一个统一吗?yes或no真的很多余...

四、系列结语

感谢你们能看到这里,陪伴这个系列从开始到现在!这个项目从有想法立项到之后跟蛋蛋沟通,再到正式开始编写,到最后我写下这段结语,大概经历了三个多月(你们看到的时候应该是更晚),现在回头再看之前写的东西,感慨颇深。

(截图来自何同学的视频)

如果以我自己的自控能力,这玩意自己搞,搞着搞着可能就凉了,在此感谢蛋蛋给予我的帮助和鼓励。关于ZK我的确之前有研究过一段时间,但是以现在的眼光看,当时的研究其实也就是皮毛而已(可能现在也还是),很多东西是我这次整理时现学的,收获非常多,最直观的感受就是,我以后出去面试不会再害怕ZK相关的问题了。

感谢大家这3个月的陪伴,本系列终结喽!如果还有什么想学的开源框架和技术可以留言告诉我们,后续继续为大家安排免费的干货教程。

最最后,来个大大的赞吧!

分享 转发
TOP
发新话题 回复该主题