ZooKeeper 由 Yahoo 开发,后来捐赠给了 Apache ,现已成为 Apache 顶级项目。ZooKeeper 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 Paxos 算法的 ZAB 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。
ZAB
Zookeeper 架构
作为一个优秀高效且可靠的分布式协调框架,ZooKeeper 在解决分布式数据一致性问题时并没有直接使用 Paxos ,而是专门定制了一致性协议叫做 ZAB(ZooKeeper Automic Broadcast) 原子广播协议,该协议能够很好地支持 崩溃恢复 。

ZAB 中的三个角色
Leader:集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。Follower:能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给Leader。在选举过程中会参与投票,有选举权和被选举权 。Observer:就是没有选举权和被选举权的Follower。
在 ZAB 协议中对 zkServer(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 消息广播 和 崩溃恢复 。
消息广播模式
说白了就是 ZAB 协议是如何处理写请求的,只有 Leader 能处理写请求,那么 Follower 和 Observer 也需要同步更新数据,即在整个集群中保持数据的一致性
第一步需要 Leader 将写请求 广播 出去,问 Followers 是否同意更新,如果超过半数以上的同意那么就进行 Follower 和 Observer 的更新(和 Paxos 一样)

两个 Queue 的作用是 ZAB 需要让 Follower 和 Observer 保证顺序性 。何为顺序性,比如我现在有一个写请求A,此时 Leader 将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 Follower F1因为网络原因没有收到,而 Leader 又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 产生数据不一致问题 。
所以在 Leader 这端,它为每个其他的 zkServer 准备了一个 队列 ,采用先进先出的方式发送消息。由于协议是 **通过 TCP **来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。
除此之外,在 ZAB 中还定义了一个 全局单调递增的事务ID ZXID ,它是一个64位long型,其中高32位表示 epoch 年代(对应leader),低32位表示事务id。定义这个的原因也是为了顺序性,每个 proposal 在 Leader 中生成后需要 通过其 ZXID 来进行排序 ,才能得到处理。
崩溃恢复模式
首先要提到 ZAB 中的 Leader 选举算法,可以分为两个不同的阶段:
Zookeeper启动时如何初始化选举
假设我们集群中有3台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了 server1 ,它会首先 投票给自己 ,投票内容为服务器的 myid 和 ZXID ,因为初始化所以 ZXID 都为0,此时 server1 发出的投票为 (1,0)。但此时 server1 的投票仅为1,所以不能作为 Leader ,此时还在选举阶段所以整个集群处于 Looking 状态。
接着 server2 启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(server1也会,只是它那时没有其他的服务器了),server1 在收到 server2 的投票信息后会将投票信息与自己的作比较。首先它会比较 ZXID ,ZXID 大的优先为 Leader,如果相同则比较 myid,myid 大的优先作为 Leader。所以此时server1 发现 server2 更适合做 Leader,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后server2 收到之后发现和自己的一样无需做更改,并且自己的 投票已经超过半数 ,则 确定 server2 为 Leader,server1 也会将自己服务器设置为 Following 变为 Follower。整个服务器就从 Looking 变为了正常状态。
当 server3 启动发现集群没有处于 Looking 状态时,它会直接以 Follower 的身份加入集群。
Leader宕机时如何重新选举假设
Leader (server2)宕机,首先剩下的两个Follower的状态 从Following变为Looking状态 ,然后每个server会向初始化投票一样首先给自己投票(这不过这里的zxid可能不是0了)。假设
server1给自己投票为(1,99),然后广播给其他 server,server3首先也会给自己投票(3,95),然后也广播给其他 server。server1和server3此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid大的优先,如果相同那么就myid大的优先)。这个时候server1收到了server3的投票发现没自己的合适故不变,server3收到server1的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后server1收到了发现自己的投票已经超过半数就把自己设为Leader,server3也随之变为Follower。
崩溃恢复 就是 当集群中有机器挂了,整个集群如何保证数据一致性
如果只是 Follower 挂了,而且挂的没超过半数的时候,因为在 Leader 中会维护队列,所以不用担心后面的数据没接收到导致数据不一致性。
如果 Leader 挂了需要先暂停服务变为 Looking 状态然后进行 Leader 的重新选举,要分为两种情况了:
确保已经被Leader提交的提案最终能够被所有的Follower提交
假设
Leader (server2)发送commit请求,他发送给了server3,然后要发给server1的时候突然挂了。这个时候重新选举的时候我们如果把server1作为Leader的话,那么肯定会产生数据不一致性,因为server3肯定会提交刚刚server2发送的commit请求的提案,而server1根本没收到所以会丢弃。但实际这时
server1已经不可能成为Leader了,因为进行投票选举的时候会比较ZXID,而此时server3的ZXID肯定比server1的大。
跳过那些已经被丢弃的提案
假设
Leader (server2)此时同意了提案N1,自身提交了这个事务并且要发送给所有Follower要commit的请求,却在这个时候挂了,此时肯定要重新进行Leader的选举,比如说此时选server1为Leader(这无所谓)。但是过了一会,这个 挂掉的 Leader 又重新恢复了 ,此时它肯定会作为Follower的身份进入集群中,需要注意的是刚刚server2已经同意提交了提案N1,但其他server并没有收到它的commit信息,所以其他server不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 该提案N1最终需要被抛弃掉 。
Zookeeper的几个理论知识
数据模型
zookeeper 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 zookeeper 中没有文件系统中目录与文件的概念,而是 使用了 znode 作为数据节点 。znode 是 zookeeper 中的最小数据单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。

每个 znode 都有自己所属的 节点类型 和 节点状态。
其中节点类型可以分为 持久节点、持久顺序节点、临时节点 和 临时顺序节点。
- 持久节点:一旦创建就一直存在,直到将其删除。
- 持久顺序节点:一个父节点可以为其子节点 维护一个创建的先后顺序 ,这个顺序体现在 节点名称 上,是节点名称后自动添加一个由 10 位数字组成的数字串,从 0 开始计数。
- 临时节点:临时节点的生命周期是与 客户端会话 绑定的,会话消失则节点消失 。临时节点 只能做叶子节点 ,不能创建子节点。
- 临时顺序节点:父节点可以创建一个维持了顺序的临时节点(和前面的持久顺序性节点一样)。
节点状态中包含了很多节点的属性,在 zookeeper 中是使用 Stat 这个类来维护的。
czxid:Created ZXID,该数据节点被 创建 时的事务ID。mzxid:Modified ZXID,节点 最后一次被更新时 的事务ID。ctime:Created Time,该节点被创建的时间。mtime:Modified Time,该节点最后一次被修改的时间。version:节点的版本号。cversion:子节点 的版本号。aversion:节点的ACL版本号。ephemeralOwner:创建该节点的会话的sessionID,如果该节点为持久节点,该值为0。dataLength:节点数据内容的长度。numChildre:该节点的子节点个数,如果为临时节点为0。pzxid:该节点子节点列表最后一次被修改时的事务ID,注意是子节点的 列表 ,不是内容。
会话
客户端和服务端是通过 TCP 长连接 维持的会话机制,其实对于会话来说可以理解为 保持连接状态 。
在 zookeeper 中,会话还有对应的事件,比如CONNECTION_LOSS 连接丢失事件、SESSION_MOVED 会话转移事件、SESSION_EXPIRED 会话超时失效事件。
ACL
ACL 为 Access Control Lists ,它是一种权限控制。在 zookeeper 中定义了5种权限,它们分别为:
CREATE:创建子节点的权限。READ:获取节点数据和子节点列表的权限。WRITE:更新节点数据的权限。DELETE:删除子节点的权限。ADMIN:设置节点 ACL 的权限。
Watcher机制
Watcher 为事件监听器,是 zk 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 注册 指定的 watcher ,当服务端符合了 watcher 的某些事件或要求则会 向客户端发送事件通知 ,客户端收到通知后找到自己定义的 Watcher 然后 执行相应的回调方法 。

Zookeeper的几个典型应用场景
选主
因为 Zookeeper 的强一致性,能够很好地在保证 在高并发的情况下保证节点创建的全局唯一性 (即无法重复创建同样的节点)。利用这个特性,我们可以 让多个客户端创建一个指定的节点 ,创建成功的就是 master。
可以 让其他不是 master 的节点监听节点的状态 ,比如监听临时节点的父节点,如果子节点个数变了就代表 master 挂了,这个时候 触发回调函数进行重新选举;或者直接监听节点的状态,可以通过节点是否已经失去连接来判断 master 是否挂了等等。
总的来说,可以完全 利用临时节点、节点状态 和 watcher 来实现选主的功能,临时节点主要用来选举,节点状态和watcher 可以用来判断 master 的活性和进行重新选举。

分布式锁
分布式锁的实现方式有很多种,比如 Redis 、数据库 、zookeeper 等。
使用
zookeeper实现互斥锁 —— zk在高并发的情况下保证节点创建的全局唯一性因为创建节点的唯一性,可以让多个客户端同时创建一个临时节点,创建成功的就说明获取到了锁 。
然后没有获取到锁的客户端也像上面选主的非主节点创建一个
watcher进行节点状态的监听,如果这个互斥锁被释放了(获取锁的客户端宕机或主动释放了锁)可以调用回调函数重新获得锁。不需要向
redis那样考虑锁得不到释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了。使用
zookeeper同时实现 共享锁和独占锁 —— 采用有序节点若为读请求(获取共享锁),若 没有比自己更小的节点,或比自己小的节点都是读请求 ,则可以获取到读锁。若比自己小的节点中有写请求 ,则当前客户端无法获取到读锁,只能等待前面的写请求完成。
若为写请求(获取独占锁),若 没有比自己更小的节点 ,则表示客户端可以直接获取到写锁。若发现 有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁 ,等待所有前面的操作完成。
优化:当一个锁得到释放它会通知所有等待的客户端从而造成 羊群效应 。此时可以通过让等待的节点只监听他们前面的节点 —— 让读请求监听比自己小的最后一个写请求节点,写请求只监听比自己小的最后一个节点 。
集群管理
如果需要了解整个集群中有多少机器在工作,对每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等时, zookeeper 的 watcher 和 临时节点能很好的实现这些需求。
可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 watcher 进行状态监控和回调。

注册中心
同样也是让 服务提供者 在 zookeeper 中创建一个临时节点并且将自己的 ip、port、调用方式 写入节点,当 服务消费者 需要进行调用的时候会 通过注册中心找到相应的服务的地址列表(IP端口什么的) ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。
当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机。
