Zookeeper的使用及场景分析

1.Zookeeper简介

1.1分布式系统定义及面临的问题

分布式系统定义
同时跨越多个物理主机,独立运行的多个软件所组成的系统。
优点显而易见:
人多干活快,并且互为备份,一台机器宕机通常不会影响到服务的高可用。
缺点也很明显:
分布式系统中经常会出现各种各样的异常, 如何保证信息的同步是通畅的且没有冲突?如何保证数据的一致性?
解决上述的问题,通常需要类似Zookeeper这样的分布式协调组件,通过服务进程之间的通信,让各个节点的信息能够同步和共享
通信方式通常有以下两种:

  • 通过网络进行信息共享
    这是最容易理解的信息共享方式,类似于现实生活中,开发leader在会议上把任务传达给每个开发人员,当任务发生变化时,leader也会单独告诉某个开发人员或者邮件通知。信息通过人与人之间的沟通完成传递。
    在计算机世界也是类似,机器间通过网络将信息进行共享,发生变化时也第一时间通知到目标对象。
  • 通过共享存储
    类似于开发leader按照约定的时间和路径,将任务分配信息放到了svn上,组员每天去拉取最新的任务分配表,然后干活。其中SVN就是共享存储。更好一点的做法是,svn文件发生更新时就发送一封邮件给开发人员,让开发人员再去拉最新的任务分配。

1.2 zookeeper如何解决分布式系统面临的问题

zk使用的是共享存储这种方式,当然了,分布式应用也需要和存储进行网络通信。
zk实现分布式协同的原理其实也类似上文中SVN同步工作任务的例子。
请添加图片描述

注意:follow节点要想获取zk的更新通知,需要对关心的数据节点进行监听。

2.zookeeper的基本概念

zookeeper是一个开源的分布式协调服务,其设计目标是讲复杂且容易出错的分布式一致性服务封装起来,以简单接口的形式提供给用户使用。zk是一个典型的分布式数据一致性的解决方案,分布式应用程序可以使用它来完成数据订阅/发布、负载均衡、命名服务、集群管理、分布式锁和分布式队列等功能

基本概念
1.集群角色
zk没有沿用传统的master/slave概念,而是引入了leader,follower,observer三种角色。
zk集群通过leader选举来选定一台被称为leader的机器,leader机器服务器为客户端提供读和写服务,除leader外,follower和observer都能提供读服务。follower和observer唯一的区别在于observer不参与leader选举,不参与写操作的过半写成功策略,因此observer可以在不影响写性能的情况下提升zk集群的性能

2.会话(session)
session指客户端会话,一个客户端会话是指客户端和服务端之间的一个长连接。客户端启动的时候,首先会与zk服务端建立一个TCP连接,从第一次建立连接开始,客户端会话的生命周期就开始了,通过这个连接,客户端能通过与服务器进行心跳检测来保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来⾃服务器的Watch事件通知。

3.数据节点
在zk中,节点分为两类,第一类是指构成集群的机器,称之为机器节点。第二类是指zk数据模型中的数据单元,我们称之为数据节点,zk将所有数据存储在内存中,数据模型是一棵树(Znode Tree),类似于文件系统,由斜杠/进行分割的路径,就是一个Znode,例如/app/path1,每个Znode都会保存自己的数据内容和一系列属性信息。

4.版本
对于每个Znode,zk都会维护一个叫做Stat的数据结构,Stat记录了Znode的三个数据版本,分别是version(当前Znode的数据版本),cversion(当前Znode的children节点的版本),aversion(当前Znode的ACL版本)

5.watcher(事件监听器)
zk允许用户在指定节点上注册一些watcher,并且在被指定节点的一些特定事件触发时,zk服务端将该事件通知到客户端,该机制是zk实现分布式协调事务的重要特性。

6.ACL
zk采用ACL(Access Control List)策略来进行权限控制,定义了五种权限:

  • Create:创建子节点的权限
  • Read:获取节点数据和子节点列表的权限
  • WRITE:更新节点数据的权限
  • DELETE:删除子节点的权限
  • ADMIN:设置节点ACL的权限

3.Zookeeper基本使用

3.1系统模型

在zk中,数据被保存在数据节点(Znode)上。Znode是zk中的最小数据单元,在Znode下面又可以再挂Znode,这样一层层下去就形成了一个层次化命名空间Znode树,我们称之为Znode Tree,它采用了类似文件系统的层级树状结构进行管理。见下图事例:
在这里插入图片描述

3.1.1Znode的类型

Znode节点的类型可以分为三大类:
持久性节点(Persisent)
临时性节点(Ephemeral)
顺序性节点(Sequential)
实际开发中通过组合会有四种节点:
持久节点,持久顺序节点,临时节点,临时顺序节点,不同类型的节点会有不同的生命周期。
持久节点:
节点被创建后会一直存在服务器,直到删除操作主动清除。
持久顺序节点:
有顺序的持久节点,顺序特性实质是在创建节点的时候,会在节点后面加上一个数字后缀,来表示其顺序。
临时节点:
会被自动清理的节点,它的生命周期和session(客户端会话)绑定,客户端会话结束时,节点就会被删除掉。与持久性节点不同的是,临时节点无法创建子节点。
临时顺序节点:
有顺序的临时节点,顺序特性实质是在创建节点的时候,会在节点后面加上一个数字后缀,来表示其顺序。

3.1.2事务ID

广义上 事务是指物理和抽象的应用状态的集合。
狭义上通常指的是数据库的事务,包含了一系列对数据库有序的读写操作,这些数据库事务通常具有ACID特性,即原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)

而在zk中,事务是指能够改变zk服务器状态的操作,我们也称之为事务操作或者更新操作。一般包括节点的创建、删除,节点内容更新。对于每一个事务请求,zk都会为其分配一个全局唯一的事务id,用ZXID来表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出zookeeper处理这些更新操作请求的全局顺序。

3.1.3 Znode的状态信息

在这里插入图片描述

整个Znode的节点内容包括两部分:节点数据内容节点状态信息
图中quota是数据内容,其他的属于状态信息。
cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime 就是 Create Time,表示节点创建时间。
mZxid 就是 Modified ZXID,表示节点最后⼀次被修改时的事务ID。
mtime 就是 Modified Time,表示节点最后⼀次被修改的时间。
pZxid 表示该节点的⼦节点列表最后⼀次被修改时的事务 ID。只有⼦节点列表变更才会更新 pZxid,
⼦节点内容变更不会更新。
cversion 表示⼦节点的版本号。
dataVersion 表示内容版本号。
aclVersion 标识acl版本
ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
dataLength 表示数据⻓度。
numChildren 表示直系⼦节点数。

3.1.4Watcher-数据变更通知

一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态发生变化时,会通知到所有的订阅者,使他们能做出对应的处理。

zk使用watcher机制来实现分布式数据的发布/订阅功能。
zk允许客户端向服务端注册一个watcher来监听,当服务端的一些指定事件触发了这个watcher,就会像指定的客户端发送一个事件通知来实现分布式的通知功能。
在这里插入图片描述
zk的watcher机制主要包括客户端线程、客户端watchermanager、zookeeper服务器三部分。
具体的工作流程为:
客户端将watcher对象注册在zk服务器上,同时存储在客户端的watchermanager中,当zk服务器触发了watcher事件后,会像客户端线程发送通知,客户端线程从wathcermanager中取出对应的watcher对象来执行对应的回调逻辑。

3.1.5ACL机制(Access Control List)-保障数据安全

我们可以从三个方面来理解ACL机制:权限模式(Scheme)、授权对象(ID)、权限(Permission)
通常使用"scheme🆔permission"来标识一个有效的ACL信息

权限模式(Scheme):
权限模式用来确定权限校验过程中使用的校验策略,有如下四种:
1.IP
通过IP地址粒度来进行权限控制,支持 192.168.0.1 和192.168.0.1/24和192.168.0.*等多种形式。
2.Digest
最常用的权限控制模式,使用"username:password"形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制。
注意:当使用"username:password"形式配置了权限标识后,zk会先后对其进行SHA-1加密和BASE64编码
3.World
一种最开放的权限模式,数据节点的访问对所有用户开放。可以视作一种特殊的Digest方式,标识为"world:anyone"
4.Super
一种特殊的Digest模式,超级用户可以对zk的数据节点进行任何操作。

授权对象(ID)
授权对象指的是权限赋予的具体用户或指定实体。在不同的权限模式下,授权对象也是不同的。如下图。

权限模式授权对象
IP通常是一个IP地址或地址段
Digest自定义,通常是username:BASE64(SHA-1(username:password))例如:zhangsan:sdfndsllndlksfn7c=
Worldanyone
Super超级用户

权限
权限就是那些通过权限检查后可以被执行的具体操作。zk中对数据的操作权限分为以下五个大类:
CREATE:数据节点的创建权限,允许授权对象在该数据节点下创建子节点。
DELETE:子节点的删除权限,允许授权对象删除该数据节点的子节点。
READ:数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等。
WRITE:数据节点的更新权限,允许授权对象对该数据节点进行更新操作。
ADMIN:数据节点的管理权限,允许授权对象对该数据节点进⾏行ACL 相关的设置操作。

3.2基本使用

zookeeper主要有两个开源客户端
1.ZkClient
2.Curator
具体代码略。

4.zk应用场景分析

4.1数据发布/订阅

数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中心。顾名思义就是发布者将数据发布到zk的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置中心的集中式管理和数据的动态更新。

发布/订阅系统通常有两种设计模式,分别是**推(PUSH)拉(PULL)**模式,在推模式中,服务端主动将数据更新发送给所有的订阅的客户端。在拉模式则是由客户端主动发起请求来拉去最新的数据,通常都采用定时轮询拉取的策略。

zk实现的发布/订阅系统则是采用推拉相结合的方式。客户端向服务端注册自己需要关注的节点,⼀旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。

配置中心场景:

  • 配置存储
    在进行配置管理之前,⾸先我们需要将初始化配置信息存储到Zookeeper上去,⼀般情况下,我们可以在Zookeeper上选取⼀个数据节点⽤于配置信息的存储,例如:/app1/database_config

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

  • 配置变更
    在系统运行过程中,可能会出现需要进行配置信息更新的情况。借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。

4.2命名服务

命名服务(Name Service)是分布式系统最基本的公共服务之一。
在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等。这些我们都可以称之为名字(Name)。
zk提供的命名服务能够帮助应用系统通过一个资源引用的方式来实现对资源的定位与使用。另外,广义上命名服务的资源定位都不是真正意义的实体资源–在分布式环境中,上层应用仅仅需要一个该资源全局唯一的名字,类似于数据库的唯一主键。

我们借助一个分布式任务调度系统来看zk如何实现全局唯一id的生成。
利用zk节点创建的API可以创建一个顺序节点,并且在API中会返回这个节点的完整名称。利用这个特性,我们可以借助zk来生成全局唯一的ID。
在这里插入图片描述
1.所有客户端都会根据自己的任务类型,在指定任务类型下面创建顺序节点,例如创建"job-“节点。
2.节点创建完毕后,会返回节点的节点名,例如"job-00000001”
3.客户端拿到返回值后,拼接上type类型,例如"type2-job-00000001",这样就可以作为一个全局唯一的id了

在zk中,每一个数据节点都能维护顺序子节点的顺序,当客户端对其创建顺序子节点时,zk自动以后缀的形式在子节点上添加一个序号。上面的全局id场景就是利用了这个特性。

4.3集群管理

所谓集群管理,包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后
者则是对集群进行操作与控制。

Zookeeper的两大特性:
  1.客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
  2.对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除
  
zk利用这两大特性来完成集群管理。下面以分布式日志收集系统为例来学习zk如何完成集群管理。

分布式日志收集系统
分布式日志收集系统(以下简称日志系统)的核心工作就是收集分布在不同机器上的系统日志。
一个典型的日志系统的架构设计中,会将需要收集的日志机器(以下称日志源机器)分为多个组别,每个组别对应一台收集器(以下称收集器机器),这个收集器本身其实也是一个后台机器,用于收集日志。
对于大规模的分布式系统日志收集场景,通常需要解决两个问题:

  • 变化的日志源机器
  • 变化的收集器机器

无论是日志源机器还是收集器机器的变更,最终都可以归结为如何快速、合理、动态地为每个收集器分配对应的日志源机器。

zk解决以上问题的方案的步骤如下:

1.注册收集器机器
典型做法是在zk上创建一个节点作为收集器的根节点,例如/logs/collector(后续以"收集器节点"代表该数据节点),每个收集器在启动时,在收集器节点下创建自己的节点,比如/logs/collector/[Hostname]
在这里插入图片描述

2.任务分发
待所有收集器机器都创建好自己对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成若干组,然后将分组后的日志源机器列表写入到收集器创建的子节点(例如/logs/collector/host1)上去。这样一来,每个收集器机器都能从自己对应的节点获取到当前日志源机器列表,从而开始进行日志收集工作。

3.状态汇报
完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能,因此我们在收集器机器节点下再创建一个状态子节点,例如/logs/collector/host1/status,每个收集器机器需要定期向该状态子节点写入自己的状态信息。
我们可以把这种方式看成是一种特殊的心跳检测机制,通常收集器会在这个status节点中写入日志收集进度信息,日志系统根据该状态子节点的最后更新时间来判断对应的收集器机器是否存活。

4.动态分配
如果收集器发生扩缩容,就需要动态地进行收集任务的再分配。在运行过程中,日志系统始终关注着/logs/collector这个节点下所有子节点的变更。一旦发现有收集器停止汇报或者是有新的收集器机器加入,就要重新开始分配任务。无论是哪种情况,日志系统都要将之前分配给该收集器的所有任务进行转移。为了解决这个问题,通常有两种做法:

全局动态分配
简单粗暴,每次扩缩容时都立即对所有日志源机器进行重新分组,分配给收集器机器。

局部动态分配:
顾名思义是在小范围内进行任务的动态分配。在这种策略中,收集器在汇报日志收集状态时,也会把自己的负载汇报上去。注意这里的负载不仅仅是指CPU负载(load),而是一个对当前收集器机器任务执行状态的综合评估,这个评估算法和zk无关,这里不再赘述。
在这种策略下,如果某个收集器机器挂了,就会将这个收集器分配的任务转移到那些负载较低的机器上。
如果有新的机器加入,就会将负载高的机器上的任务转移部分到新的机器上来。

以上说明了整个日志系统的工作模型,注意以下两点:
1.节点类型
在/logs/collector/[Hostname]创建的是持久几点,用/status节点来表征每⼀个收集器机器的状态,这样机器挂了之后数据不会消失,日志系统依然能将任务还原。
2.日志系统节点监听
若采用watcher机制,通知的消息量的网络开销会非常大,通常是日志系统主动轮询收集器节点的策略,节省网络流量,会存在一定延时。

4.4Master选举

Master/Salve机制是分布式系统常见的一种数据模型,在分布式系统中,Master往往用来协调系统其它组件,也往往具有更改分布式系统状态的决定权,比如数据库的读写分离场景中,Master用来写操作,Salve用来读操作。而在另一些场景中,Master往往用来完成一些复杂操作,并在完成后将结果同步给其他Salve节点。
我们就结合“⼀种海量数据处理与共享模型”这个具体例
子来看看 ZooKeeper在集群Master选举中的应用场景。

分布式系统中,经常遇到以下场景:集群中的所有系统单元都需要对前端提供展示数据,比如一个推荐商品ID,又或者一个网站轮播的广告ID,这些商品ID或者广告ID通常是从海量数据中通过各种算法处理得出,考虑到广告推荐的千人千面,这通常是一个非常消耗IO和CPU的操作。如果整个集群都来做这个操作,通常会耗费非常多的资源而且及时性无法得到保障。
一种比较好的方法是让集群中的少部分机器甚至是一台机器执行数据计算,一旦计算出数据结果。可以共享给整个集群的其他节点,大大减少性能消耗和资源浪费。
在这里插入图片描述
将上图拆解为若干步骤:
1.从客户端集群中选举出Master节点
2.Master节点注册到zk上
3.Master节点负责从海量数据处理,并得到一个结果
4.Master节点将计算结果同步到内存/数据库
5.其他客户端使用时直接访问内存/数据库

利用zookeeper的强一致性,能够很好的保证在高并发情况下节点的创建能保证全局唯一性,即zk会保证客户端无法重复创建一个已经存在的客户端。利用这个特性,可以很方便的实现选举。

比如所有客户端都会在/master_election/2020-11-11/下创建binding临时节点,但是只有一个客户端能成功创建,那么这台客户端即为Master节点,其他客户端则注册对该节点的watcher,当Master节点发生故障时,其他客户端监听到Master节点挂掉了,则会重新发生选举。

4.5分布式锁

分布式锁是控制分布式系统同步访问共享资源的一种方式。如果不同的系统或者同一个系统间的不同主机共享了某一种资源,那么在访问这些资源时,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,这时就需要使用分布式锁了。

平时开发时,我们很少在意分布式锁,而是依赖于数据库的排他性来实现不同进程之间的互斥。这是一个不错的分布式锁的实现方式,但有一个不争的事实,目前绝大多数分布式系统的性能瓶颈就在于数据库操作上。

下面我们看看zk如何实现分布式锁,这里主要讲解排他锁共享锁

排他锁:
排他锁(Exclusive Lock,简称X锁),又称为写锁或独占锁,是一种基本的锁类型。如果事务T1对数据对象O1加了X锁,那么整个加锁期间,其他任何事务都不能对O1做任何类型的操作-直到T1释放了X锁。

排他锁的核心就是如何保证当前有且仅有一个事务获得锁,且锁被释放后,所有正在等待获取锁的事务都能被通知到。

zk实现排他锁步骤:
1.定义锁
通过zk上的数据节点来表示一个锁,例如/exclusive_lock/lock临时节点就可以被定义为⼀个锁,如图:
在这里插入图片描述
2.获取锁
在需要获取排他锁时,所有的客户端都会试图通过调用
create()接口,最终只有一个客户端能创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock 注册一个子节点变更的watcher监听。

3.释放锁
当占有锁的客户端发生如下两种情况时都会触发锁释放(节点移除):1.客户端发生故障 2.客户端处理完毕后主动释放
当锁被释放后,所有监听节点的客户端都会受到通知,然后重复步骤2获取锁。

共享锁
共享锁(Shared Lock),又称读锁,同样是一种基本类型的锁。
如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个对象加共享锁-直到该数据上的所有共享锁都被释放。

共享锁和排他锁最大的区别在于,加上排他锁后,数据对象只对一个事务可见,而加上共享锁后,事务对所有事务都可见。

1.定义锁
通过zk的临时顺序节点来表示一个锁,
在这里插入图片描述
2.获取锁
在需要获取锁时,需要创建对应的临时顺序节点,如果是读请求就创建如/shared_lock/host1-R-0000000001节点,如果是写请求就创建如/shared_lock/host2-W-0000000002节点

判断读写顺序:
通过zk来确定分布式读写顺序,大致分为4步。
1.创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点变更进行监听。
2.确定自己的节点顺序在所有子节点中的顺序。
3.对于读请求:若没有比自己序号小的节点或者所有比自己小的节点都是读请求时,表明已经获取到锁,同时开始执行读取逻辑。
对于写请求:若是自己不是序号最小的节点,则需要等待。直到变为最小,开始执行写逻辑。
4.接受到watcher通知后,重复1-3

3.释放锁
其释放锁的流程与独占锁一致。

羊群效应
上述的流程有一个弊端,当集群规模非常大时,节点状态变更会很频繁,大量的watcher通知和子节点列表获取两个操作会对zk集群造成很大的压力。而实际上每个节点能不能获取锁并不需要关注全局的子节点列表变更,只需要关心比自己序号小的那个节点的状态即可。
改进后的步骤:
1.客户端调用create()接口,创建类似/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。
2.客户端调用getChildren()接口获取所有已创建的子节点列表(不注册任何watcher)
3.如果无法获取共享锁,就调用exist接口对比自己小的节点来注册watcher。
对于读请求:对比自己序号小的最后一个写节点注册watcher
对于写请求:对比自己序号小的节点注册watcher
4.等待watcher通知,进入步骤2

4.6 分布式队列

分布式队列主要可以分为两种:
一种是常规的FIFO先入先出队列模型,还有一种是等待队列元素聚集后统一安排处理的Barrier模型

1.FIFO先入先出
FIFO(First Input First Output,先入先出),FIFO队列是一种非常典型且应用广泛的按序执行的队列模型。

使用zk实现FIFO模型,和之前提到的共享锁的实现非常类似。FIFO队列就类似于一个全写的共享锁模型。
大体设计思路如下:所有的客户端都会到/queue_fifo 这个节点下面创建⼀个临时顺序节点,例如/queue_fifo/host1-00000001。在这里插入图片描述
创建完节点后,根据如下4步确定执行顺序:
1.调用getChildren接口获取/queue_fifo下所有子节点
2.确定自己的节点序号在所有子节点中的顺序。
3.如果自己的序号不是最小,则需要等待,同时向比自己序号小的最后一个节点注册watcher监听。
4.接受到watcher通知后,重复步骤1

在这里插入图片描述
2.Barrier模型:分布式屏障
Barrier原意是指障碍物、屏障。在分布式系统中,barrier特指系统之间的一个协调条件,规定了一个队列的元素必须都集聚后才能统一进行安排,否则一直等待。
这往往出现大规模分布式并行计算的应用场景上:
最终的合并计算需要很多并行计算的子结果来进行。

大致的设计思想如:
开始时,/queue_barrier 节点是⼀个已经存在的默认节点,并且
将其节点的数据内容赋值为⼀个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的
⼦节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrie节点下创建⼀个临
时节点,例如/queue_barrier/host1,如图所示。
在这里插入图片描述
创建完节点后,按照如下步骤执⾏。

  1. 通过调用getData接⼝获取/queue_barrier节点的数据内容:10。
  2. 通过调用getChildren接⼝获取/queue_barrier节点下的所有子节点,同时注册对子节点变更的
    Watcher监听。
  3. 统计子节点的个数。
  4. 如果子节点个数还不足10个,那么需要等待。
  5. 接受到Wacher通知后,重复步骤2
    在这里插入图片描述

5.Zookeeper深入进阶

5.1ZAB协议

zookeeper并没有完全采用paxos算法,而是使用了一种称为Zookeeper Atomic Broadcast(ZAB,zookeeper 原子消息广播协议)的协议作为其数据一致性的核心算法。

ZAB协议并不像Paxos算法那样是一种通用的分布式一致性算法,它是一种特别为zookeeper专门设计的支持崩溃恢复的原子广播协议

在zk中,主要就是依赖ZAB协议来实现分布式数据的⼀致性,基于该协议,Zookeeper实现了
⼀种主备模式的系统架构来保持集群中各副本之间的数据的⼀致性,表现形式就是 使用⼀个单⼀的主进
程来接收并处理客户端的所有事务请求,并采⽤ZAB的原子⼴播协议,将服务器数据的状态变更以事务
Proposal的形式⼴播到所有的副本进程中,ZAB协议的主备模型架构保证了同⼀时刻集群中只能够有⼀
个主进程来⼴播服务器的状态变更,因此能够很好地处理客户端⼤量的并发请求。但是,也要考虑到主
进程在任何时候都有可能出现崩溃退出或重启现象,因此,ZAB协议还需要做到当前主进程当出现上述异
常情况的时候,依旧能正常⼯作。

zab核心
zab协议的核心是定义了那些会改变zk服务器数据状态的事务请求的处理方式。

即:所有事务请求必须由⼀个全局唯⼀的服务器来协调处理,这样的服务器被称为Leader服务器,余下的
	服务器则称为Follower服务器,Leader服务器负责将⼀个客户端事务请求转化成⼀个事务Proposal(提
	议),并将该Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有
	Follower服务器的反馈,⼀旦超过半数的Follower服务器进⾏了正确的反馈后,那么Leader就会再次向
	所有的Follower服务器分发Commit消息,要求其将前⼀个Proposal进⾏提交

在这里插入图片描述
ZAB协议介绍
ZAB协议包括两种基本的模式:崩溃恢复消息广播
进入崩溃恢复模式:
当整个服务框架启动过程中,或者是Leader服务器出现网络中断、崩溃退出或重启等异常情况时,ZAB
协议就会进⼊崩溃恢复模式,同时选举产⽣新的Leader服务器。当选举产生了新的Leader服务器,同
时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式,其
中,所谓的状态同步 就是指数据同步,⽤来保证集群中过半的机器能够和Leader服务器的数据状态保
持⼀致

进入消息广播模式:
当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进
消息广播模式,当⼀台同样遵守ZAB协议的服务器启动后加⼊到集群中,如果此时集群中已经存在⼀
个Leader服务器在负责进行消息⼴播,那么加⼊的服务器就会自觉地进⼊数据恢复模式:找到Leader
所在的服务器,并与其进行数据同步,然后⼀起参与到消息⼴播流程中去

Zookeeper只允许唯⼀的⼀个Leader服务器来进行事务请求的处理,Leader服务器在接收到客户端的事务请求后,会生成对应的事务提议并发起⼀轮广播协议,⽽如果集群中的其他机器收到客户端的事务请求后,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

1.消息广播
ZAB协议的消息广播过程使⽤原子广播协议,类似于⼀个⼆阶段提交过程,针对客户端的事务请求,
Leader服务器会为其生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集
各自的选票,最后进行事务提交。
在这里插入图片描述
在ZAB的⼆阶段提交过程中,移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务
Proposal,要么就抛弃Leader服务器,同时,ZAB协议将⼆阶段提交中的中断逻辑移除意味着我们可以
在过半的Follower服务器已经反馈Ack之后就开始提交事务Proposal了,而不需要等待集群中所有的
Follower服务器都反馈响应,但是,在这种简化的⼆阶段提交模型下,⽆法处理因Leader服务器崩溃退
出而带来的数据不⼀致问题,因此ZAB采⽤了崩溃恢复模式来解决此问题,另外,整个消息⼴播协议是
基于具有FIFO特性的TCP协议来进⾏网络通信的,因此能够很容易保证消息⼴播过程中消息接受与发送
的顺序性。

在整个消息广播过程中,Leader服务器会为每个事务请求成对应的Proposal来进⾏⼴播,并且在⼴播
事务Proposal之前,Leader服务器会⾸先为这个事务Proposal分配⼀个全局单调递增的唯⼀ID,称之
为事务ID(ZXID),由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每个事务Proposal按
照其ZXID的先后顺序来进⾏排序和处理。

具体的过程:在消息⼴播过程中,Leader服务器会为每⼀个Follower服务器都各自分配⼀个单独的队
,然后将需要⼴播的事务 Proposal 依次放⼊这些队列中去,并且根据 FIFO策略进⾏消息发送。每⼀
个Follower服务器在接收到这个事务Proposal之后,都会首先将其以事务⽇志的形式写⼊到本地磁盘
去,并且在成功写⼊后反馈给Leader服务器⼀个Ack响应。当Leader服务器接收到超过半数Follower的
Ack响应后,就会⼴播⼀个Commit消息给所有的Follower服务器以通知其进行事务提交,同时Leader
⾃身也会完成对事务的提交,⽽每⼀个Follower服务器在接收到Commit消息后,也会完成对事务的提
交。

2.崩溃恢复
ZAB协议的这个基于原子⼴播协议的消息⼴播过程,在正常情况下运行非常良好,但是⼀旦在Leader服
务器出现崩溃,或者由于⽹络原因导致Leader服务器失去了与过半Follower的联系,那么就会进⼊崩溃
恢复模式。在ZAB协议中,为了保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的Leader
服务器,因此,ZAB协议需要⼀个⾼效且可靠的Leader选举算法,从⽽保证能够快速地选举出新的
Leader,同时,Leader选举算法不仅仅需要让Leader⾃身知道已经被选举为Leader,同时还需要让集
群中的所有其他机器也能够快速地感知到选举产⽣出来的新Leader服务器。

基本特性
根据上面的内容,我们了解到,ZAB协议规定了如果⼀个事务Proposal在⼀台机器上被处理成功,那么
应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。接下来我们看看在崩溃恢复过程中,可能
会出现的两个数据不⼀致性的隐患及针对这些情况ZAB协议所需要保证的特性。

ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交
假设⼀个事务在 Leader 服务器上被提交了,并且已经得到过半 Follower 服务器的Ack反馈,但是在它
将Commit消息发送给所有Follower机器之前,Leader服务器挂了,如图所示
在这里插入图片描述
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务
如果在崩溃恢复过程中出现⼀个需要被丢弃的提案,那么在崩溃恢复结束后需要跳过该事务Proposal,
如图所示。
在这里插入图片描述
在图所示的集群中,假设初始的 Leader 服务器 Server1 在提出了⼀个事务Proposal3 之后就崩溃退出
了,从⽽导致集群中的其他服务器都没有收到这个事务Proposal3。于是,当 Server1 恢复过来再次加
⼊到集群中的时候,ZAB 协议需要确保丢弃Proposal3这个事务。

结合上⾯提到的这两个崩溃恢复过程中需要处理的特殊情况,就决定了 ZAB 协议必须设计这样⼀个
Leader 选举算法:能够确保提交已经被 Leader 提交的事务 Proposal,同时丢弃已经被跳过的事务
Proposal。针对这个要求,如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所
有机器最⾼编号(即ZXID最⼤)的事务Proposal,那么就可以保证这个新选举出来的Leader⼀定具有
所有已经提交的提案。更为重要的是,如果让具有最⾼编号事务Proposal 的机器来成为 Leader,就可
以省去 Leader 服务器检查Proposal的提交和丢弃⼯作的这⼀步操作了。

数据同步
完成Leader选举之后,在正式开始⼯作(即接收客户端的事务请求,然后提出新的提案)之前,
Leader服务器会⾸先确认事务⽇志中的所有Proposal是否都已经被集群中过半的机器提交了,即是否完
成数据同步。下⾯我们就来看看ZAB协议的数据同步过程。
所有正常运⾏的服务器,要么成为 Leader,要么成为 Follower 并和 Leader 保持同步。Leader服务器
需要确保所有的Follower服务器能够接收到每⼀条事务Proposal,并且能够正确地将所有已经提交了的
事务Proposal应⽤到内存数据库中去。具体的,Leader服务器会为每⼀个Follower服务器都准备⼀个队
列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,
并在每⼀个Proposal消息后⾯紧接着再发送⼀个Commit消息,以表示该事务已经被提交。等到
Follower服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功应⽤到本地数
据库中后,Leader服务器就会将该Follower服务器加⼊到真正的可⽤Follower列表中,并开始之后的其
他流程。

运行时状态分析
在ZAB协议的设计中,每个进程都有可能处于如下三种状态之⼀
  · LOOKING:Leader选举阶段。
  · FOLLOWING:Follower服务器和Leader服务器保持同步状态。
  · LEADING:Leader服务器作为主进程领导状态。
  所有进程初始状态都是LOOKING状态,此时不存在Leader,接下来,进程会试图选举出⼀个新的
Leader,之后,如果进程发现已经选举出新的Leader了,那么它就会切换到FOLLOWING状态,并开始
和Leader保持同步,处于FOLLOWING状态的进程称为Follower,LEADING状态的进程称为Leader,当
Leader崩溃或放弃领导地位时,其余的Follower进程就会转换到LOOKING状态开始新⼀轮的Leader选
举。
  ⼀个Follower只能和⼀个Leader保持同步,Leader进程和所有的Follower进程之间都通过⼼跳检测
机制来感知彼此的情况。若Leader能够在超时时间内正常收到⼼跳检测,那么Follower就会⼀直与该
Leader保持连接,⽽如果在指定时间内Leader⽆法从过半的Follower进程那⾥接收到⼼跳检测,或者
TCP连接断开,那么Leader会放弃当前周期的领导,并转换到LOOKING状态,其他的Follower也会选择
放弃这个Leader,同时转换到LOOKING状态,之后会进⾏新⼀轮的Leader选举。

ZAB与Paxos的联系和区别
联系:
  ① 都存在⼀个类似于Leader进程的⻆⾊,由其负责协调多个Follower进程的运⾏。
  ② Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将⼀个提议进⾏提交。
  ③ 在ZAB协议中,每个Proposal中都包含了⼀个epoch值,⽤来代表当前的Leader周期,在Paxos
算法中,同样存在这样的⼀个标识,名字为Ballot。
  区别:
  Paxos算法中,新选举产⽣的主进程会进⾏两个阶段的⼯作,第⼀阶段称为读阶段,新的主进程和
其他进程通信来收集主进程提出的提议,并将它们提交。第⼆阶段称为写阶段,当前主进程开始提出⾃
⼰的提议。
  ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保 存在过半的Follower已经提交
了之前的Leader周期中的所有事务Proposal。这⼀同步阶段的引⼊,能够有效地保证Leader在新的周
期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交。
  总的来说,ZAB协议和Paxos算法的本质区别在于,两者的设计⽬标不太⼀样,ZAB协议主要⽤于
构建⼀个⾼可⽤的分布式数据主备系统,⽽Paxos算法则⽤于构建⼀个分布式的⼀致性状态机系统/

ZAB 协议选举leader算法:
针对每⼀个投票,服务器都需要将别⼈的投票和⾃⼰的投票进⾏PK,PK规则如下
    · 优先检查ZXID。ZXID⽐较⼤的服务器优先作为Leader。
    · 如果ZXID相同,那么就⽐较myid。myid较⼤的服务器作为Leader服务器。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值