美文网首页
从PAXOS到ZOOKEEPER分布式一致性原理与实践:zook

从PAXOS到ZOOKEEPER分布式一致性原理与实践:zook

作者: peareaden | 来源:发表于2021-01-04 17:19 被阅读0次

    本文内容为《从PAXOS到ZOOKEEPER分布式一致性原理与实践》一书学习笔记。本文主要概述第六章ZooKeeper典型应用场景的内容。

    数据发布/订阅

    定义:发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,从而实现配置信息的集中式管理和数据的动态更新。

    发布/订阅系统一般有两种模式,推(Push)模式和拉(Pull)模式。ZooKeeper是推拉相结合的模式,客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,服务端就会向对应的客户端发送Water事件通知,客户端收到这个消息通知后,需要主动到服务端获取最新的数据。

    全局配置信息有以下几个特征:数据量小,数据内容在运行时会动态变化,集群中各机器共享,配置一致。通常做法是把配置信息存储在本地配置文件或内存变量里。本地配置方式:系统在应用启动时读取到本地磁盘中一个文件进行初始化,并在运行过程中定时对文件进行读取,以此来检测文件内容的变更;需要更新配置信息时修改配置文件即可。另外一种借助内存变量的方式,以Java为例,可以采用JMX方式来对更新系统运行时内存变量。但当机器规模变大且配置信息变更越来越频繁后,这两种方式就不适用了。

    如果将配置信息存放到ZooKeeper上进行集中管理,应用在启动时都会主动到ZooKeeper服务端上进行一次配置消息的获取,同时在指定节点上注册一个Watcher监听。这样一旦配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而实时获取最新配置信息。

    配置存储:将初始化配置存储到ZooKeeper中一个数据节点里去,将需要集中管理的配置信息写入到该数据节点。

    配置获取:集群中每台机器在启动阶段,都会从上面提到的ZooKeeper配置节点读取数据库信息,同时客户端还需要在该配置节点上注册一个数据变更的Water监听,一旦发生节点数据变更,所有订阅的客户端都能获取到数据变更通知。

    配置变更:系统运行中如果发生数据库切换等需要进行配置变更的情况,只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能把数据变更的通知发送到各个客户端,每个客户端在接收到这个通知后重新进行最新数据的获取。

    命名服务

    定义:分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等,这些都可以被称为名字,例如分布式服务框架RPC、RMI中的服务地址列表。客户端用户可以根据指定名字来获取资源的实体、服务地址和提供者的消息。

    UUID是非常不错的全局唯一ID生成方式,能非常简便的保证分布式环境中的唯一性。但它的缺陷是长度过长和含义不明。

    ZooKeeper利用顺序子节点的特性可以生成全局唯一ID。


    分布式协调通知

    协调者负责控制整个系统的运行流程,例如分布式事务的处理、机器间的互相协调。引入协调者有助于将分布式协调的职责从应用中分离出来,减少系统之间的耦合性,提高系统的可扩展性。ZooKeeper特有的Watcher机制与异步通知机制能很好实现不同系统之间的协调与通知。基于ZooKeeper实现分布式协调与通知,不同客户端对ZooKeeper上同一数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点);一旦发生变化所有订阅的客户端都能接收到相应Watcher通知。

    分布式协调通知有以下场景:

    1. 数据库复制处理
      利用临时顺序子节点和Watcher特性,实现任务服务器的热备份。
      为了应对复制任务故障或复制任务所在主机故障,复制组件采用热备份(同一复制任务部署在不同主机上,即任务机器),主、备任务机器通过ZooKeeper互相检测运行健康状况。
      复制任务集群所有机器(即所有任务机器)都要在ZooKeeper上注册,在任务节点上上创建临时顺序子节点,创建完成后每台任务机器都可以获取到一个节点名和所有子节点列表,通过判断可以得出自己是否是所有子节点中序号最小的,小序号优先获得复制任务,为RUNNING状态,其他为STANDBY。如果RUNNING挂了,节点变更的Watcher通知,开始进行热备切换(新一轮RUNNING选举,还是小序号优先获得复制任务)

    2. 分布式系统机器间通信方式
      系统机器间的通信分三种:心跳检测、工作进度汇报、系统调度。
      心跳检测:分布式环境中不同机器之间需要检测到彼此是否在正常运行。基于ZooKeeper的临时节点特性,可以让不同的机器都在ZooKeeper的一个指定节点下创建临时节点(检测到离线时会自动删除,不需要人工干预),不同的机器之间可以通过这个临时节点判断对应的客户机器是否存活,检测系统和被检测系统之间并不需要直接相关联,而是通过ZooKeeper上的某个节点进行关联,减少了系统耦合。代码可参考:链接
      工作进度汇报:在任务分发系统中,任务被分发到不同的机器上执行,需要实时地将自己的任务执行进度汇报给分发系统。在ZooKeeper上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,可以实现两个功能:通过判断临时节点是否存在来确定任务机器是否存活(其实就是心跳检测);各个任务机器会实时地将自己的任务执行进度写到临时节点上去,以便中心系统能够实时的获取到任务的执行进度。
      任务调度:分布式系统由控制台和一些客户端系统两部分组成,控制台负责将一些指令信息发送给所有的客户端,以控制它们相应的业务逻辑。后台管理人员在控制台上做的操作时实际上就是修改了ZooKeeper某些节点的数据,ZooKeeper进一步把这些数据变更以事件通知的形式发送给了对应的订阅客户端。

    Master选举

    分布式最核心的特性就是能够将具有独立计算能力的系统单元部署在不同机器上,构成一个完整的分布式系统,Master用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权。

    Master选举的需求:在集群中所有机器中选举出一台作为Master。通常我们可以利用关系数据库的主键特性来实现:集群中所有机器都向数据库中插入一条相同主键ID的记录,数据库会帮助我们自动进行主键冲突检查,所有进行插入操作的客户端机器中,只有一台机器可以成功,可以认为向数据库中成功插入数据的客户端机器即Master。但如果Master挂了,关系型数据库是没法通知这个事件的,所以引入ZooKeeper。

    利用ZooKeeper的强一致性,能保证在分布式高并发情况下节点的创建一定能保证全局唯一性,ZooKeeper保证客户端无法重复创建一个已经存在的数据节点。同时有多个客户端请求在某个节点下创建同一节点(临时节点),最终只有一个客户端请求能创建成功(Master)。其他客户端则在这个节点上注册一个子节点变更的Watcher,用于监控当前的Master机器是否存活(Master挂了则子节点没了),一旦Master挂了,则其他客户端得到事件通知,重新进行Master选举。

    分布式锁

    分布式锁是控制分布式系统之间同步访问共享资源的一种方式。不同的系统或同一系统的不同主机之间共享了一个或一组资源,访问这些资源的时候,需要一些互斥手段来防止彼此之间的干扰。

    通常我们依赖关系型数据库固有的排他性来实现不同进程之间的互斥,这是一种很简便的分布式锁实现方式。然而目前大型分布式系统的性能瓶颈都集中在数据库操作上,如果上层业务再给数据库添加一些额外的锁,例如行锁、表锁、事务处理,会让数据库不堪重负。
    ZooKeeper实现的分布式锁有两种:排他锁和共享锁。

    1. 排他锁(Exclusive Locks, X锁):又称写锁或独占锁。事务T1对数据对象O1加上了排他锁,那么整个加锁期间,只允许事务T1对数据对象O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,直到T1释放排他锁。排他锁的核心是保证当前有且仅有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能被通知到。



      ZooKeeper实现排他锁:(我的理解是类似Master选举。)
      获取锁时:所有客户端试图通过调用create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock,最终只有一个客户端创建成功(该客户端获取了锁),没有获取到锁的客户端需要在/exclusive_lock节点下注册一个子节点变更的Watcher监听,实时监听到lock节点的变更情况。
      释放锁时:两种情况释放锁:一是获取锁的客户端机器发生宕机,ZooKeeper上这个临时节点就被移除;二是正常执行完逻辑后,客户端主动将自己创建的临时节点删除。无论哪种情况发生,ZooKeeper都会通知所有在/exclusive_lock注册了子节点变更Watcher监听的客户端,这些客户端重复获取锁的过程。

    2. 共享锁(Share Locks, S锁):又称读锁。事务T1对数据对象O1加上了共享锁,那么T1只能对O1进行读取(无更新)操作,其他任何事务也只能对O1加共享锁,直到O1上的所有共享锁都被释放。加上排他锁后,O1只对T1可见,加上共享锁后O1对所有事务可见。



      ZooKeeper实现共享锁:
      定义锁:与排他锁一样,同样是通过ZooKeeper上的数据节点来表示一个锁。
      获取锁:需要获取共享锁时,所有客户端都会到/shared_lock节点下创建临时顺序节点,并在名称里标注R或W来区分读请求和写请求。
      判断读写顺序:首先有个原则:不同事务都可以同时对同一数据对象进行读取操作,但更新操作必须在当前没有任何事务进行读写操作的情况下进行,ZooKeeper节点确定分布式读写顺序,分为4个步骤:
      1)创建完节点后,获取/share_lock节点下所有子节点,并对该节点注册子节点变更的Watcher监听。
      2)确定自己的节点序号在所有子节点中的顺序。
      3)对于读请求:如果没有比自己序号小的子节点,或者所有比自己序号小的子节点都是读请求,表明获取共享锁,执行读操作;如果比自己序号小的子节点中有写请求,那么需要等待。
      对于写请求:如果自己不是序号最小的的子节点,那么等待。
      4)接收到Watcher通知后,重复步骤1。
      释放锁:逻辑与排他锁一样。

    羊群效应


    考虑上图所示共享锁实例。
    1. 192.168.0.1这台机器首先进行读操作,完成后将节点/192.168.0.1-R-0000000001删除。
    2. 余下的4台机器均收到了这个节点被移除的通知,然后重新从/shared_lock节点上获取一份新的子节点列表。
    3. 每个机器判断自己的读写顺序,192.168.0.2这台机器检测到自己已经是序号最小的机器了,于是开始进行写操作,余下的其他机器发现没有轮到自己进行读取或更新操作,继续等待。
    4. 继续……

    从以上过程可以看出,192.168.0.1客户端移除自己的共享锁后,ZooKeeper发送了子节点变更Watcher通知给所有机器,然而这个通知除了给192.168.0.2这台机器产生实际影响外,对于余下的其他所有机器都没有任何作用。

    在整个分布式锁的竞争过程中,Watcher通知和子节点列表获取两个操作重复运行,且大多数运行结果都是判断出自己并非是序号最小的节点,从而继续等待下一次通知。客户端无端的收到过多和自己毫不相关的事件通知。集群规模如果较大,会对ZooKeeper服务器造成性能影响和网络冲击;且如果同一时间内多个节点对应的客户端完成事务或是事务中断引起节点消失,ZooKeeper服务器就会在短时间内向其余客户端发送大量的事件通知——即羊群效应。

    出现羊群效应根源:没有找准客户端真正的关注点。分布式锁竞争过程的核心逻辑在于判断自己是否是所有子节点中序号最小的。所以每个节点对应的客户端只需要关注比自己序号小的那个相关节点的变更情况就可以了,不需要关注全局的子列表变更情况。

    改进后分布式锁实现:
    1)客户端调用create()方法在share_lock节点下创建临时顺序节点
    2)客户端调用getChildren()接口获取所有已经创建的子节点列表,不注册任何Watcher。
    3)如果无法获取共享锁,调用exist()来对比自己小的那个节点注册Watcher。“比自己小的节点”概念在读请求和写请求中不同:
    读请求:向比自己序号小的最后一个写请求节点注册Watcher监听。
    写请求:向比自己序号小的最后一个节点注册Watcher监听。
    4)等待Watcher通知,继续进入步骤2。

    分布式队列

    ZooKeeper实现有两种:FIFO和Barrier模型。
    FIFO:与共享锁实现类似。类似全写的共享锁模型。
    所有客户端都到/queue_fifo节点下创建临时顺序节点。然后4步骤:

    1. 调用getChilden()接口获取/queue_fifo节点下所有子节点,即获取队列中所有元素。
    2. 确定自己的节点序号在所有子节点中的顺序。
    3. 如果自己不是序号最小的子节点,那么进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
    4. 接收到Watcher通知后,重复步骤1。

    相关文章

      网友评论

          本文标题:从PAXOS到ZOOKEEPER分布式一致性原理与实践:zook

          本文链接:https://www.haomeiwen.com/subject/beyvoktx.html