请简述 Zookeeper 的选举机制?

假设有五台服务器组成的 Zookeeper 集群,它们的 id 从 1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么。

  1. 服务器 1 启动,此时只有它一台服务器启动了,它发出去的报没有任何响应,所以它的选举状态一直是 LOOKING 状态;
  2. 服务器 2 启动,它与最开始启动的服务器 1 进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以 id 值较大的服务器 2 胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是 3),所以服务器 1、2 还是继续保持 LOOKING 状态;
  3. 服务器 3 启动,根据前面的理论分析,服务器 3 成为服务器 1、2、3 中的 Leader,而与上面不同的是,此时有三台服务器选举了它,所以它成为了这次选举的 Leader
  4. 服务器 4 启动,根据前面的分析,理论上服务器 4 应该是服务器 1、2、3、4 中最大的,但是由于前面已经有半数以上的服务器选举了服务器 3,所以它成为 Follower
  5. 服务器 5 启动,同 4 一样成为 Follower

注意:如果按照 5,4,3,2,1 的顺序启动,那么 5 将成为 Leader,因为在满足半数条件后,ZooKeeper 集群启动,5 的 Id 最大,被选举为 Leader

客户端如何正确处理 CONNECTIONLOSS(连接断开)和 SESSIONEXPIRED(Session 过期)两类连接异常?

在 ZooKeeper 中,服务器和客户端之间维持的是一个长连接,在 SESSION_TIMEOUT 时间内,服务器会确定客户端是否正常连接(客户端会定时向服务器发送 heart_beat),服务器重置下次 SESSION_TIMEOUT 时间。因此,在正常情况下,Session 一直有效,并且 ZK 集群所有机器上都保存这个 Session 信息。在出现问题的情况下,客户端与服务器之间连接断了(客户端所连接的那台 ZK 机器挂了,或是其它原因的网络闪断),这个时候客户端会主动在地址列表(初始化的时候传入构造方法的那个参数 connectString)中选择新的地址进行连接。

以上即为服务器与客户端之间维持长连接的过程,在这个过程中,用户可能会看到两类异常 CONNECTIONLOSS(连接断开)和 SESSIONEXPIRED(Session 过期)。

发生 CONNECTIONLOSS 后,此时用户不需要关心我的会话是否可用,应用所要做的就是等待客户端帮我们自动连接上新的 ZK 机器,一旦成功连接上新的 ZK 机器后,确认之前的操作是否执行成功了。

一个客户端修改了某个节点的数据,其他客户端能够马上获取到这个最新数据吗?

ZooKeeper 不能确保任何客户端能够获取(即 Read Request)到一样的数据,除非客户端自己要求,方法是客户端在获取数据之前调用 org.apache.zookeeper.AsyncCallback.VoidCallback, java.lang.Object) sync

  • 通常情况下(这里所说的通常情况满足:1. 对获取的数据是否是最新版本不敏感,2. 一个客户端修改了数据,其它客户端是否需要立即能够获取最新数据),可以不关心这点。
  • 在其它情况下,最清晰的场景是这样:ZK 客户端 A 对 /my_test 的内容从 v1->v2, 但是 ZK 客户端 B 对 /my_test 的内容获取,依然得到的是 v1。请注意,这个是实际存在的现象,当然延时很短。解决的方法是客户端 B 先调用 sync(), 再调用 getData()

ZooKeeper 对节点的 watch 监听是永久的吗?为什么?

不是。

官方声明:一个 Watch 事件是一个一次性的触发器,当被设置了 Watch 的数据发生了改变的时候,则服务器将这个改变发送给设置了 Watch 的客户端,以便通知它们。

为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,这太消耗性能了。

一般是客户端执行 getData("/节点 A",true),如果节点 A 发生了变更或删除,客户端会得到它的 watch 事件,但是在之后节点 A 又发生了变更,而客户端又没有设置 watch 事件,就不再给客户端发送。

在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。

ZooKeeper 中使用 watch 的注意事项有哪些?

使用 watch 需要注意的几点:

  • Watches 通知是一次性的,必须重复注册。
  • 发生 CONNECTIONLOSS 之后,只要在 session_timeout 之内再次连接上(即不发生 SESSIONEXPIRED),那么这个连接注册的 watches 依然在。
  • 节点数据的版本变化会触发 NodeDataChanged,注意,这里特意说明了是版本变化。存在这样的情况,只要成功执行了 setData() 方法,无论内容是否和之前一致,都会触发 NodeDataChanged。
  • 对某个节点注册了 watch,但是节点被删除了,那么注册在这个节点上的 watches 都会被移除。
  • 同一个 ZK 客户端对某一个节点注册相同的 watch,只会收到一次通知。
  • Watcher 对象只会保存在客户端,不会传递到服务端。

能否收到每次节点变化的通知?

如果节点数据的更新频率很高的话,不能。

原因在于:当一次数据修改,通知客户端,客户端再次注册 watch,在这个过程中,可能数据已经发生了许多次数据修改,因此,千万不要做这样的测试:“数据被修改了 n 次,一定会收到 n 次通知”来测试 server 是否正常工作。

能否为临时节点创建子节点?

ZooKeeper 中不能为临时节点创建子节点,如果需要创建子节点,应该将要创建子节点的节点创建为永久性节点。

是否可以拒绝单个 IP 对 ZooKeeper 的访问?

如何实现?ZK 本身不提供这样的功能,它仅仅提供了对单个 IP 的连接数的限制。你可以通过修改 iptables 来实现对单个 ip 的限制。

创建的临时节点什么时候会被删除,是连接一断就删除吗?

延时是多少?连接断了之后,ZK 不会马上移除临时数据,只有当 SESSIONEXPIRED 之后,才会把这个会话建立的临时数据移除。因此,用户需要谨慎设置 Session_TimeOut。

ZooKeeper 是否支持动态进行机器扩容?如果目前不支持,那么要如何扩容呢?

ZooKeeper 中的动态扩容其实就是水平扩容,Zookeeper 对这方面的支持不太好,目前有两种方式:

  • 全部重启:关闭所有 Zookeeper 服务,修改配置之后启动,不影响之前客户端的会话。
  • 逐个重启:这是比较常用的方式。

ZooKeeper 集群中服务器之间是怎样通信的?

Leader 服务器会和每一个 Follower/Observer 服务器都建立 TCP 连接,同时为每个 F/O 都创建一个叫做 LearnerHandler 的实体。LearnerHandler 主要负责 Leader 和 F/O 之间的网络通讯,包括数据同步,请求转发和 Proposal 提议的投票等。Leader 服务器保存了所有 F/O 的 LearnerHandler。

ZooKeeper 是否会自动进行日志清理?

如何进行日志清理?ZK 自己不会进行日志清理,需要运维人员进行日志清理。

谈谈你对 ZooKeeper 的理解?

  • Zookeeper 作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的一致性问题。ZooKeeper 提供的服务包括:分布式消息同步和协调机制服务器节点动态上下线统一配置管理负载均衡集群管理等。

  • ZooKeeper 提供基于类似于 Linux 文件系统的目录节点树方式的数据存储,即分层命名空间。Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化,通过监控这些数据状态的变化,从而可以达到基于数据的集群管理,ZooKeeper 节点的数据上限是 1MB。

  • 我们可以认为 Zookeeper = 文件系统 + 通知机制,对于 ZooKeeper 的数据结构,每个子目录项如 NameService 都被称作为 znode,这个 znode 是被它所在的路径唯一标识,如 Server1 这个 znode 的标识为 /NameService/Server1

  • znode 可以有子节点目录,并且每个 znode 可以存储数据,注意 EPHEMERAL 类型的目录节点不能有子节点目录(因为它是临时节点);

  • znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据;

  • znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除,Zookeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 session,如果 znode 是临时节点,这个 session 失效,znode 也就删除了;

  • znode 的目录名可以自动编号,如 App1 已经存在,再创建的话,将会自动命名为 App2;

  • znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于这个特性实现的,后面在典型的应用场景中会有实例介绍。

ZooKeeper 节点类型?

  1. znode 有两种类型:
    • 短暂(ephemeral):客户端和服务器端断开连接后,创建的节点自己删除。
    • 持久(persistent):客户端和服务器端断开连接后,创建的节点不删除。
  2. znode 有四种形式的目录节点(默认是 persistent)
    • 持久化目录节点(PERSISTENT)客户端与 Zookeeper 断开连接后,该节点依旧存在。
    • 持久化顺序编号目录节点(PERSISTENT_SEQUENTIAL) 客户端与 Zookeeper 断开连接后,该节点依旧存在,只是 Zookeeper 给该节点名称进行顺序编号。
    • 临时目录节点(EPHEMERAL) 客户端与 Zookeeper 断开连接后,该节点被删除。
    • 临时顺序编号目录节点(EPHEMERAL_SEQUENTIAL)客户端与 Zookeeper 断开连接后,该节点被删除,只是 Zookeeper 给该节点名称进行顺序编号。

请说明 ZooKeeper 的通知机制?

ZooKeeper 选择了基于通知(notification)的机制,即:客户端向 ZooKeeper 注册需要接受通知的 znode,通过 znode 设置监控点(watch)来接受通知。监视点是一个单次触发的操作,意即监视点会触发一个通知。为了接收多个通知,客户端必须在每次通知后设置一个新的监视点。在下图阐述的情况下,当节点 /task 发生变化时,客户端会受到一个通知,并从 ZooKeeper 读取一个新值。

ZooKeeper 的监听原理是什么?

在应用程序中,main() 方法首先会创建 zkClient,创建 zkClient 的同时就会产生两个进程,即 Listener 进程(监听进程)和 connect 进程(网络连接/传输进程),当 zkClient 调用 getChildren() 等方法注册监视器时,connect 进程向 ZooKeeper 注册监听器,注册后的监听器位于 ZooKeeper 的监听器列表中,监听器列表中记录了 zkClient 的 IP,端口号以及要监控的路径,一旦目标文件发生变化,ZooKeeper 就会把这条消息发送给对应的 zkClient 的 Listener() 进程,Listener 进程接收到后,就会执行 process() 方法,在 process() 方法中针对发生的事件进行处理。

请说明 ZooKeeper 使用到的各个端口的作用?

  • 2888:Follower 与 Leader 交换信息的端口。
  • 3888:万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。

ZooKeeper 的部署方式有哪几种?集群中的角色有哪些?集群最少需要几台机器?

ZooKeeper 的部署方式有单机模式集群模式,集群中的角色有 Leader 和 Follower,集群最少 3(2N+1)台,根据选举算法,应保证奇数。

ZooKeeper 集群如果有 3 台机器,挂掉一台是否还能工作?挂掉两台呢?

对于 ZooKeeper 集群,过半存活即可使用。

ZooKeeper 使用的 ZAB 协议与 Paxo 算法的异同?

Paxos 算法是分布式选举算法,Zookeeper 使用的 ZAB 协议(Zookeeper 原子广播),两者的异同如下:

  • 相同之处:

    比如都有一个 Leader,用来协调 N 个 Follower 的运行;Leader 要等待超半数的 Follower 做出正确反馈之后才进行提案;二者都有一个值来代表 Leader 的周期。

  • 不同之处:

    ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统。

请谈谈对 ZooKeeper 对事务性的支持?

ZooKeeper 对于事务性的支持主要依赖于四个函数,zoo_create_op_initzoo_delete_op_initzoo_set_op_init 以及 zoo_check_op_init

每一个函数都会在客户端初始化一个 operation,客户端程序有义务保留这些operations。当准备好一个事务中的所有操作后,可以使用 zoo_multi 来提交所有的操作,由 Zookeeper 服务来保证这一系列操作的原子性。也就是说只要其中有一个操作失败了,相当于此次提交的任何一个操作都没有对服务端的数据造成影响。zoo_multi 的返回值是第一个失败操作的状态信号。