本文最后更新于:星期二, 六月 16日 2020, 3:09 下午
这一周学习了集中式服务Zookeeper,可以和Hadoop配合使用,看的都是一些偏理论的东西,做一下记录
Hadoop学习笔记(三)之Zookeeper
Zookeeper
zookeeper是什么,有什么应用场景
zookeeper是什么
ZooKeeper is a centralized service for maintaining configuration information, naming, providing distributed synchronization, and providing group services. All of these kinds of services are used in some form or another by distributed applications. Each time they are implemented there is a lot of work that goes into fixing the bugs and race conditions that are inevitable. Because of the difficulty of implementing these kinds of services, applications initially usually skimp on them ,which make them brittle in the presence of change and difficult to manage. Even when done correctly, different implementations of these services lead to management complexity when the applications are deployed.
ZooKeeper aims at distilling the essence of these different services into a very simple interface to a centralized coordination service. The service itself is distributed and highly reliable. Consensus, group management, and presence protocols will be implemented by the service so that the applications do not need to implement them on their own. Application specific uses of these will consist of a mixture of specific components of Zoo Keeper and application specific conventions. ZooKeeper Recipes shows how this simple service can be used to build much more powerful abstractions.
—— Zookeeper Wiki
以上是Apache官方对Zookeeper的定位。
简单地说,Zookeeper是一种集中式服务(根据官方文档,centralized service应该翻译为集中式服务,但是维基百科和百度百科却说它是个分布式协调服务,无解)。它可以为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。由于独立的应用程序在实现对分布式管理的功能时容易对各种变化和竞争分析不全面,导致应用程序变得非常脆弱。而Zookeeper的目的就是为这些需要使用分布式协调服务的应用程序提供一个统一易用的接口并提供稳定可靠的分布式协调服务。
具体的应用场景
- 数据发布与订阅(配置中心)
发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZooKeeper节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。
- 应用中用到的一些配置信息放到ZooKeeper上进行集中管理。这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册一个Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的。
- 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在ZooKeeper的一些指定节点,供各个客户端订阅使用。
- 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用来分配收集任务单元,因此需要在ZooKeeper上创建一个以应用名作为path的节点P,并将这个应用的所有机器ip,以子节点的形式注册到节点P上,这样一来就能够实现机器变动的时候,能够实时通知到收集器调整任务分配。
- 系统中有些信息需要动态获取,并且还会存在人工手动去修改这个信息的发问。通常是暴露出接口,例如JMX接口,来获取一些运行时的信息。引入ZooKeeper之后,就不用自己实现一套方案了,只要将这些信息存放到指定的ZooKeeper节点上即可。
(简单的说,Zookeeper只是一种分布式协调服务,适用于处理少量的索引信息,而不是直接用于大量数据的处理)
命名服务
命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端 应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的 机器,提供的服务,远程对象等等——这些我们都可以统称他们为名字。 其中较为常见的就是一些分布式服务框架(如RPC)中的服务地址列表。通过在ZooKeepr里 创建顺序节点,能够很容易创建一个全局唯一的路径,这个路径就可以作为一个名字。 ZooKeeper 的命名服务即生成全局唯一的ID。
(根据我自己的理解,这种服务有点类似于提供一个字典,通过索引键就可以获得对应值的信息,DNS服务器应该可以利用ZooKeeper提供的协调服务实现域名与ip地址的对应)分布式协调服务/通知
ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZooKeeper上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理。
- 另一种心跳检测机制:检测系统和被检测系统之间并不直接关联起来,而是通过ZooKeeper上某个节点关联,大大减少系统耦合。
- 另一种系统调度模式:某系统有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了ZooKeeper上某些节点的状态,而ZooKeeper就把这些变化通知给他们注册Watcher的客户端,即推送系统,于是,作出相应的推送任务。
- 另一种工作汇报模式:一些类似于任务分发系统,子任务启动后,到ZooKeeper来注册一个临时节点,并且定时将自己的进度进行汇报(将进度写回这个临时节点),这样任务管理者就能够实时知道任务进度。
总之,使用ZooKeeper来进行分布式通知和协调能够大大降低系统之间的耦合。
Master选举
针对 Master 选举的需求,通常情况下,我们可以选择常见的关系型数据库中的主键特性来 实现:希望成为 Master 的机器都向数据库中插入一条相同主键ID的记录,数据库会帮我们进行 主键冲突检查,也就是说,只有一台机器能插入成功——那么,我们就认为向数据库中成功插入数据 的客户端机器成为Master。 依靠关系型数据库的主键特性确实能够很好地保证在集群中选举出唯一的一个Master。 但是,如果当前选举出的 Master 挂了,那么该如何处理?谁来告诉我 Master 挂了呢? 显然,关系型数据库无法通知我们这个事件。但是,ZooKeeper 可以做到! 利用 ZooKeepr 的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够 保证全局唯一性,即 ZooKeeper 将会保证客户端无法创建一个已经存在的 数据单元节点。 也就是说,如果同时有多个客户端请求创建同一个临时节点,那么最终一定只有一个客户端 请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行 Master 选举了。 成功创建该节点的客户端所在的机器就成为了 Master。同时,其他没有成功创建该节点的 客户端,都会在该节点上注册一个子节点变更的 Watcher,用于监控当前 Master 机器是否存 活,一旦发现当前的Master挂了,那么其他客户端将会重新进行 Master 选举。 这样就实现了 Master 的动态选举。(Master的动态选举极大的提高了Zookeeper的可靠性)分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式
分布式锁又分为排他锁和共享锁两种
- 排它锁
ZooKeeper如何实现排他锁?- 定义锁
ZooKeeper 上的一个机器节点可以表示一个锁 - 获得锁
把ZooKeeper上的一个节点看作是一个锁,获得锁就通过创建临时节点的方式来实现。
ZooKeeper 会保证在所有客户端中,最终只有一个客户端能够创建成功,那么就可以
认为该客户端获得了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock
节点上注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。 - 释放锁
因为锁是一个临时节点,释放锁有两种方式:- 当前获得锁的客户端机器发生宕机或重启,那么该临时节点就会被删除,释放锁。
- 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除,释放锁。
无论在什么情况下移除了lock节点,ZooKeeper 都会通知所有在 /exclusive_lock 节点上注册了节点变更 Watcher 监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复『获取锁』过程。
- 定义锁
- 共享锁
共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个 EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用 exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。
zookeeper的watch机制
Zookeeper的watch机制由Watcher类实现。顾名思义,Watcher,即监听器。在刚开始学习的时候,会发现对Zookeeper的架构没有概念的话,会不好理解Watcher在其中起到什么样的作用。于是我先行了解了一下Zookeeper的架构。
ZooKeeper的基本架构
- 每个Server在内存中存储了一份数据;
- Zookeeper启动时,将从实例中选举一个leader(Paxos协议);
- Leader负责处理数据更新等操作(Zab协议);
- 一个更新操作成功,当且仅当大多数Server在内存中成功修改数据。
所以说,Watcher就是一个客户端注册在节点上的监听器,当节点中的数据发生变化时,服务端就会将相对应的事件通知到客户端上。Zookeeper中所有的读操作—— getData(), getChildren(), exists()都可以设置监视。 并且,事件具有one-time trigger(一次性触发)的特性,于是监听事件又可以理解为一次性的触发器。
watch事件
官方对Watch事件的定义如下:
a watch event is one-time trigger, sent to the client that set the watch, which occurs when the data for which the watch was set changes
其中有三个关键字:
(一次性触发)One-time trigger
当设置监视的数据发生改变时,该监视事件会被发送到客户端,例如,如果客户端调用了 getData(“/znode1”, true) 并且稍后 /znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件,而如果 /znode1 再一次发生了变化,除非客户端再次对 /znode1 设置监视,否则客户端不会收到事件通知。(发送至客户端)Sent to the client
Zookeeper 客户端和服务端是通过 socket 进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的,Zookeeper 本身提供了保序性(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的 znode 发生了变化(a client will never see a change for which it has set a watch until it first sees the watch event). 网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序。
-(被设置watch的数据)The data for which the watch was set
这意味着 znode 节点本身具有不同的改变方式。你也可以想象 Zookeeper 维护了两条监视链表:数据监视和子节点监视(data watches and child watches) getData() and exists() 设置数据监视,getChildren() 设置子节点监视。 或者,你也可以想象 Zookeeper 设置的不同监视返回不同的数据,getData() 和 exists() 返回 znode 节点的相关信息,而 getChildren() 返回子节点列表。因此, setData() 会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的 create() 操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视。一次成功的 delete() 操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch。
Zookeeper 中的监视是轻量级的,因此容易设置、维护和分发。当客户端与 Zookeeper 服务器端失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说 这通常是透明的。只有一种情况会导致监视事件的丢失,即:通过 exists() 设置了某个 znode 节点的监视,但是如果某个客户端在此 znode 节点被创建和删除的时间间隔内与 zookeeper 服务器失去了联系,该客户端即使稍后重新连接 zookeeper服务器后也得不到事件通知。
Zookeeper Watcher的运行机制
- Watch是轻量级的,其实就是本地JVM的Callback,服务器端只是存了是否有设置了Watcher的布尔类型。(源码见:org.apache.zookeeper.server.FinalRequestProcessor)
- 在服务端,在FinalRequestProcessor处理对应的Znode操作时,会根据客户端传递的watcher变量,添加到对应的ZKDatabase(org.apache.zookeeper.server.ZKDatabase)中进行持久化存储,同时将自己NIOServerCnxn做为一个Watcher callback,监听服务端事件变化
- Leader通过投票通过了某次Znode变化的请求后,然后通知对应的Follower,Follower根据自己内存中的zkDataBase信息,发送notification信息给zookeeper客户端。
- Zookeeper客户端接收到notification信息后,找到对应变化path的watcher列表,挨个进行触发回调。
流程图如下
![](https://cdn.diaossama.work/pic/Zookeeper Watcher运行机制.png)
zookeeper用了什么一致性算法
zookeeper用于实现一致性的算法是ZAB协议
参考资料:https://blog.csdn.net/weixin_36145588/article/details/78470482
ZAB协议
ZAB协议,全称ZooKeeper Atomic Broadcast protocol,它分为如下四个阶段(看完参考资料,我用自己的话简单地进行一个总结)
1. Faster Leader Election
Leader(Master) 是Zookeeper服务器集群中的一个重要节点,它起到一个带领其他服务器Follower的作用,所有其他节点上的数据信息更新都以Zookeeper为准,它也是Zookeeper实现一致性的核心
所以,如何高效地在众多节点中选举出Leader便是很重要的一环。
在Leader的选举过程中需要关注的两个要点:
- 所有机器刚启动时进行leader选举过程
- 如果leader选举完成,刚启动起来的server怎么识别到leader选举已完成
在所有机器刚刚启动时,每个机器都向其他机器通知自己的id(发送投票信息),如果一台机器的id比另一台小,则将id改成大的那一个,当某个id票数过半,则其对应的server就被推选为Leader
当Leader选举已经完成,有新的机器加入时,则涉及到server状态的判断。
server的状态分为: - LOOKING:进入leader选举状态
- FOLLOWING:leader选举结束,进入follower状态
- LEADING:leader选举结束,进入leader状态
- OBSERVING:处于观察者状态
如果新加入的机器收到发来投票的server的状态是FOLLOWING或LEADING,说明leader选举已经完成,发过来的投票就是leader的信息。
这里有一个问题:参考资料中提到,“这里就需要判断发过来的投票是否在recvset或者outofelection中过半了,同时还要检查leader是否给自己发送过投票信息,从投票信息中确认该leader是不是LEADING状态” 但是此时leader选举已经完成,为什么还需要判断投票信息是否过半?这里没有搞懂 。2. Recovery Phase
一旦leader选举完成,就开始进入恢复阶段,就是follower要同步leader上的数据信息。
- 通信初始化
恢复阶段首先要重新建立通信,此时Leader会创建一个SeverSocket来接受follower的链接,而且Leader会为每一个连接用一个LearnerHandler线程进行服务。 - 更新事务的处理轮次
server中会存放一个名为peerEpoch的数据,用于标记处理事务的轮次。Leader会将所有Follower的该数据收集起来,取其中最大的那一个,同步到其他Follower上。更新PeerEpoch可以防止旧Leader向follower放命令并执行,防止出现不一致性 - 已经处理的事务议案的同步
先前已经处理完成的事务会在leader中留下数据标记,follower向leader同步时,则利用这几个标记,判断自身缺少或者多出的事务日志,进行补充和删除,保持与Leader的一致,也保证整个系统的一致性 - 未处理事务议案的同步
与同步已处理事务议案的过程相似,不再赘述 - 将LearnerHandler加入到正式follower列表中
意味着该LearnerHandler正式接受请求。即此时leader可能正在处理客户端请求,leader针对该请求发出一个议案,然后对该正式follower列表才会进行执行发送工作。这里有一个地方就是:
上述我们在比较lastProcessedZxid和minCommittedLog和maxCommittedLog差异的时候,必须要获取leader内存数据的读锁,即在此期间不能执行修改操作,当欠缺的数据包已经补上之后(先放置在一个队列中,异步发送),才能加入到正式的follower列表,否则就会出现顺序错乱的问题。
同时也说明了,一旦一个follower在和leader进行同步的过程(这个同步过程仅仅是确认要发送的议案,先放置到队列中即可等待异步发送,并不是说必须要发送过去),该leader是暂时阻塞一切写操作的。
对于快照方式的同步,则是直接同步写入的,写入期间对数据的改动会放在上述队列中的,然后当同步写入完成之后,再启动对该队列的异步写入。
上述的要理解的关键点就是:既要不能漏掉,又要保证顺序 - LearnerHandler发送Leader.NEWLEADER以及Leader.UPTODATE命令
该命令是在同步结束之后发的,follower收到该命令之后会执行一次版本快照等初始化操作,如果收到该命令的ACK则说明follower都已经完成同步了并完成了初始化。
leader开始进入心跳检测过程,不断向follower发送心跳命令,不断检是否有过半机器进行了心跳回复,如果没有过半,则执行关闭操作,开始进入leader选举状态。
LearnerHandler向对应的follower发送Leader.UPTODATE,follower接收到之后,开始和leader进入Broadcast处理过程。3. Broadcast
整个Broadcast的处理过程可以描述为:
- Leader针对客户端的事务请求(leader为该请求分配了zxid),创建出一个议案,并将该议案对应的id存放到待处理列表中
- Leader开始向所有的follower发送该议案,如果过半的follower回复OK的话,则Leader认为可以提交该议案,则将该议案从待处理列表中删除,然后存放到等待应用列表中
- Leader对该议案进行提交,会向所有的follower发送提交该议案的命令,Leader自己也开始执行提交过程,会将该请求的内容应用到ZooKeeper的内存树中,然后更新lastProcessedZxid为该请求的zxid,同时将该请求的议案存放到上述committedLog,同时更新maxCommittedLog和minCommittedLog
- Leader就开始向客户端进行回复,然后就会将该议案从等待应用列表中删除
zookeeper如何高可用
要搞清楚zookeeper如何高可用,首先就要知道高可用的定义。
高可用性,顾名思义,就是高的可用性,也就是通过设计减少系统不能够提供服务的时间。
zookeeper实现高可用依靠的便是上面所讲的在server中实现的Leader(Master)-Follower机制,在服务器的集群中,只要不是过半的服务器全部宕机,即使Leader服务器宕机,其他服务器也可以重新选举Leader,保证系统继续运行。并且由于zookeeper的高一致性算法,也保证了数据的完整性。这便是我理解的zookeeper实现高可用的方式。
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!