Zookeeper
ZooKeeper 概览
ZooKeeper 是一个开源的分布式协调服务,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
原语: 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。
具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。
ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
这些功能的实现主要依赖于 ZooKeeper 提供的 数据存储+事件监听 功能。ZooKeeper 将数据保存在内存中,性能是不错的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。
- Kafka : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。不过,在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构。
- Hbase : ZooKeeper 为 Hbase 提供确保整个集群只有一个 Master 以及保存和提供 regionserver 状态信息(是否在线)等功能。
- Hadoop : ZooKeeper 为 Namenode 提供高可用支持。
ZooKeeper 特点
- 顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
- 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
- 单一系统映像: 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
- 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
ZooKeeper 应用场景
ZooKeeper通常被用于实现数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
典型的应用场景:
-
命名服务:可以通过 ZooKeeper 的顺序节点生成全局唯一 ID。
zookeeper
是通过 树形结构 来存储数据节点的,那也就是说,对于每个节点的 全路径,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是可自定义的,可设计语义。 -
数据发布/订阅:通过 Watcher 机制 可以很方便地实现数据发布/订阅。当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。
-
分布式锁:通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。分布式锁的实现也需要用到 Watcher 机制.
-
选主:临时节点因为
Zookeeper
的强一致性,能够很好地在保证 在高并发的情况下保证节点创建的全局唯一性 (即无法重复创建同样的节点)。利用这个特性,我们可以 让多个客户端创建一个指定的节点 ,创建成功的就是master
。如果这个master
挂了,代表会话断了,意味着这个节点没了。我们可以 让其他不是master
的节点监听节点的状态 ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表master
挂了,这个时候我们 触发回调函数进行重新选举 ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断master
是否挂了等等。 - 集群管理:如果需要了解整个集群中有多少机器在工作,想对集群中的每台机器的运行时状态进行数据采集,对集群中机器进行上下线操作等等。
zookeeper
天然支持的watcher
和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的watcher
进行状态监控和回调。 - 注册中心:同样也是让 服务提供者 在
zookeeper
中创建一个临时节点并且将自己的ip、port、调用方式
写入节点,当 服务消费者 需要进行调用的时候会 通过注册中心找到相应的服务的地址列表(IP 端口什么的) ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得Eureka
会先试错,然后再更新)。
ZooKeeper 重要概念
Data model(数据模型)
ZooKeeper 数据模型采用层次化的多叉树形结构,每个节点上都可以存储数据,这些数据可以是数字、字符串或者是二级制序列。并且。每个节点还可以拥有 N 个子节点,最上层是根节点以“/”来代表。每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。并且,每个 znode 都一个唯一的路径标识。
强调一句:ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper 给出的上限是每个结点的数据大小最大是 1M。
ZooKeeper 节点路径标识方式和 Unix 文件系统路径非常相似,都是由一系列使用斜杠"/"进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。
znode(数据节点)
每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元, 你要存放的数据就放在上面。
通常将 znode 分为 4 大类:
- 持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
- 临时(EPHEMERAL)节点:临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点只能做叶子节点 ,不能创建子节点。
- 持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如
/node1/app0000000001
、/node1/app0000000002
。 - 临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性
每个 znode 由 2 部分组成:
- stat:状态信息 (Stat 类包含了一个数据节点的所有状态信息字段,包括事务ID(cZxid)、节点创建时间(ctime)和子节点个数(numChildren)等)
- data:节点存放的数据的具体内容
版本(version)
对应于每个 znode,ZooKeeper 都会为其维护一个叫作 Stat 的数据结构,Stat 中记录了这个 znode 的三个相关的版本:
- dataVersion:当前 znode 节点的版本号
- cversion:当前 znode 子节点的版本
- aclVersion:当前 znode 的 ACL 的版本。
ACL(权限控制)
ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。
对于 znode 操作的权限,ZooKeeper 提供了以下 5 种:
- CREATE : 能创建子节点
- READ:能获取节点数据和列出其子节点
- WRITE : 能设置/更新节点数据
- DELETE : 能删除子节点
- ADMIN : 能设置节点 ACL 的权限
其中尤其需要注意的是,CREATE 和 DELETE 这两种权限都是针对 子节点 的权限控制。
对于身份认证,提供了以下几种方式:
- world:默认方式,所有用户都可无条件访问。
- auth :不使用任何 id,代表任何已认证的用户。
- digest :用户名:密码认证方式:username:password 。
- ip : 对指定 ip 进行限制。
Watcher(事件监听器)
Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
会话(Session)
Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watcher 事件通知。
Session 有一个属性叫做:sessionTimeout
,sessionTimeout
代表会话超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在sessionTimeout
规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
另外,在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID
。由于 sessionID
是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID
的,因此,无论是哪台服务器为客户端分配的 sessionID
,都务必保证全局唯一。
ZooKeeper 集群
为了保证高可用,最好是以集群形态来部署 ZooKeeper,只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。通常 3 台服务器就可以构成一个 ZooKeeper 集群。ZooKeeper 官方提供的架构图就是一个 ZooKeeper 集群整体对外提供服务。
上图中每一个 Server 代表一个安装 ZooKeeper 服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 ZAB 协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。
最典型集群模式:Master/Slave 模式(主备模式)。在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
ZooKeeper 集群角色
但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。
ZooKeeper 集群中的所有机器通过一个 Leader 选举过程 来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能.
ZooKeeper 集群 Leader 选举过程
当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入 Leader 选举过程,这个过程会选举产生新的 Leader 服务器。
这个过程大致是这样的:
- Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。
- Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。
- Synchronization(同步阶段) : 同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后准 leader 才会成为真正的 leader。
- Broadcast(广播阶段) : 到了这个阶段,ZooKeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。
ZooKeeper 集群中的服务器状态有下面几种:
- LOOKING:寻找 Leader。
- LEADING:Leader 状态,对应的节点为 Leader。
- FOLLOWING:Follower 状态,对应的节点为 Follower。
- OBSERVING:Observer 状态,对应节点为 Observer,该节点不参与 Leader 选举。
ZooKeeper 集群为啥最好奇数台?
ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用。假如我们的集群中有 n 台 ZooKeeper 服务器,那么也就是剩下的服务数必须大于 n/2。先说一下结论,2n 和 2n-1 的容忍度是一样的,都是 n-1,这应该是一个很简单的数学问题了。
比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。
假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。
综上,没必要增加那一个不必要的 ZooKeeper。
ZooKeeper 选举的过半机制防止脑裂
何为集群脑裂?
对于一个集群,通常多台机器会部署在不同机房,来提高这个集群的可用性。保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群。这时候子集群各自选主导致“脑裂”的情况。
举例说明:比如现在有一个由 6 台服务器所组成的一个集群,部署在了 2 个机房,每个机房 3 台。正常情况下只有 1 个 leader,但是当两个机房中间网络断开的时候,每个机房的 3 台服务器都会认为另一个机房的 3 台服务器下线,而选出自己的 leader 并对外提供服务。若没有过半机制,当网络恢复的时候会发现有 2 个 leader。仿佛是 1 个大脑(leader)分散成了 2 个大脑,这就发生了脑裂现象。脑裂期间 2 个大脑都可能对外提供了服务,这将会带来数据一致性等问题。
过半机制是如何防止脑裂现象产生的?
ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂。
一致性问题
设计一个分布式系统必定会遇到一个问题—— 因为分区容忍性(partition tolerance)的存在,就必定要求我们需要在系统可用性(availability)和数据一致性(consistency)中做出权衡 。这就是著名的 CAP
定理。
Eureka
的处理方式,它保证了 AP(可用性),ZooKeeper
的处理方式,它保证了 CP(数据一致性)。一致性协议和算法
而为了解决数据一致性问题,就出现了很多的一致性协议和算法。比如 2PC(两阶段提交),3PC(三阶段提交),Paxos 算法等等。
Paxos
算法
Paxos
算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致 。
在 Paxos
中主要有三个角色,分别为 Proposer提案者
、Acceptor表决者
、Learner学习者
。Paxos
算法和 2PC
一样,也有两个阶段,分别为 Prepare
和 accept
阶段。
# prepare 阶段
Proposer提案者
:负责提出proposal
,每个提案者在提出提案时都会首先获取到一个 具有全局唯一性的、递增的提案编号 N,即在整个集群中是唯一的编号 N,然后将该编号赋予其要提出的提案,在第一阶段是只将提案编号发送给所有的表决者。Acceptor表决者
:每个表决者在accept
某提案后,会将该提案编号 N 记录在本地,这样每个表决者中保存的已经被 accept 的提案中会存在一个编号最大的提案,其编号假设为maxN
。每个表决者仅会accept
编号大于自己本地maxN
的提案,在批准提案时表决者会将以前接受过的最大编号的提案作为响应反馈给Proposer
。
# accept 阶段
当一个提案被 Proposer
提出后,如果 Proposer
收到了超过半数的 Acceptor
的批准(Proposer
本身同意),那么此时 Proposer
会给所有的 Acceptor
发送真正的提案(你可以理解为第一阶段为试探),这个时候 Proposer
就会发送提案的内容和提案编号。
表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 大于等于 已经批准过的最大提案编号,那么就 accept
该提案(此时执行提案内容但不提交),随后将情况返回给 Proposer
。如果不满足则不回应或者返回 NO 。
Proposer
收到超过半数的 accept
,那么它这个时候会向所有的 acceptor
发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 acceptor
批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要向未批准的 acceptor
发送提案内容和提案编号并让它无条件执行和提交,而对于前面已经批准过该提案的 acceptor
来说 仅仅需要发送该提案的编号 ,让 acceptor
执行提交就行了。
Proposer
如果没有收到超过半数的 accept
那么它将会将 递增 该 Proposal
的编号,然后 重新进入 Prepare
阶段 。paxos 算法的死循环问题
比如说,此时提案者 P1 提出一个方案 M1,完成了 Prepare
阶段的工作,这个时候 acceptor
则批准了 M1,但是此时提案者 P2 同时也提出了一个方案 M2,它也完成了 Prepare
阶段的工作。然后 P1 的方案已经不能在第二阶段被批准了(因为 acceptor
已经批准了比 M1 更大的 M2),所以 P1 自增方案变为 M3 重新进入 Prepare
阶段,然后 acceptor
,又批准了新的 M3 方案,它又不能批准 M2 了,这个时候 M2 又自增进入 Prepare
阶段。。。
就这样无休无止的永远提案下去,这就是 paxos
算法的死循环问题。就允许一个能提案。
ZAB 协议和 Paxos 算法
Paxos 算法应该可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。另外,在 ZooKeeper 的官方文档中也指出,ZAB 协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为 Zookeeper 设计的崩溃可恢复的原子消息广播算法。
ZAB 协议介绍
ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
ZAB 中的三个角色
首先来了解一下在 ZAB
中三个主要的角色,Leader 领导者
、Follower跟随者
、Observer观察者
。
Leader
:集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。Follower
:能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给Leader
。在选举过程中会参与投票,有选举权和被选举权 。Observer
:就是没有选举权和被选举权的Follower
。
在 ZAB
协议中对 zkServer
(即上面我们说的三个角色的总称) 还有两种模式的定义,分别是 消息广播 和 崩溃恢复
ZAB 协议两种基本的模式:崩溃恢复和消息广播
(1)消息广播:当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。 当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。
需要Leader
将写请求 广播 出去,让 Leader
问问 Followers
是否同意更新,如果超过半数以上的同意那么就进行 Follower
和 Observer
的更新(和 Paxos
一样)。ZAB
需要让 Follower
和 Observer
保证顺序性 。为了保障副本之间的数据一致,ZAB协议做了以下几点: 1.所有的写请求都通过 Leader 进行操作,如果 Follower 收到写请求,会转发给 Leader。 2.Leader 针对写请求会生成一个提案,并为这个提案生成一个ZXID(保障一致、顺序。) 3.Followers 在收到提案后 ack 给 Leader。 4.Leader 在收到过半的 Follower ack 后会广播一个 commit 消息。 5.Follower 收到 commit 消息后会比较 ZXID,如果之前没有处理过比 ZXID 大的写请求,那就进行提交。
所以在 Leader
这端,它为每个其他的 zkServer
准备了一个 队列 ,采用先进先出的方式发送消息。由于协议是 通过 TCP
来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。
除此之外,在 ZAB
中还定义了一个 全局单调递增的事务 ID ZXID
,它是一个 64 位 long 型,其中高 32 位表示 epoch
年代,低 32 位表示事务 id。epoch
是会根据 Leader
的变化而变化的,当新 Leader
上位时,年代(epoch
)就变了。而低 32 位可以简单理解为递增的事务 id。定义这个的原因也是为了顺序性,每个 proposal
在 Leader
中生成后需要 通过其 ZXID
来进行排序 ,才能得到处理。
当集群中有机器挂了,我们整个集群如何保证数据一致性?
如果只是 Follower
挂了,而且挂的没超过半数的时候,因为我们一开始讲了在 Leader
中会维护队列,所以不用担心后面的数据没接收到导致数据不一致性。
如果 Leader
挂了那就麻烦了,我们肯定需要先暂停服务变为 Looking
状态然后进行 Leader
的重新选举(上面我讲过了),但这个就要分为两种情况了,分别是 确保已经被 Leader 提交的提案最终能够被所有的 Follower 提交 和 跳过那些已经被丢弃的提案 。
总结
- ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。
- 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。
- ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持 znode 中存储的数据量较小的进一步原因)。
- ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地明显,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)
- ZooKeeper 有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个 znode 被创建了,除非主动进行 znode 的移除操作,否则这个 znode 将一直保存在 ZooKeeper 上。
- ZooKeeper 底层其实只提供了两个功能:① 管理(存储、读取)用户程序提交的数据;② 为用户程序提供数据节点监听服务。