ZooKeeper 是一个开源的分布式协调服务,ZooKeeper 由雅虎研究院开发,是Google Chubby 的开源实现,由于最初雅虎公司的内部研究小组的项目大多以动物的名字命名,所以后来就以Zookeeper(动物管理员)来命名了,后来托管到 Apache,于2010年11月正式成为 Apache 的顶级项目。
ZooKeeper目标他的目标是可以提供高性能、高可用和顺序访问控制的能力,同时也是为了解决分布式环境下数据一致性的问题。
高性能ZooKeeper 将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,尤其适用于以读为主的应用场景
高可用ZooKeeper 一般以集群的方式对外提供服务,一般 3 ~ 5 台机器就可以组成一个可用的 Zookeeper 集群了,每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都相互保持着通信。只要集群中超过一般的机器都能够正常工作,那么整个集群就能够正常对外服务
严格顺序访问对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反映了所有事务 *** 作的先后顺序
ZooKeeper 的五大特性ZooKeeper一般以集群的方式对外提供服务,一个集群包含多个节点,每个节点对应一台ZooKeeper服务器,所有的节点共同对外提供服务,整个集群环境对分布式数据一致性提供了全面的支持,具体包括以下五大特性:
全局数据一致每个服务端保存一份相同的数据副本,客户端无论连接到哪个服务端,数据都是一致的。
高可用性集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
实时性当某个请求被成功处理后,ZooKeeper仅仅保证在一定的时间段内,客户端最终一定能从服务端上读取到最新的数据状态,即ZooKeeper保证数据的最终一致性。
顺序性从同一个客户端发起的请求,最终将会严格按照其发送顺序进入 ZooKeeper 中。
原子性所有请求的响应结果在整个分布式集群环境中具备原子性,即要么整个集群中所有机器都成功的处理了某个请求,要么就都没有处理,绝对不会出现集群中一部分机器处理了某一个请求,而另一部分机器却没有处理的情况。
ZooKeeper集群角色在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。如下图所示:
集群通过一个 Leader 选举过程从所有的机器中选举一台机器作为 Leader,Leader 能为客户端提供读和写服务,
Leader 服务器是整个集群工作机制的核心,主要工作:
- 事务请求的唯一调度者和处理者,保证集群事务处理的顺序性
- 集群内部各服务器的调度者
除了除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写 *** 作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。
Zookeeper 建议集群节点个数为奇数,只要超过一半的机器能够正常提供服务,那么整个集群都是可用的状态。
数据节点 - ZNodeZooKeeper 内部拥有一个树状的内存模型,类似文件系统,只是在 ZooKeeper 中将这些目录与文件系统统称为ZNode,ZNode 是 ZooKeeper 中数据的最小单元,每个 ZNode 上可以保存数据,还可以挂载子节点,因此构成了一个层次化的命名空间。
ZooKeeper 中使用斜杠 / 分割的路径表示 ZNode 路径,斜杠 / 表示根节点
而 Znode 又分为持久节点、持久顺序节点、临时节点、临时顺序节点 4 大类。
持久节点是指只要被创建,除非主动移除,否则都应该一直保存在 ZooKeeper 中。
临时节点不同的是,他的生命周期和客户端 Session 会话一样,会话失效,那么临时节点就会被移除。
临时顺序节点和持久顺序节点,除了基本的特性之外,子节点的名称还具有有序性。
会话SessionSession 指的是 ZooKeeper 服务器与客户端会话。在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接。客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。
通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watch 事件通知。 Session 的 sessionTimeout 值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在 sessionTimeout 规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID。由于 sessionID 是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。
事件监听器Wather在 ZooKeeper 中,引入 Watcher 机制来实现分布式数据的发布/订阅功能。ZooKeeper 允许客户端向服务器注册一个 Watcher 监听,当服务器的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能 。
Watcher 机制分为以下 3 个过程:
- 客户端注册Watcher
- 在创建一个ZooKeeper客户端对象实例时,可以向构造方法中传入一个Watcher,这个Watcher将作为整个ZooKeeper会话期间的默认Watcher,一致保存在客户端,并向ZooKeeper服务器注册Watcher 客户端并不会把真实的Watcher对象传递到服务器,仅仅只是在客户端请求中使用boolean类型属性进行标记,降低网络开销和服务器内存开销。
- 服务端处理Watcher
- 服务端执行数据变更,当Watcher监听的对应数据节点的数据内容发生变更,如果找到对应的Watcher,会将其提取出来,同时从管理中将其删除,触发Watcher,向客户端发送通知。
- 客户端回调Watcher
- 客户端获取通知,识别出事件类型,从相应的Watcher存储中去除对应的Watcher。
为了有效保障 ZooKeeper 中数据的安全,避免因误 *** 作而带来数据随意变更导致分布式系统异常,ZooKeeper 提供了一套完善的 ACL 权限控制机制来保障数据的安全。
包含以下 5 种:
- CREATE:子节点的创建权限,允许授权对象在该数据节点下创建子节点
- DELETE:子节点的删除权限,允许授权对象在该数据节点下删除子节点
- READ:数据节点的读取权限,允许授权对象访问该数据节点并读取数据节点的内容或子节点列表
- WRITE:数据节点的更新权限,允许授权对象对该数据节点进行更新 *** 作
- ADMIN:数据节点的管理权限,允许授权对象对该数据节点进行 ACL相关的设置 *** 作
这个其实是 ZooKeeper 很经典的一个用法,简单来说,就好比 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。
这里有个问题是怎么让系统 A 知道系统 B 已经成功对库存进行了修改?
这里就用 ZooKeeper 来做分布式协调,具体包括下面 3 个步骤:
- 系统 A 创建一个订单 orderId = 1,同时在 zk 中对 orderId=1 的 node 注册一个监听
- 系统 B 更新订单之后修改 A 订阅的 node 的状态,比如说约定好修改为 finish_update
- 系统 A 发现它订阅的节点发生了变化,就知道系统 B 成功对库存
这里举个例子:比如说现在连续有两个请求要对一个数据进行 *** 作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 ZooKeeper 分布式锁,一个机器接收到了请求之后先获取 ZooKeeper 上的一把分布式锁,就是可以去创建一个 Znode,接着执行 *** 作;然后另外一个机器也尝试去创建那个 Znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。
元数据/配置信息管理zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 ZooKeeper 来做一些元数据、配置信息的管理,dubbo 也推荐使用 ZooKeeper 来作为注册中心。
HA高可用性这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 ZooKeeper 来开发 HA 高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过 ZooKeeper 感知到切换到备用进程。
Zookeeper 可以提供分布式数据的发布/订阅功能,依赖的就是Wather监听机制。
简单的理解就是客户端会对某个 Znode 注册一个 watcher 事件,当该 Znode 发生变化时,这些客户端会收到 ZooKeeper 的通知。
他有以下 4 个特性:
- 一次性:一旦一个 Wather 触发之后,ZooKeeper 就会将它从存储中移除,如果还要继续监听这个节点,就需要我们在客户端的监听回调中,再次对节点的监听 Wather 事件设置为 True。否则客户端只能接收到一次该节点的变更通知
- 客户端串行:客户端的 Wather 回调处理是串行同步的过程,不要因为一个 Wather 的逻辑阻塞整个客户端
- 轻量:Wather 通知的单位是 WathedEvent,只包含通知状态、事件类型和节点路径,不包含具体的事件内容,具体的时间内容需要客户端主动去重新获取数据
- 异步: ZooKeeper 服务器发送 Wather 的通知事件到客户端是异步的,不能期望能够监控到节点每次的变化,ZooKeeper 只能保证最终的一致性,而无法保证强一致性。
主要流程如下图:
- 客户端向服务端注册 Wather 监听
- 保存 Wather 对象到客户端本地的 WatherManager 中
- 服务端 Wather 事件触发后,客户端收到服务端通知,从 WatherManager 中取出对应 Wather 对象执行回调逻辑
ZooKeeper 是通过 Zab (ZooKeeper Atomic Broadcast,ZooKeeper 原子广播协议)协议来保证分布式事务的最终一致性,类似 2PC 两阶段提交的过程
由于Zookeeper只有 Leader 节点可以写入数据,如果是其他节点收到写入数据的请求,则会将之转发给 Leader 节点。
流程如下:
- 客户端的写请求进来之后,Leader 会将写请求包装成 Proposal 事务,并添加一个递增事务 ID,也就是 Zxid,Zxid 是单调递增的,以保证每个消息的先后顺序。
- 广播这个 Proposal 事务,Leader 节点和 Follower 节点是解耦的,通信都会经过一个先进先出的消息队列,Leader 会为每一个 Follower 服务器分配一个单独的 FIFO 队列,然后把 Proposal 放到队列中。
- Follower 节点收到对应的 Proposal 之后会把它持久到磁盘上,当完全写入之后,发一个 ACK 给 Leader。
- 当 Leader 收到超过半数 Follower 机器的 ack 之后,会提交本地机器上的事务,同时开始广播 commit, Follower 收到 commit 之后,完成各自的事务提交。
Zab 包含两种基本模式,崩溃恢复和消息广播。
整个集群服务在启动、网络中断或者重启等异常情况的时候,首先会进入到崩溃恢复状态,此时会通过选举产生Leader 节点,当集群过半的节点都和 Leader 状态同步之后,Zab 就会退出恢复模式。之后,就会进入消息广播的模式。
这里顺便说一下什么是 Zxid?
Zxid 是 Zab 协议的一个事务编号,Zxid 是一个 64 位的数字,其中低 32 位是一个简单的单调递增计数器,针对客户端每一个事务请求,计数器加 1;而高 32 位则代表 Leader 周期年代的编号。
这里 Leader 周期的英文是 epoch,可以理解为当前集群所处的年代或者周期
每当有一个新的 Leader 选举出现时,就会从这个 Leader 服务器上取出其本地日志中最大事务的 Zxid,并从中读取 epoch 值,然后加 1,以此作为新的周期 ID。总结一下,高 32 位代表了每代 Leader 的唯一性,低 32 位则代表了每代 Leader 中事务的唯一性。
ZooKeeper 如何进行 Leader 选举的?Leader的选举可以分为两个方面,同时选举主要包含事务 zxid 和 myid,节点主要包含LEADINGFOLLOWINGLOOKING 3个 状态。
LEADING:说明此节点已经是 Leader 节点,处于领导者地位的状态,差不多就是一般集群中的 master。但在ZooKeeper 中,只有 Leader 才有写权限,其他节点(FOLLOWING)是没有写权限的,可以读
LOOKING:选举中,正在寻找 Leader,即将进入 Leader 选举流程中
FOLLOWING:跟随者,表示当前集群中的 Leader 已经选举出来了,主要具备以下几个功能点
- 向leader发送请求(PING消息、REQUEST消息、ACK消息、RevalIDATE消息)
- 接收leader消息并进行处理
- 接收client发送过来的请求,如果为写请求,会发送给Leader进行投票处理,然后返回client结果
选举分为两种情况:
- 服务启动期间的选举
- 服务运行期间的选举
服务启动期间的选举:
- 首先,每个节点都会对自己进行投票,然后把投票信息广播给集群中的其他节点
- 节点接收到其他节点的投票信息,然后和自己的投票进行比较,首先zxid较大的优先,如果zxid相同那么则会去选择myid更大者,此时大家都是LOOKING的状态
- 投票完成之后,开始统计投票信息,如果集群中过半的机器都选择了某个节点机器作为leader,那么选举结束
- 最后,更新各个节点的状态,leader改为LEADING状态,follower改为FOLLOWING状态
服务运行期间的选举:
如果开始选举出来的leader节点宕机了,那么运行期间就会重新进行leader的选举。
- leader宕机之后,非observer节点都会把自己的状态修改为LOOKING状态,然后重新进入选举流程
- 生成投票信息(myid,zxid),同样,第一轮的投票大家都会把票投给自己,然后把投票信息广播出去
- 接下来的流程和上面的选举是一样的,都会优先以zxid,然后选择myid,最后统计投票信息,修改节点状态,选举结束
那实际上 ZooKeeper 在选举之后,Follower 和 Observer(统称为 Learner)就会去向 Leader 注册,然后就会开始数据同步的过程。
ZooKeeper 中数据同步一共分为 4 类,如下:
- 直接差异化同步 DIFF同步
- 先回滚再差异化同步 TRUNC+DIFF同步
- 仅回滚同步 TRUNC同步
- 全量同步 SNAP同步
不同的场景,会有不同的数据同步方式,具体选择哪种方式,还需要参考以下三个参数,根据这三个参数的大小对比结果,选择对应的数据同步方式。
- PeerLastZxid:Learner服务器最后处理的ZXID
- minCommittedLog:Leader提议缓存队列中最小ZXID
- maxCommittedLog:Leader提议缓存队列中最大ZXID
上图中 Follower 最后处理的 zxid 即为 0x00000004,Leader 服务器提交在队列中的最小 zxid 为 0x00000003,最大 zxid 为0x00000005,因此 minCommittedLog 为 0x500000003,maxCommittedLog 为 0x500000005,如果PeerLastZxid 在 minCommittedLog 和 maxCommittedLog 之间,那么则说明 Learner 服务器还没有完全同步最新的数据。
接下来 Follower 和 Leader 之间会进行如下图所示的多次数据包通信:
流程简单一点就是:
- Learner 在注册的最后阶段,会给 Leader 发送 ACKEPOCH 数据包,将当前 Learner 的纪元 currentEpoch 和最新事务序号 lastZxid 发送过去,告诉 Leader 自己的状态
- 在确认了需要使用DIFF直接差异化同步后,Leader 会发送 DIFF 指令给 Learner,代表开始差异化同步,然后把差异数据(从 PeerLastZxid 到maxCommittedLog 之间的数据)提议 proposal 发送给 Learner
- 发送完成之后发送一个 NEWLEADER 命令给 Learner,同时 Learner 返回 ACK 表示已经完成了同步
- 接着等待集群中过半的 Learner 响应了 ACK 之后,就发送一个 UPTODATE 命令,Learner 返回 ACK,同步流程结束
这个设置针对的是一个异常的场景,这种场景是比较特殊的情况,简单来说就是,当 Leader 将事务提交到本地事务日志中后,正准备将 proposal 发送给其他的 Follower 进行投票时突然宕机,这个时候 ZooKeeper 集群会选取出新的 Leader 对外服务,并且可能提交了几个事务,此后当老 Leader 再次上线,新 Leader 发现它身上有自己没有的事务,就需要回滚抹去老 Leader 上自己没有的事务,再让老 Leader 同步完自己新提交的事务,这就是TRUNC + DIFF 的场景。
如上图所示,当 Leader 准备将 zxid 为 0x000003 的 proposal 发送 Learner 投票就宕机了,导致 Leader 上会多出一条未在集群同步的数据。
此时选取了新的 Leader,并且 epoch 在上次的基础上加 1,并且新 Leader 提交了两个事务,zxid 分别为 0x1000001 和 0x1000002。
当老 Leader 重新上线后,新 Leader 发现它身上有一个 0x000003 事务记录是自己没有的,这个时候对于老 Leader来说,peerLastZxid 为 0x000003,而 minCommittedLog 为 0x 000001,maxCommittedLog 为 0x1000002,peerLastZxid 在 minCommittedLog 和 maxCommittedLog 之间。这个时候新 Leader 会发送 TRUNC 指令给这个老Leader ,让它截取一部分事务记录,这样老 Leader 会截取到最靠近 peerLastZxid 同时又存在于提议缓存队列的事务,即截取掉 0x000003 的事务记录。
截取完成后,后面就是DIFF差异化同步了。
仅回滚同步 TRUNC同步针对 PeerLastZxid 大于 maxCommittedLog 的场景,流程和上述一致,事务将会被回滚到 maxCommittedLog 的记录。
这个其实就更简单了,也就是你可以认为 TRUNC + DIFF 中的例子,新的 Leader B 没有处理提议,所以 B 中minCommittedLog = 1,maxCommittedLog = 3。
所以 A 的 PeerLastZxid = 4就会大于 maxCommittedLog 了,也就是 A 只需要回滚就行了,不需要执行差异化同步DIFF 了。
全量同步 SNAP 同步SNAP全量同步在两种情况下会发生:
- PeerLastZxid 小于 minCommittedLog
- Leader 服务器上没有提议缓存队列,并且 PeerLastZxid 不等于 Leader 的最大 zxid
这两种情况下 Learner 和提议缓存队列之间,要么事务有不重叠的地方,要么无法使用提议缓存队列,因此只能使用 SNAP 全量同步。
全量同步就是将 Leader 上的全量内存数据都同步到 Learner,Leader 会先给 Learner 发送一个 SNAP 指令,然后Leader 会准备数据,从内存数据库中获取全量的数据节点和会话超时时间记录器后,将其序列化后发送给 Learner,Learner 接收到后对其进行反序列化后存储内存数据库中,完成全量同步。
那么有可能会出现数据不一致的问题吗? 查询不一致因为Zookeeper是过半成功即代表成功,假设我们有5个节点,如果123节点写入成功,如果这时候请求访问到4或者5节点,那么有可能读取不到数据,因为可能数据还没有同步到4、5节点中,也可以认为这算是数据不一致的问题。
解决方案可以在读取前使用sync命令。
leader未发送proposal宕机这也就是数据同步说过的问题。
leader刚生成一个proposal,还没有来得及发送出去,此时leader宕机,重新选举之后作为follower,但是新的leader没有这个proposal。
这种场景下的日志将会被丢弃。
leader发送proposal成功,发送commit前宕机如果发送proposal成功了,但是在将要发送commit命令前宕机了,如果重新进行选举,还是会选择zxid最大的节点作为leader,因此,这个日志并不会被丢弃,会在选举出leader之后重新同步到其他节点当中。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)