ZK
一.基本概念
Zookeeper是⼀个开源的分布式协调服务,其设计⽬标是将那些复杂的且容易出错的分布式⼀致性服务封装起来,构成⼀个⾼效可靠的原语集,并以⼀些简单的接⼝提供给⽤户使⽤。
zookeeper是⼀个典型的分布式数据⼀致性的解决⽅案,分布式应⽤程序可以基于它实现诸如数据订阅/发布、负载均衡、命名服务、集群管理、分布式锁和分布式队列等功能
⻆⾊
通常在分布式系统中,构成⼀个集群的每⼀台机器都有⾃⼰的⻆⾊,最典型的集群就是Master/Slave模式(主备模式),此情况下把所有能够处理写操作的机器称为Master机器,把所有通过异步复制⽅式获取最新数据,并提供读服务的机器为Slave机器。
⽽在Zookeeper中,这些概念被颠覆了。它没有沿⽤传递的Master/Slave概念,⽽是引⼊了Leader、Follower、Observer三种⻆⾊。Zookeeper集群中的所有机器通过Leader选举来选定⼀台被称为Leader的机器,Leader服务器为客户端提供读和写服务.Follower和Observer都能提供读服务,唯⼀的区别在于Observer不参与Leader选举过程,不参与写操作的过半写成功策略,因此Observer可以在不影响写性能的情况下提升集群的性能。
会话(session)
Session指客户端会话,⼀个客户端连接是指客户端和服务端之间的⼀个TCP⻓连接,Zookeeper对外的服务端⼝默认为2181,客户端启动的时候,⾸先会与服务器建⽴⼀个TCP连接,从第⼀次连接建⽴开始,客户端会话的⽣命周期也开始了,通过这个连接,客户端能够⼼跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来⾃服务器的Watch事件通知。
事务ID
事务是对物理和抽象的应⽤状态上的操作集合。往往在现在的概念中,狭义上的事务通常指的是数据库事务,⼀般包含了⼀系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原⼦性(Atomic)、⼀致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
⽽在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,⼀般包括数据节点创建与删除、数据节点内容更新等操作。对于每⼀个事务请求,ZooKeeper都会为其分配⼀个全局唯⼀的事务ID,⽤ ZXID 来表示,通常是⼀个 64 位的数字。每⼀个 ZXID 对应⼀次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序
数据节点(Znode)
是指组成集群的每⼀台机器。在ZooKeeper中,"节点"分为两类,第⼀类同样是指构成集群的机器,我们称之为机器节点;第⼆类则是指数据模型中的数据单元,我们称之为数据节点—ZNode。
在ZooKeeper中,数据信息被保存在⼀个个数据节点(ZNode)上。ZNode 是Zookeeper 中最⼩数据单位,在 ZNode 下⾯⼜可以再挂 ZNode,这样⼀层层下去就形成了⼀个层次化命名空间 ZNode 树,我们称为 ZNode Tree,它采⽤了类似⽂件系统的层级树状结构进⾏管理
ZNode类型
ZNode的类型可以分为三⼤类:
- 持久性节点(Persistent)
- 临时性节点(Ephemeral)
- 顺序性节点(Sequential)
通过组合可以⽣成以下四种节点类型:
- 持久节点:是Zookeeper中最常⻅的⼀种节点类型,所谓持久节点,就是指节点被创建后会⼀直存在服务器,直到删除操作主动清除
- 持久顺序节点:就是有顺序的持久节点,节点特性和持久节点是⼀样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后⾯加上⼀个数字后缀,来表示其顺序。
- 临时节点:就是会被⾃动清理掉的节点,它的⽣命周期和客户端会话绑在⼀起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建⼦节点。
- 临时顺序节点:就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后⾯加上数字后缀。
ZNode状态信息
ZNode 节点内容包括两部分:节点数据内容和节点状态信息。节点数据就是该节点存储的数据
节点状态的含义如下:
- 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: 直系⼦节点数
Watcher(事件监听器)
⼀个典型的发布/订阅模型系统定义了⼀种⼀对多的订阅关系,能够让多个订阅者同时监听某⼀个主题对象,当这个主题对象⾃身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。
在 ZooKeeper 中,引⼊了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册⼀个 Watcher 监听,当服务端的⼀些指定事件触发了这个 Watcher,那么就会向指定客户端发送⼀个事件通知来实现分布式的通知功能。该机制是Zookeeper实现分布式协调服务的重要特性
Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。
具体⼯作流程为:客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端WatcherManager当中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执⾏回调逻辑。
ACL
Zookeeper作为⼀个分布式协调框架,其内部存储了分布式系统运⾏时状态的元数据,这些元数据会直接影响基于Zookeeper进⾏构造的分布式系统的运⾏状态,因此,如何保障系统中数据的安全,从⽽避免因误操作所带来的数据随意变更⽽导致的数据库异常⼗分重要,在Zookeeper中,提供了⼀套完善的ACL(Access Control List)权限控制机制来保障数据的安全。
我们可以从三个⽅⾯来理解ACL机制:权限模式(Scheme)、授权对象(ID)、权限(Permission),通常使⽤"scheme: id : permission"来标识⼀个有效的ACL信息。
权限模式(Scheme):⽤来确定权限验证过程中使⽤的检验策略,有如下四种模式:
-
IP: 通过IP地址粒度来进⾏权限控制,如"ip:192.168.0.110"表示权限控制针对该IP地址,同时IP模式可以⽀持按照⽹段⽅式进⾏配置,如"ip:192.168.0.1/24"表示针对192.168.0.*这个⽹段进⾏权限控制。
-
Digest:是最常⽤的权限控制模式,更符合我们对权限控制的认识,其⽤"username:password"形式的权限标识来进⾏权限配置,便于区分不同应⽤来进⾏权限控制。当我们通过“username:password”形式配置了权限标识后,Zookeeper会先后对其进⾏SHA-1加密和BASE64编码。
-
World:是⼀种最开放的权限控制模式,这种权限控制⽅式⼏乎没有任何作⽤,数据节点的访问权限对所有⽤户开放,即所有⽤户都可以在不进⾏任何权限校验的情况下操作ZooKeeper上的数据。另外,World模式也可以看作是⼀种特殊的Digest模式,它只有⼀个权限标识,即“world:anyone”。
-
Super:顾名思义就是超级⽤户的意思,也是⼀种特殊的Digest模式。在Super模式下,超级⽤户可以对任意ZooKeeper上的数据节点进⾏任何操作。
授权对象(ID):授权对象指的是权限赋予的⽤户或⼀个指定实体,例如 IP 地址或是机器等。在不同的权限模式下,授权对象是不同的,表中列出了各个权限模式和授权对象之间的对应关系。
权限模式 | 授权对象 |
---|---|
IP | 通常是⼀个IP地址或IP段:例如:192.168.10.110 或192.168.10.1/2 |
Digest | ⾃定义,通常是username:BASE64(SHA-1(username:password))例如:zm:sdfndsllndlksfn7c= |
World | 只有⼀个ID :anyone |
Super | 超级⽤户 |
权限:权限就是指那些通过权限检查后可以被允许执⾏的操作。在ZooKeeper中,所有对数据的操作权限分为以下五⼤类:
- CREATE(C):数据节点的创建权限,允许授权对象在该数据节点下创建⼦节点。
- DELETE(D):⼦节点的删除权限,允许授权对象删除该数据节点的⼦节点。
- READ(R):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或⼦节点列表等。
- WRITE(W):数据节点的更新权限,允许授权对象对该数据节点进⾏更新操作。
- ADMIN(A):数据节点的管理权限,允许授权对象对该数据节点进⾏ ACL 相关的设置操作。
其中需要注意的是,CREATE和DELETE这两种权限都是针对⼦节点的权限控制
二.Zookeeper 安装(Mac)、配置、常用命令及简单的java实例
三.应⽤场景
ZooKeeper是⼀个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使⽤它来进⾏分布式
数据的发布与订阅。另⼀⽅⾯,通过对ZooKeeper中丰富的数据节点类型进⾏交叉使⽤,配合Watcher
事件通知机制,可以⾮常⽅便地构建⼀系列分布式应⽤中都会涉及的核⼼功能,如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应⽤场景来做下介绍
数据发布/订阅
数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中⼼,顾名思义就是发布者将数据发布到ZooKeeper的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的⽬的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统⼀般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。
在推模式中,服务端主动将数据更新发送给所有订阅的客户端
⽽拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采⽤定时进⾏轮询拉取的⽅式。
ZooKeeper 采⽤的是推拉相结合的⽅式:客户端向服务端注册⾃⼰需要关注的节点,⼀旦该节点的数据发⽣变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到ZooKeeper上进⾏集中管理,那么通常情况下,应⽤在启动的时候都会主动到ZooKeeper服务端上进⾏⼀次配置信息的获取,同时,在指定节点上注册⼀个Watcher监听,这样⼀来,但凡配置信息发⽣变更,服务端都会实时通知到所有订阅的客户端,从⽽达到实时获取最新配置信息的⽬的。
在平常的应⽤系统开发中,经常会碰到这样的需求:系统中需要使⽤⼀些通⽤的配置信息,例如机器列表信息、运⾏时的开关配置、数据库配置信息等。这些全局配置信息通常具备以下3个特性。
- 数据量通常⽐较⼩。
- 数据内容在运⾏时会发⽣动态变化。
- 集群中各机器共享,配置⼀致。
步骤如下:
- 初始化:选取一个节点比如/config节点,将需要管理的配置信息写⼊到该数据节点中去
- 配置获取:集群中每台机器在启动初始化阶段,⾸先会从上⾯提到的ZooKeeper配置节点上读取信息,同时,客户端还需要在该配置节点上注册⼀个数据变更的 Watcher监听,⼀旦发⽣节点数据变更,所有订阅的客户端都能够获取到数据变更通知。
- 配置变更:在系统运⾏过程中,可能会出现需要进⾏配置更改的情况。借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进⾏更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进⾏最新数据的获取
命名服务
命名服务(Name Service)也是分布式系统中⽐较常⻅的⼀类场景,是分布式系统最基本的公共服务之⼀。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等,其中较为常⻅的就是⼀些分布式服务框架(如RPC、RMI)中的服务地址列表,通过使⽤命名服务,客户端应⽤能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。
在关系型数据库中,各个表都需要⼀个主键来唯⼀标识每条数据库记录,这个主键就是这样的唯⼀ID。在过去的单库单表型系统中,通常可以使⽤数据库字段⾃带的auto_increment属性来⾃动为每条数据库记录⽣成⼀个唯⼀的ID,数据库会保证⽣成的这个ID在全局唯⼀。但是随着数据库数据规模的不断增⼤,分库分表随之出现,⽽auto_increment属性仅能针对单⼀表中的记录⾃动⽣成ID,因此在这种情况下,就⽆法再依靠数据库的auto_increment属性来唯⼀标识⼀条记录了。于是,我们必须寻求⼀种能够在分布式环境下⽣成全局唯⼀ID的⽅法。
UUID是⼀个⾮常不错的全局唯⼀ID⽣成⽅式,能够⾮常简便地保证分布式环境中的唯⼀性。⼀个标准的UUID 是⼀个包含 32 位字符和 4 个短线的字符串,例如“e70f1357-f260-46ffff-a32d-53a086c57ade”。但是UUID的长度过长会浪费更多的空间而且含义不明
在ZooKeeper中,每⼀个数据节点都能够维护⼀份⼦节点的顺序顺列,当客户端对其创建⼀个顺序⼦节点的时候 ZooKeeper 会⾃动以后缀的形式在其⼦节点上添加⼀个序号.使用ZK的顺序节点可以实现这类全局唯⼀ID的⽣成。
- 选取一个节点比如/id节点
- 客户端在/id下创建一个顺序节点
- 获取返回的节点名,这个节点name后会有一个编号,这个编号是唯一的
集群管理
所谓集群管理,包括集群监控与集群控制两⼤块,前者侧重对集群运⾏时状态的收集,后者则是对集群进⾏操作与控制。
利⽤ZK的Watcher监听和会话失效后临时节点自动删除的特性,可以实现集群机器存活监控系统
-
监控系统在/monitor节点上注册⼀个Watcher监听,
-
服务器上线时在/monitor节点下创建⼀个临时节点:/monitor/[Hostname]这样,监控系统就能够实时监测机器的变动情况。
Master选举
Master选举是⼀个在分布式系统中⾮常常⻅的应⽤场景,对于分布式系统往往需要在分布在不同机器上的独⽴系统单元中选出⼀个所谓的“⽼⼤”,在计算机中,我们称之为Master。
利用ZK不能重复创建同一个节点特性以及Watcher监听机制可以实现master选举
- 选取某个节点比如/master_election/2022-07-28
- 所有的客户端去节点下创建一个一样的节点,比如/master_election/2022-07-28/master
- 创建成功的机器成为Master,然后所有的客户端在/master_election/2022-07-28注册⼦节点变更的 Watcher,⽤于监控当前的 Master 机器是否存活
- 如果监听到当前的 Master 挂了,那么其余的客户端重新进⾏Master选举。
分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的⼀种⽅式。如果不同的系统或是同⼀个系统的不同主机之间共享了⼀个或⼀组资源,那么访问这些资源的时候,往往需要通过⼀些互斥⼿段来防⽌彼此之间的⼲扰,以保证⼀致性,在这种情况下,就需要使⽤分布式锁了。
排他锁
排他锁(Exclusive Locks,简称 X 锁),⼜称为写锁或独占锁,是⼀种基本的锁类型。
如果事务 T1对数据对象 O1加上了排他锁,那么在整个加锁期间,只允许事务 T1对 O1进⾏读取和更新操作,其他任何事务都不能再对这个数据对象进⾏任何类型的操作——直到T1释放了排他锁
排他锁的核⼼是如何保证当前有且仅有⼀个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到。
-
定义锁:在ZooKeeper中,通过数据节点来表示⼀个锁,例如/exclusive_lock/lock节点就可以被定义为⼀个锁
-
获取锁:所有的客户端都在/exclusive_lock节点下创建临时⼦节点/exclusive_lock/lock。创建成功的获取锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock 节点上注册⼀个⼦节点变更的Watcher监听,以便实时监听到lock节点的变更情况
-
释放锁:因为/exclusive_lock/lock 是⼀个临时节点,因此在以下两种情况下,都有可能释放锁。
- 当前获取锁的客户端机器发⽣宕机,那么ZooKeeper上的这个临时节点就会被移除。
- 正常执⾏完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。
⽆论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了⼦节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。
共享锁
共享锁(Shared Locks,简称S锁),⼜称为读锁,同样是⼀种基本的锁类型。
如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进⾏读取操作,其他事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对⼀个事务可⻅,⽽加上共享锁后,数据对所有事务都可⻅。
-
定义锁:和独占锁一样,通过数据节点来表示⼀个锁,是一个类似“/shared_lock/[Hostname]-请求类型-序号”的临时顺序节点,例如/shared_lock/host1-R-0000000001,那么,这个节点就代表了⼀个共享锁
-
获取锁:所有客户端都会到/shared_lock 这个节点下⾯创建⼀个临时顺序节点,如果当前是读请求,那么就创建例如/shared_lock/host1-R-0000000001的节点;如果是写请求,那么就创建例如/shared_lock/host2-W-0000000002的节点。
-
创建完节点后,获取/shared_lock节点下所有⼦节点
-
确定⾃⼰的节点序号在所有⼦节点中的顺序。
-
如果是读请求且没有⽐⾃⼰序号⼩的⼦节点或所有⽐⾃⼰序号⼩的⼦节点都是读请求,那么表明⾃⼰已经成功获取到共享锁,同时开始执⾏读取逻辑,否则向⽐⾃⼰序号⼩的最后⼀个写请求节点注册Watcher监听,进行等待
如果是写请求且是序号最⼩的⼦节点,那么就获取到共享锁,开始进行写逻辑,否则向⽐⾃⼰序号⼩的最后⼀个节点注册Watcher监听,进行等待
-
-
释放锁:因为/shared_lock/[Hostname]-请求类型-序号也是⼀个临时节点,因此在以下两种情况下,都有可能释放锁。
- 当前获取锁的客户端机器发⽣宕机,那么ZooKeeper上的这个临时节点就会被移除。
- 正常执⾏完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。
移除了对应的节点后ZooKeeper会通知所有在该节点上注册了Watcher监听的客户端。这些客户端在接收到通知后重新校验一下自己的顺序从而判断是否可以获取锁
分布式队列
分布式队列可以简单分为两⼤类:⼀种是常规的FIFO先⼊先出队列模型,还有⼀种是等待队列元素聚集后统⼀安排处理执⾏的Barrier模型。
FIFO
FIFO(First Input First Output,先⼊先出), FIFO 队列是⼀种⾮常典型且应⽤⼴泛的按序执⾏的队列
模型:先进⼊队列的请求操作先完成后,才会开始处理后⾯的请求。
使⽤ZooKeeper实现FIFO队列,和共享锁的实现⾮常类似。FIFO队列就类似于⼀个全写的共享锁模型
- 选取一个节点,比如/queue_fifo ,所有客户端都到/queue_fifo 这个节点下⾯创建⼀个临时顺序点,例如如/queue_fifo/host1-00000001
- 创建完节点后,获取/queue_fifo节点下所有⼦节点
- 确定⾃⼰的节点序号在所有⼦节点中的顺序
- 如果⾃⼰的序号不是最⼩,那么需要等待,同时向⽐⾃⼰序号⼩的最后⼀个节点注册Watcher监听
- 接收到Watcher通知后,重复步骤2及以后步骤
Barrier
Barrier原意是指障碍物、屏障,⽽在分布式系统中,特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进⾏安排,否则⼀直等待。这往往出现在那些⼤规模分布式并⾏计算的应⽤
- 选取一个节点,比如/queue_barrier,并且将其节点的数据内容赋值为⼀个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的⼦节点个数达到10后,才会打开Barrier
- 所有的客户端都到/queue_barrie节点下创建⼀个临时节点,例如/queue_barrier/host1
- 创建完节点后,获取/queue_barrie节点的数据内容n并获取子节点的的数量,同时注册对⼦节点变更的Watcher监听。
- 如果子节点的个数小于n,那么需要等待
- 接受到Wacher通知后,重复步骤3及后续步骤
四.ZK深入
ZAB协议
概念
zookeeper并没有完全采⽤paxos算法,⽽是使⽤了⼀种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原⼦消息⼴播协议)的协议作为其数据⼀致性的核⼼算法。
ZAB协议并不像Paxos算法那样是⼀种通⽤的分布式⼀致性算法,它是⼀种特别为zookeeper专⻔设计的⼀种⽀持崩溃恢复的原⼦⼴播协议,在zookeeper中,主要就是依赖ZAB协议来实现分布式数据的⼀致性,基于该协议,Zookeeper实现了⼀种主备模式的系统架构来保持集群中各副本之间的数据的⼀致性,表现形式就是使⽤⼀个单⼀的主进程来接收并处理客户端的所有事务请求,并采⽤ZAB的原⼦⼴播协议,将服务器数据的状态变更以事务Proposal的形式⼴播到所有的副本进程中,ZAB协议的主备模型架构保证了同⼀时刻集群中只能够有⼀个主进程来⼴播服务器的状态变更,因此能够很好地处理客户端⼤量的并发请求。但是,也要考虑到主进程在任何时候都有可能出现崩溃退出或重启现象,因此,ZAB协议还需要做到当前主进程当出现上述异常情况的时候,依旧能正常⼯作。
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协议包括两种基本的模式:崩溃恢复和消息⼴播
崩溃恢复:
ZAB协议的这个基于原⼦⼴播协议的消息⼴播过程,在正常情况下运⾏⾮常良好,但是⼀旦在Leader服务器出现崩溃,或者由于⽹络原因导致Leader服务器失去了与过半Follower的联系,那么就会进⼊崩溃恢复模式。在ZAB协议中,为了保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的Leader服务器,因此,ZAB协议需要⼀个⾼效且可靠的Leader选举算法,从⽽保证能够快速地选举出新的Leader,同时,Leader选举算法不仅仅需要让Leader⾃身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产⽣出来的新Leader服务器。
当选举产⽣了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式,其中,所谓的状态同步就是指数据同步,⽤来保证集群中过半的机器能够和Leader服务器的数据状态保持⼀致
完成Leader选举之后,在正式开始⼯作(即接收客户端的事务请求,然后提出新的提案)之前,Leader服务器会⾸先确认事务⽇志中的所有Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步。
所有正常运⾏的服务器,要么成为 Leader,要么成为 Follower 并和 Leader 保持同步。Leader服务器需要确保所有的Follower服务器能够接收到每⼀条事务Proposal,并且能够正确地将所有已经提交了的事务Proposal应⽤到内存数据库中去。
具体的,Leader服务器会为每⼀个Follower服务器都准备⼀个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每⼀个Proposal消息后⾯紧接着再发送⼀个Commit消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务 Proposal 都从 Leader服务器上同步过来并成功应⽤到本地数据库中后,Leader服务器就会将该Follower服务器加⼊到真正的可⽤Follower列表中,并开始之后的其他流程
消息⼴播:
当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进⼊消息⼴播模式,当⼀台同样遵守ZAB协议的服务器启动后加⼊到集群中,如果此时集群中已经存在⼀个Leader服务器在负责进⾏消息⼴播,那么加⼊的服务器就会⾃觉地进⼊数据恢复模式:找到Leader所在的服务器,并与其进⾏数据同步,然后⼀起参与到消息⼴播流程中去。
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消息后,也会完成对事务的提交
运⾏时状态分析
在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协议的核⼼是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理⽅式,即:所有事务请求必须由⼀个全局唯⼀的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器,Leader服务器负责将⼀个客户端事务请求转化成⼀个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,⼀旦超过半数的Follower服务器进⾏了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前⼀个Proposal进⾏提交
基本特性
ZAB协议规定了如果⼀个事务Proposal在⼀台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。在崩溃恢复过程中,可能会出现的两个数据不⼀致性的隐患及针对这些情况ZAB协议所需要保证的特性。
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交
假设⼀个事务在 Leader 服务器上被提交了,并且已经得到过半 Folower 服务器的Ack反馈,但是在它将Commit消息发送给所有Follower机器之前,Leader服务器挂了
图中的消息C2就是⼀个典型的例⼦:在集群正常运⾏过程中的某⼀个时刻,Server1 是 Leader 服务器,其先后⼴播了消息 P1、P2、C1、P3 和 C2,其中,当Leader服务器将消息C2(C2是Commit Of Proposal2的缩写,即提交事务Proposal2)发出后就⽴即崩溃退出了。针对这种情况,ZAB协议就要确保事务Proposal2最终能够在所有的服务器上都被提交成功,否则将出现不⼀致。
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服务器是Zookeeper集群⼯作的核⼼,其主要⼯作有以下两个:
- 事务请求的唯⼀调度和处理者,保证集群事务处理的顺序性。
- 集群内部各服务器的调度者。
对于客户端的请求,Leader是使用责任链来处理的
从prepRequestProcessor到FinalRequestProcessor前后⼀共7个请求处理器组成了leader服务器的请求处理链
- PrepRequestProcessor。请求预处理器,也是leader服务器中的第⼀个请求处理器。在Zookeeper中,那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等),PrepRequestProcessor能够识别出当前客户端请求是否是事务请求。对于事务请求,PrepRequestProcessor处理器会对其进⾏⼀系列预处理,如创建请求事务头、事务体、会话检查、ACL检查和版本检查等。
- ProposalRequestProcessor。事务投票处理器。也是Leader服务器事务处理流程的发起者,对于⾮事务性请求,ProposalRequestProcessor会直接将请求转发到CommitProcessor处理器,不再做任何处理,⽽对于事务性请求,除了将请求转发到CommitProcessor外,还会根据请求类型创建对应的Proposal提议,并发送给所有的Follower服务器来发起⼀次集群内的事务投票。同时,ProposalRequestProcessor还会将事务请求交付给SyncRequestProcessor进⾏事务⽇志的记录。
- SyncRequestProcessor。事务⽇志记录处理器。⽤来将事务请求记录到事务⽇志⽂件中,同时会触发Zookeeper进⾏数据快照。
- AckRequestProcessor。负责在SyncRequestProcessor完成事务⽇志记录后,向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务⽇志记录。
- CommitProcessor。事务提交处理器。对于⾮事务请求,该处理器会直接将其交付给下⼀级处理器处理;对于事务请求,其会等待集群内针对Proposal的投票直到该Proposal可被提交,利⽤CommitProcessor,每个服务器都可以很好地控制对事务请求的顺序处理
- ToBeCommitProcessor。该处理器有⼀个toBeApplied队列,⽤来存储那些已经被CommitProcessor处理过的可被提交的Proposal。其会将这些请求交付给FinalRequestProcessor处理器处理,待其处理完后,再将其从toBeApplied队列中移除。
- FinalRequestProcessor。⽤来进⾏客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理器还会负责将事务应⽤到内存数据库中。
Follower
Follower服务器是Zookeeper集群状态中的跟随者,其主要⼯作有以下三个:
- 处理客户端⾮事务性请求(读取数据),转发事务请求给Leader服务器。
- 参与事务请求Proposal的投票。
- 参与Leader选举投票。
和leader⼀样,Follower也采⽤了责任链模式组装的请求处理链来处理每⼀个客户端请求,由于不需要对事务请求的投票处理,因此Follower的请求处理链会相对简单
和 Leader 服务器的请求处理链最⼤的不同点在于,Follower 服务器的第⼀个处理器换成了FollowerRequestProcessor处理器,同时由于不需要处理事务请求的投票,因此也没有了ProposalRequestProcessor处理器。
- FollowerRequestProcessor:其⽤作识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,Leader服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进⾏处理。
- SendAckRequestProcessor:其承担了事务⽇志记录反馈的⻆⾊,在完成事务⽇志记录后,会向Leader服务器发送ACK消息以表明⾃身完成了事务⽇志的记录⼯作
Observer
Observer是ZooKeeper⾃3.3.0版本开始引⼊的⼀个全新的服务器⻆⾊。从字⾯意思看,该服务器充当了⼀个观察者的⻆⾊——其观察ZooKeeper集群的最新状态变化并将这些状态变更同步过来。
Observer服务器在⼯作原理上和Follower基本是⼀致的,对于⾮事务请求,都可以进⾏独⽴的处理,⽽对于事务请求,则会转发给Leader服务器进⾏处理。和Follower唯⼀的区别在于,Observer不参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票。简单地讲,Observer服务器只提供⾮事务服务,通常⽤于在不影响集群事务处理能⼒的前提下提升集群的⾮事务处理能⼒。
Observer 服务器在初始化阶段会将SyncRequestProcessor处理器也组装上去,但是在实际运⾏过程中,Leader服务器不会将事务请求的投票发送给Observer服务器。
服务器启动
服务端整体架构图
ZK启动的步骤如下:
- 配置⽂件解析
- 初始化数据管理器
- 初始化⽹络I/O管理器
- 数据恢复
- 对外服务
单机启动
预启动流程:
- 统⼀由QuorumPeerMain作为启动类。⽆论单机或集群,在zkServer.cmd和zkServer.sh中都配置了QuorumPeerMain作为启动⼊⼝类。
- 解析配置⽂件zoo.cfg中运⾏时的基本参数,如tickTime、dataDir、clientPort等参数。
- 创建并启动历史⽂件清理器DatadirCleanupManager。对事务⽇志和快照数据⽂件进⾏定时清理。
- 判断当前是集群模式还是单机模式启动。若是单机模式,则委托给ZooKeeperServerMain进⾏启动。
- 再次进⾏配置⽂件zoo.cfg的解析。
- 创建服务器实例ZooKeeperServer。Zookeeper服务器⾸先会进⾏服务器实例的创建,然后对该服务器实例进⾏初始化,包括连接器、内存数据库、请求处理器等组件的初始化。
初始化流程:
- 创建服务器统计器ServerStats。ServerStats是Zookeeper服务器运⾏时的统计器。
- 创建Zookeeper数据管理器FileTxnSnapLog。FileTxnSnapLog是Zookeeper上层服务器和底层数据存储之间的对接层,提供了⼀系列操作数据⽂件的接⼝,如事务⽇志⽂件和快照数据⽂件。Zookeeper根据zoo.cfg⽂件中解析出的快照数据⽬录dataDir和事务⽇志⽬录dataLogDir来创建FileTxnSnapLog。
- 设置服务器tickTime和会话超时时间限制。
- 创建ServerCnxnFactory。通过配置系统属性zookeper.serverCnxnFactory来指定使⽤Zookeeper⾃⼰实现的NIO还是使⽤Netty框架作为Zookeeper服务端⽹络连接⼯⼚。
- 初始化ServerCnxnFactory。Zookeeper会初始化Thread作为ServerCnxnFactory的主线程,然后再初始化NIO服务器。
- 启动ServerCnxnFactory主线程。进⼊Thread的run⽅法,此时服务端还不能处理客户端请求。
- 恢复本地数据。启动时,需要从本地快照数据⽂件和事务⽇志⽂件进⾏数据恢复。
- 创建并启动会话管理器。Zookeeper会创建会话管理器SessionTracker进⾏会话管理。
- 初始化Zookeeper的请求处理链。Zookeeper请求处理⽅式为责任链模式的实现。会有多个请求处理器依次处理⼀个客户端请求,在服务器启动时,会将这些请求处理器串联成⼀个请求处理链。
- 注册JMX服务。Zookeeper会将服务器运⾏时的⼀些信息以JMX的⽅式暴露给外部。
- 注册Zookeeper服务器实例。将Zookeeper服务器实例注册给ServerCnxnFactory,之后Zookeeper就可以对外提供服务
集群启动
上图的过程可以分为预启动、初始化、Leader选举、Leader与Follower启动期交互、Leader与Follower启动等过程
预启动
- 统⼀由QuorumPeerMain作为启动类。
- 解析配置⽂件zoo.cfg。
- 创建并启动历史⽂件清理器DatadirCleanupFactory。
- 判断当前是集群模式还是单机模式的启动。在集群模式中,在zoo.cfg⽂件中配置了多个服务器地址,可以选择集群启动。
初始化
- 创建ServerCnxnFactory。
- 初始化ServerCnxnFactory。
- 创建Zookeeper数据管理器FileTxnSnapLog。
- 创建QuorumPeer实例。Quorum是集群模式下特有的对象,是Zookeeper服务器实例(ZooKeeperServer)的托管者,QuorumPeer代表了集群中的⼀台机器,在运⾏期间,QuorumPeer会不断检测当前服务器实例的运⾏状态,同时根据情况发起Leader选举。
- 创建内存数据库ZKDatabase。ZKDatabase负责管理ZooKeeper的所有会话记录以及DataTree和事务⽇志的存储。
- 初始化QuorumPeer。将核⼼组件如FileTxnSnapLog、ServerCnxnFactory、ZKDatabase注册到QuorumPeer中,同时配置QuorumPeer的参数,如服务器列表地址、Leader选举算法和会话超时时间限制等。
- 恢复本地数据。
- 启动ServerCnxnFactory主线程
Leader选举
- 初始化Leader选举。集群模式特有,Zookeeper⾸先会根据⾃身的服务器ID(SID)、最新的ZXID(lastLoggedZxid)和当前的服务器epoch(currentEpoch)来⽣成⼀个初始化投票,在初始化过程中,每个服务器都会给⾃⼰投票。然后,根据zoo.cfg的配置,创建相应Leader选举算法实现,Zookeeper提供了三种默认算法(LeaderElection、AuthFastLeaderElection、FastLeaderElection),可通过zoo.cfg中的electionAlg属性来指定,但现只⽀持FastLeaderElection选举算法。在初始化阶段,Zookeeper会创建Leader选举所需的⽹络I/O层QuorumCnxManager,同时启动对Leader选举端⼝的监听,等待集群中其他服务器创建连接。
- 注册JMX服务。
- 检测当前服务器状态, 运⾏期间,QuorumPeer会不断检测当前服务器状态。在正常情况下,Zookeeper服务器的状态在LOOKING、LEADING、FOLLOWING/OBSERVING之间进⾏切换。在启动阶段,QuorumPeer的初始状态是LOOKING,因此开始进⾏Leader选举。
- Leader选举,ZooKeeper的Leader选举过程,简单地讲,就是⼀个集群中所有的机器相互之间进⾏⼀系列投票,选举产⽣最合适的机器成为Leader,同时其余机器成为Follower或是Observer的集群机器⻆⾊初始化过程。关于Leader选举算法,简⽽⾔之,就是集群中哪个机器处理的数据越新(通常我们根据每个服务器处理过的最⼤ZXID来⽐较确定其数据是否更新),其越有可能成为Leader。当然,如果集群中的所有机器处理的ZXID⼀致的话,那么SID最⼤的服务器成为Leader,其余机器称为Follower和Observer
Leader与Follower启动期交互
到该步,ZooKeeper已经完成了Leader选举,并且集群中每个服务器都已经确定了⾃⼰的⻆⾊——
通常情况下就分为 Leader 和 Follower 两种⻆⾊。Leader和Follower在启动期间其⼤致交互流程如图所示
- 创建Leader服务器和Follower服务器。完成Leader选举后,每个服务器会根据⾃⼰服务器的⻆⾊创建相应的服务器实例,并进⼊各⾃⻆⾊的主流程。
- Leader服务器启动Follower接收器LearnerCnxAcceptor。运⾏期间,Leader服务器需要和所有其余的服务器(统称为Learner)保持连接以确集群的机器存活情况,LearnerCnxAcceptor负责接收所有⾮Leader服务器的连接请求。
- Follower服务器开始和Leader建⽴连接。所有Follower会找到Leader服务器,并与其建⽴连接。
- Leader服务器创建LearnerHandler。Leader接收到来⾃其他机器连接创建请求后,会创建⼀个LearnerHandler实例,每个LearnerHandler实例都对应⼀个Leader与Follower服务器之间的连接,其负责Leader和Follower服务器之间⼏乎所有的消息通信和数据同步。
- 向Leader注册。Follower完成和Leader的连接后,会向Leader进⾏注册,即将Follower服务器的基本信息(LearnerInfo),包括SID和ZXID,发送给Leader服务器。
- Leader解析Follower发来的信息,计算新的epoch。Leader接收到Follower服务器基本信息后,会解析出该Follower的SID和ZXID,然后根据ZXID解析出对应的epoch_of_learner,并和当前Leader服务器的epoch_of_leader进⾏⽐较,如果该Follower的epoch_of_learner更⼤,则更新Leader的epoch_of_leader = epoch_of_learner + 1。然后LearnHandler进⾏等待,直到过半Learner已经向Leader进⾏了注册,同时更新了epoch_of_leader后,Leader就可以确定当前集群的epoch了。
- 发送Leader状态。计算出新的epoch后,Leader会将该信息以⼀个LEADERINFO消息的形式发送给Learner,并等待Learner的响应。
- Follower发送ACK消息。Follower接收到LEADERINFO后,会解析出epoch和ZXID,然后向Leader发送ACK消息
- 数据同步。Leader收到Follower的ACKEPOCH后,即可进⾏数据同步。
- 启动Leader和Follower服务器。当有过半Follower已经完成了数据同步,那么Leader和Follower服务器实例就可以启动了
Leader与Follower启动
- 创建启动会话管理器。
- 初始化Zookeeper请求处理链,集群模式的每个处理器也会在启动阶段串联请求处理链。
- 注册JMX服务。
- 注册Zookeeper服务器实例。将Zookeeper服务器实例注册给ServerCnxnFactory,之后Zookeeper就可以对外提供服务
leader选举
Leader选举是zookeeper最重要的技术之⼀,也是保证分布式数据⼀致性的关键所在。
当Zookeeper集群中的⼀台服务器出现以下两种情况之⼀时,需要进⼊Leader选举。
- 服务器初始化启动。
- 服务器运⾏期间⽆法和Leader保持连接。
服务器启动时期的Leader选举
若进⾏Leader选举,则⾄少需要两台机器。在集群初始化阶段,当有⼀台服务器Server1启动时,其单独⽆法进⾏和完成Leader选举,当第⼆台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进⼊Leader选举过程。选举过程如下
-
每个Server发出⼀个投票:由于是初始情况,Server1(假设myid为1)和Server2假设myid为2)都会将⾃⼰作为Leader服务器来进⾏投票,每次投票会包含所推举的服务器的myid和ZXID,使⽤(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各⾃将这个投票发给集群中其他机器
-
接受来⾃各个服务器的投票:集群的每个服务器收到投票后,⾸先判断该投票的有效性,如检查是否是本轮投票、是否来⾃LOOKING状态的服务器。
-
处理投票:针对每⼀个投票,服务器都需要将别⼈的投票和⾃⼰的投票进⾏PK,PK规则如下
-
优先检查ZXID。ZXID⽐较⼤的服务器优先作为Leader。
-
如果ZXID相同,那么就⽐较myid。myid较⼤的服务器作为Leader服务器。
对于Server1来说,它⾃⼰的投票是(1,0),⽽接收到的投票为(2,0)。⾸先会对⽐两者的ZXID,因为都是0,所以⽆法决定谁是Leader。接下来会对⽐两者的myid,很显然,Server1发现接收到的投票中的myid是2,⼤于⾃⼰,于是就会更新⾃⼰的投票为(2,0),然后重新将投票发出去。⽽对于Server2来说,不需要更新⾃⼰的投票
-
-
统计投票:当Server1和Server2都收到相同的投票信息(2,0)的时候,即认为已经选出了Leader。
-
改变服务器状态 :⼀旦确定了 Leader,每个服务器就会更新⾃⼰的状态:如果是 Follower,那么就变更为FOLLOWING,如果是Leader,那么就变更为LEADING。
服务器运⾏时期的Leader选举
在ZooKeeper集群正常运⾏过程中,⼀旦选出⼀个Leader,那么所有服务器的集群⻆⾊⼀般不会再发⽣变化——也就是说,Leader服务器将⼀直作为集群的Leader,即使集群中有⾮Leader机器挂了或是有新机器加⼊集群也不会影响Leader。但是⼀旦Leader所在的机器挂了,那么整个集群将暂时⽆法对外服务,⽽是进⼊新⼀轮的Leader选举。服务器运⾏期间的Leader选举和启动时期的Leader选举基本过程是⼀致的。
假设当前正在运⾏的 ZooKeeper 机器由 3 台机器组成,分别是 Server1、Server2和Server3,当前的Leader是Server2。假设在某⼀个瞬间,Leader挂了,这个时候便开始了Leader选举。
-
变更状态:Leader挂后,余下的⾮Observer服务器都会将⾃⼰的服务器状态变更为LOOKING,然后开始进⼊Leader选举过程。
-
每个Server会发出⼀个投票:在运⾏期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;
在第⼀轮投票中,Server1和Server3都会投⾃⼰,产⽣投票(1, 123),(3, 122),然后各⾃将投票发送给集群中所有机器。
-
接收来⾃各个服务器的投票,与启动时过程相同
-
处理投票。与启动时过程相同,此时,Server1将会成为Leader
-
统计投票。与启动时过程相同
-
改变服务器的状态。与启动时过程相同
五.源码简介
源码下载和编译
zk源码下载地址:https://github.com/apache/zookeeper/tree/release-3.5.4
注意:因为zk是由ant来构建的,所以需要使⽤ant命令来转换成⼯程,然后导⼊idea将准备好的zookeeper-release-3.5.4导⼊idea中
具体的编译步骤可以参考:基于Idea启动zookeeper源码
简单时序图
单机启动
QuorumPeerMain启动时判断是否是集群的关键代码,initializeAndRun方法:
可以看到单机模式的委托启动类为:ZooKeeperServerMain
ZooKeeperServerMain的main方法如下:
public static void main(String[] args) {ZooKeeperServerMain main = new ZooKeeperServerMain();main.initializeAndRun(args);
}
protected void initializeAndRun(String[] args)throws ConfigException, IOException, AdminServerException
{ServerConfig config = new ServerConfig();//如果⼊参只有⼀个,则认为是配置⽂件的路径if (args.length == 1) {config.parse(args[0]);} else {//否则是各个参数config.parse(args);}runFromConfig(config);
}
其中runFromConfig是关键方法,如下:
public void runFromConfig(ServerConfig config)throws IOException, AdminServerException {LOG.info("Starting server");FileTxnSnapLog txnLog = null;try {// Note that this thread isn't going to be doing anything else,// so rather than spawning another thread, we will just call// run() in this thread.// create a file logger url from the command line args//初始化⽇志⽂件txnLog = new FileTxnSnapLog(config.dataLogDir, config.dataDir);//初始化ZkServer对象final ZooKeeperServer zkServer = new ZooKeeperServer(txnLog,config.tickTime, config.minSessionTimeout, config.maxSessionTimeout, null);txnLog.setServerStats(zkServer.serverStats());// Registers shutdown handler which will be used to know the// server error or shutdown state changes.final CountDownLatch shutdownLatch = new CountDownLatch(1);zkServer.registerServerShutdownHandler(new ZooKeeperServerShutdownHandler(shutdownLatch));// Start Admin serveradminServer = AdminServerFactory.createAdminServer();adminServer.setZooKeeperServer(zkServer);adminServer.start();boolean needStartZKServer = true;if (config.getClientPortAddress() != null) {//初始化server端IO对象,默认是NIOServerCnxnFactorycnxnFactory = ServerCnxnFactory.createFactory();//初始化配置信息cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false);//启动服务cnxnFactory.startup(zkServer);// zkServer has been started. So we don't need to start it again in secureCnxnFactory.needStartZKServer = false;}if (config.getSecureClientPortAddress() != null) {secureCnxnFactory = ServerCnxnFactory.createFactory();secureCnxnFactory.configure(config.getSecureClientPortAddress(), config.getMaxClientCnxns(), true);secureCnxnFactory.startup(zkServer, needStartZKServer);}//container ZNodes是3.6版本之后新增的节点类型,Container类型的节点会在它没有⼦节点时被删除(新创建的Container节点除外),该类就是⽤来周期性的进⾏检查清理⼯作containerManager = new ContainerManager(zkServer.getZKDatabase(), zkServer.firstProcessor,Integer.getInteger("znode.container.checkIntervalMs", (int) TimeUnit.MINUTES.toMillis(1)),Integer.getInteger("znode.container.maxPerMinute", 10000));containerManager.start();// Watch status of ZooKeeper server. It will do a graceful shutdown// if the server is not running or hits an internal error.shutdownLatch.await();shutdown();if (cnxnFactory != null) {cnxnFactory.join();}if (secureCnxnFactory != null) {secureCnxnFactory.join();}if (zkServer.canShutdown()) {zkServer.shutdown(true);}} catch (InterruptedException e) {// warn, but generally this is okLOG.warn("Server interrupted", e);} finally {if (txnLog != null) {txnLog.close();}}}
zk单机模式启动的主要流程中主要是解析配置和启动,解析配置在configure方法中,启动是在startup方法中
解析配置
configure的具体实现在NIOServerCnxnFactory类中
public void configure(InetSocketAddress addr, int maxcc, boolean secure) throws IOException {if (secure) {throw new UnsupportedOperationException("SSL isn't supported in NIOServerCnxn");}configureSaslLogin();//会话超时时间maxClientCnxns = maxcc;sessionlessCnxnTimeout = Integer.getInteger(ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000);// We also use the sessionlessCnxnTimeout as expiring interval for// cnxnExpiryQueue. These don't need to be the same, but the expiring// interval passed into the ExpiryQueue() constructor below should be// less than or equal to the timeout.//过期队列cnxnExpiryQueue =new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);//过期线程,从cnxnExpiryQueue中读取数据,如果已经过期则关闭expirerThread = new ConnectionExpirerThread();//根据CPU个数计算selector线程的数量int numCores = Runtime.getRuntime().availableProcessors();// 32 cores sweet spot seems to be 4 selector threadsnumSelectorThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_SELECTOR_THREADS,Math.max((int) Math.sqrt((float) numCores/2), 1));if (numSelectorThreads < 1) {throw new IOException("numSelectorThreads must be at least 1");}//计算woker线程的数量numWorkerThreads = Integer.getInteger(ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2 * numCores);//worker线程关闭时间workerShutdownTimeoutMS = Long.getLong(ZOOKEEPER_NIO_SHUTDOWN_TIMEOUT, 5000);LOG.info("Configuring NIO connection handler with "+ (sessionlessCnxnTimeout/1000) + "s sessionless connection"+ " timeout, " + numSelectorThreads + " selector thread(s), "+ (numWorkerThreads > 0 ? numWorkerThreads : "no")+ " worker threads, and "+ (directBufferBytes == 0 ? "gathered writes." :("" + (directBufferBytes/1024) + " kB direct buffers.")));//初始化selector线程for(int i=0; i<numSelectorThreads; ++i) {selectorThreads.add(new SelectorThread(i));}this.ss = ServerSocketChannel.open();ss.socket().setReuseAddress(true);LOG.info("binding to port " + addr);ss.socket().bind(addr);ss.configureBlocking(false);//初始化accept线程,这⾥看出accept线程只有⼀个,⾥⾯会注册监听ACCEPT事件acceptThread = new AcceptThread(ss, addr, selectorThreads);}
启动
startup的具体实现在NIOServerCnxnFactory类中:
public void startup(ZooKeeperServer zks, boolean startServer)throws IOException, InterruptedException {//启动线程start();setZooKeeperServer(zks);if (startServer) {//初始化数据结构zks.startdata();//启动剩余项⽬zks.startup();}}
start方法:
public void start() {stopped = false;if (workerPool == null) {//初始化worker线程池workerPool = new WorkerService("NIOWorker", numWorkerThreads, false);}for(SelectorThread thread : selectorThreads) {//挨个启动select线程if (thread.getState() == Thread.State.NEW) {thread.start();}}// ensure thread is started once and only once//启动acceptThread线程if (acceptThread.getState() == Thread.State.NEW) {acceptThread.start();}//启动expirerThread线程if (expirerThread.getState() == Thread.State.NEW) {expirerThread.start();}}
startdata方法的具体实现在ZooKeeperServer类中:
public void startdata()
throws IOException, InterruptedException {//check to see if zkDb is not null//初始化ZKDatabase,该数据结构⽤来保存ZK上⾯存储的所有数据if (zkDb == null) {//初始化数据数据,这⾥会加⼊⼀些原始节点,例如/zookeeperzkDb = new ZKDatabase(this.txnLogFactory);}//加载磁盘上已经存储的数据,如果有的话if (!zkDb.isInitialized()) {loadData();}
}
startup方法的具体实现在ZooKeeperServer类中:
public synchronized void startup() {if (sessionTracker == null) {//初始化session追踪器createSessionTracker();}//启动session追踪器startSessionTracker();//建⽴请求处理链路setupRequestProcessors();registerJMX();setState(State.RUNNING);notifyAll();}
setupRequestProcessors方法:
这⾥可以看出,单机模式下请求的处理链路为:PrepRequestProcessor -> SyncRequestProcessor -> FinalRequestProcessor
protected void setupRequestProcessors() {RequestProcessor finalProcessor = new FinalRequestProcessor(this);RequestProcessor syncProcessor = new SyncRequestProcessor(this,finalProcessor);((SyncRequestProcessor)syncProcessor).start();firstProcessor = new PrepRequestProcessor(this, syncProcessor);((PrepRequestProcessor)firstProcessor).start();
}
集群启动
集群的启动⼊⼝是QuorumPeerMain类的main⽅法。 main⽅法会调用initializeAndRun方法加载配置⽂件以后调用runFromConfig方法,runFromConfig会调用QuorumPeer类的start⽅法
public synchronized void start() {//校验ServerId是否合法if (!getView().containsKey(myid)) {throw new RuntimeException("My id " + myid + " not in the peer list");}//载⼊之前持久化的⼀些信息loadDataBase();//启动线程监听startServerCnxnFactory();try {adminServer.start();} catch (AdminServerException e) {LOG.warn("Problem starting AdminServer", e);System.out.println(e);}//初始化选举投票以及算法startLeaderElection();//当前也是⼀个线程,注意run⽅法super.start();
}
初始化选举
当⼀个节点启动时需要先发起选举寻找Leader节点,然后再根据Leader节点的事务信息进⾏同步,最后开始对外提供服务,初始化选举的方法如下:
synchronized public void startLeaderElection() {try {//判断当前的状态是否是LOOKINGif (getPeerState() == ServerState.LOOKING) {//初始化一个投给自己的选票currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());}} catch(IOException e) {RuntimeException re = new RuntimeException(e.getMessage());re.setStackTrace(e.getStackTrace());throw re;}// if (!getView().containsKey(myid)) {// throw new RuntimeException("My id " + myid + " not in the peer list");//}if (electionType == 0) {try {udpSocket = new DatagramSocket(getQuorumAddress().getPort());responder = new ResponderThread();responder.start();} catch (SocketException e) {throw new RuntimeException(e);}}//初始化选举算法,electionType默认为3this.electionAlg = createElectionAlgorithm(electionType);
}
初始化选举算法createElectionAlgorithm:
protected Election createElectionAlgorithm(int electionAlgorithm){Election le=null;//TODO: use a factory rather than a switchswitch (electionAlgorithm) {case 0:le = new LeaderElection(this);break;case 1:le = new AuthFastLeaderElection(this);break;case 2:le = new AuthFastLeaderElection(this, true);break;case 3:QuorumCnxManager qcm = createCnxnManager();//监听选举事件的listenerQuorumCnxManager oldQcm = qcmRef.getAndSet(qcm);if (oldQcm != null) {LOG.warn("Clobbering already-set QuorumCnxManager (restarting leader election?)");oldQcm.halt();}QuorumCnxManager.Listener listener = qcm.listener;if(listener != null){//开启监听器listener.start();//初始化选举算法FastLeaderElection fle = new FastLeaderElection(this, qcm);//发起选举fle.start();le = fle;} else {LOG.error("Null listener when initializing cnx manager");}break;default:assert false;}return le;
}
start方法
到QuorumPeer类中start⽅法的最后⼀⾏super.start()执行的是其run方法,主要是进行投票和进行更改服务器状态
public void run() {updateThreadName();LOG.debug("Starting quorum peer");try {jmxQuorumBean = new QuorumBean(this);MBeanRegistry.getInstance().register(jmxQuorumBean, null);for(QuorumServer s: getView().values()){ZKMBeanInfo p;if (getId() == s.id) {p = jmxLocalPeerBean = new LocalPeerBean(this);try {MBeanRegistry.getInstance().register(p, jmxQuorumBean);} catch (Exception e) {LOG.warn("Failed to register with JMX", e);jmxLocalPeerBean = null;}} else {RemotePeerBean rBean = new RemotePeerBean(this, s);try {MBeanRegistry.getInstance().register(rBean, jmxQuorumBean);jmxRemotePeerBean.put(s.id, rBean);} catch (Exception e) {LOG.warn("Failed to register with JMX", e);}}}} catch (Exception e) {LOG.warn("Failed to register with JMX", e);jmxQuorumBean = null;}try {/** Main loop*/while (running) {//根据当前节点的状态执⾏不同流程switch (getPeerState()) {case LOOKING:LOG.info("LOOKING");if (Boolean.getBoolean("readonlymode.enabled")) {LOG.info("Attempting to start ReadOnlyZooKeeperServer");// Create read-only server but don't start it immediatelyfinal ReadOnlyZooKeeperServer roZk =new ReadOnlyZooKeeperServer(logFactory, this, this.zkDb);// Instead of starting roZk immediately, wait some grace// period before we decide we're partitioned.//// Thread is used here because otherwise it would require// changes in each of election strategy classes which is// unnecessary code coupling.Thread roZkMgr = new Thread() {public void run() {try {// lower-bound grace period to 2 secssleep(Math.max(2000, tickTime));if (ServerState.LOOKING.equals(getPeerState())) {roZk.startup();}} catch (InterruptedException e) {LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");} catch (Exception e) {LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);}}};try {roZkMgr.start();reconfigFlagClear();if (shuttingDownLE) {shuttingDownLE = false;startLeaderElection();}//寻找Leader节点setCurrentVote(makeLEStrategy().lookForLeader());} catch (Exception e) {LOG.warn("Unexpected exception", e);setPeerState(ServerState.LOOKING);} finally {// If the thread is in the the grace period, interrupt// to come out of waiting.roZkMgr.interrupt();roZk.shutdown();}} else {try {reconfigFlagClear();if (shuttingDownLE) {shuttingDownLE = false;startLeaderElection();}setCurrentVote(makeLEStrategy().lookForLeader());} catch (Exception e) {LOG.warn("Unexpected exception", e);setPeerState(ServerState.LOOKING);} }break;case OBSERVING:try {LOG.info("OBSERVING");//当前节点启动模式为ObserversetObserver(makeObserver(logFactory));//与Leader节点进⾏数据同步observer.observeLeader();} catch (Exception e) {LOG.warn("Unexpected exception",e );} finally {observer.shutdown();setObserver(null); updateServerState();}break;case FOLLOWING:try {LOG.info("FOLLOWING");//当前节点启动模式为FollowersetFollower(makeFollower(logFactory));//与Leader节点进⾏数据同步follower.followLeader();} catch (Exception e) {LOG.warn("Unexpected exception",e);} finally {follower.shutdown();setFollower(null);updateServerState();}break;case LEADING:LOG.info("LEADING");try {//当前节点启动模式为LeadersetLeader(makeLeader(logFactory));//发送⾃⼰成为Leader的通知leader.lead();setLeader(null);} catch (Exception e) {LOG.warn("Unexpected exception",e);} finally {if (leader != null) {leader.shutdown("Forcing shutdown");setLeader(null);}updateServerState();}break;}start_fle = Time.currentElapsedTime();}} finally {LOG.warn("QuorumPeer main thread exited");MBeanRegistry instance = MBeanRegistry.getInstance();instance.unregister(jmxQuorumBean);instance.unregister(jmxLocalPeerBean);for (RemotePeerBean remotePeerBean : jmxRemotePeerBean.values()) {instance.unregister(remotePeerBean);}jmxQuorumBean = null;jmxLocalPeerBean = null;jmxRemotePeerBean = null;}
}
在经过选举后最终每个节点都会确认⾃⼰的身份,节点根据类型的不同会执⾏以下逻辑:
- 如果是Leader节点,⾸先会想其他节点发送⼀条NEWLEADER信息,确认⾃⼰的身份,等到各个节点的ACK消息以后开始正式对外提供服务,同时开启新的监听器,处理新节点加⼊的逻辑。
- 如果是Follower节点,⾸先向Leader节点发送⼀条FOLLOWERINFO信息,告诉Leader节点⾃⼰已处理的事务的最⼤Zxid,然后Leader节点会根据⾃⼰的最⼤Zxid与Follower节点进⾏同步,如果Follower节点落后的不多则会收到Leader的DIFF信息通过内存同步,如果Follower节点落后的很多则会收到SNAP通过快照同步,如果Follower节点的Zxid⼤于Leader节点则会收到TRUNC信息忽略多余的事务。
- 如果是Observer节点,则与Follower节点相同
FastLeaderElection选举
选举的⽗接⼝为Election,其定义了lookForLeader和shutdown两个⽅法,lookForLeader表示寻找Leader,shutdown则表示关闭,如关闭服务端之间的连接。
Election有三个实现,AuthFastLeaderElection、LeaderElection、FastLeaderElection,其中AuthFastLeaderElection、LeaderElection其在3.4.0之后的版本中已经不建议使⽤
- 外部投票:特指其他服务器发来的投票。
- 内部投票:服务器⾃身当前的投票。
- 选举轮次:ZooKeeper服务器Leader选举的轮次,即logical clock(逻辑时钟)。
- PK:指对内部投票和外部投票进⾏⼀个对⽐来确定是否需要变更内部投票。选票管理
- sendqueue:选票发送队列,⽤于保存待发送的选票。
- recvqueue:选票接收队列,⽤于保存接收到的外部投票。
当 ZooKeeper 服务器检测到当前服务器状态变成 LOOKING 时,就会触发 Leader选举,即调⽤lookForLeader⽅法来进⾏Leader选举。
在集群启动时如果节点初始化的状态为LOOKING,因此启动时直接会调⽤lookForLeader⽅法发起Leader选举
选举的大致过程:
之后每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现⽆法获取到任何外部投票,就⽴即确认⾃⼰是否和集群中其他服务器保持着有效的连接,如果没有连接,则⻢上建⽴连接,如果已经建⽴了连接,则再次发送⾃⼰当前的内部投票
在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进⾏不同的处理。
-
外部投票的选举轮次⼤于内部投票。若服务器⾃身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会⽴即更新⾃⼰的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使⽤初始化的投票来进⾏PK以确定是否变更内部投票。最终再将内部投票发送出去。
-
外部投票的选举轮次⼩于内部投票。若服务器接收的外选票的选举轮次落后于⾃身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理。
-
外部投票的选举轮次等于内部投票。此时可以开始进⾏选票PK,如果消息中的选票更优,则需要更新本服务器内部选票,再发送给其他服务器。
之后再对选票进⾏归档操作,⽆论是否变更了投票,都会将刚刚收到的那份外部投票放⼊选票集合recvset中进⾏归档,其中recvset⽤于记录当前服务器在本轮次的Leader选举中收到的所有外部投票,然后开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,然后再进⾏最后⼀次确认,判断是否⼜有更优的选票产⽣,若⽆,则终⽌投票
选举详细过程:
-
1.⾃增选举轮次。 在 FastLeaderElection 实现中,有⼀个 logicalclock 属性,⽤于标识当前Leader的选举轮次,ZooKeeper规定了所有有效的投票都必须在同⼀轮次中。ZooKeeper在开始新⼀轮的投票时,会⾸先对logicalclock进⾏⾃增操作。
-
2.初始化选票。 在开始进⾏新⼀轮的投票之前,每个服务器都会⾸先初始化⾃⼰的选票。初始化选票也就是对 Vote 属性的初始化。在初始化阶段,每台服务器都会将⾃⼰推举为Leader,
-
3.发送初始化选票。 在完成选票的初始化后,服务器就会发起第⼀次投票。ZooKeeper 会将刚刚初始化好的选票放⼊sendqueue队列中,由发送器WorkerSender负责
-
4.接收外部投票。 每台服务器都会不断地从 recvqueue 队列中获取外部投票。如果服务器发现⽆法获取到任何的外部投票,那么就会⽴即确认⾃⼰是否和集群中其他服务器保持着有效连接。如果发现没有建⽴连接,那么就会⻢上建⽴连接。如果已经建⽴了连接,那么就再次发送⾃⼰当前的内部投票。
-
5.判断选举轮次。 当发送完初始化选票之后,接下来就要开始处理外部投票了。在处理外部投票的时候,会根据选举轮次来进⾏不同的处理。 · 外部投票的选举轮次⼤于内部投票。如果服务器发现⾃⼰的选举轮次已经落后于该外部投票对应服务器的选举轮次,那么就会⽴即更新⾃⼰的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使⽤初始化的投票来进⾏PK以确定是否变更内部投票,最终再将内部投票发送出去。 外部投票的选举轮次⼩于内部投票。
- 如果接收到的选票的选举轮次落后于服务器⾃身的,那么ZooKeeper就会直接忽略该外部投票,不做任何处理,并返回第四步。
- 外部投票的选举轮次和内部投票⼀致。 这也是绝⼤多数投票的场景,如外部投票的选举轮次和内部投票⼀致的话,那么就开始进⾏选票PK。 总的来说,只有在同⼀个选举轮次的投票才是有效的投票。
-
6.选票PK。 在收到来⾃其他服务器有效的外部投票后,就要进⾏选票PK了——也就是FastLeaderElection.totalOrderPredicate⽅法的核⼼逻辑。选票PK的⽬的是为了确定当前服务器是否需要变更投票,主要从选举轮次、ZXID和 SID 三个因素来考虑,具体条件如下:在选票 PK 的时候依次判断,符合任意⼀个条件就需要进⾏投票变更。 ·
-
如果外部投票中被推举的Leader服务器的选举轮次⼤于内部投票,那么就需要进⾏投票变更。
-
如果选举轮次⼀致的话,那么就对⽐两者的ZXID。如果外部投票的ZXID⼤于内部投票,那么就需要进⾏投票变更。 · 如果两者的 ZXID ⼀致,那么就对⽐两者的SID。如果外部投票的 SID ⼤于内部投票,那么就需要进⾏投票变更。
-
-
7.变更投票。 通过选票PK后,如果确定了外部投票优于内部投票(所谓的“优于”,是指外部投票所推举的服务器更适合成为Leader),那么就进⾏投票变更——使⽤外部投票的选票信息来覆盖内部投票。变更完成后,再次将这个变更后的内部投票发送出去。
-
8.选票归档。 ⽆论是否进⾏了投票变更,都会将刚刚收到的那份外部投票放⼊“选票集合”recvset中进⾏归档。recvset⽤于记录当前服务器在本轮次的Leader选举中收到的所有外部投票——按照服务器对应的SID来区分,例如,{(1,vote1),(2,vote2),…}。
-
9.统计投票。 完成了选票归档之后,就可以开始统计投票了。统计投票的过程就是为了统计集群中是否已经有过半的服务器认可了当前的内部投票。如果确定已经有过半的服务器认可了该内部投票,则终⽌投票。否则返回第四步。
-
10等待更优选票。等待⼀段时间(默认是 200 毫秒)来确定是否有新的更优的投票
-
11.更新服务器状态。 统计投票后,如果已经确定可以终⽌投票,那么就开始更新服务器状态。服务器会⾸先判断当前被过半服务器认可的投票所对应的Leader服务器是否是⾃⼰,如果是⾃⼰的话,那么就会将⾃⼰的服务器状态更新为 LEADING。如果⾃⼰不是被选举产⽣的 Leader 的话,那么就会根据具体情况来确定⾃⼰是FOLLOWING或是OBSERVING。
以上 10 个步骤,就是 FastLeaderElection 选举算法的核⼼步骤,其中步骤 4~9 会经过⼏轮循环,直到Leader选举产⽣。另外还有⼀个细节需要注意,就是在完成步骤9之后,如果统计投票发现已经有过半的服务器认可了当前的选票,这个时候,ZooKeeper 并不会⽴即进⼊步骤 11来更新服务器状态,⽽是会等待⼀段时间(默认是 200 毫秒)来确定是否有新的更优的投票
源码如下:
public Vote lookForLeader() throws InterruptedException {try {self.jmxLeaderElectionBean = new LeaderElectionBean();MBeanRegistry.getInstance().register(self.jmxLeaderElectionBean, self.jmxLocalPeerBean);} catch (Exception e) {LOG.warn("Failed to register with JMX", e);self.jmxLeaderElectionBean = null;}if (self.start_fle == 0) {self.start_fle = Time.currentElapsedTime();}try {HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();int notTimeout = finalizeWait;synchronized(this){// ⾸先会将逻辑时钟⾃增,每进⾏⼀轮新的leader选举,都需要更新逻辑时钟logicalclock.incrementAndGet();// 更新选票(初始化选票)updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());}LOG.info("New election. My id = " + self.getId() +", proposed zxid=0x" + Long.toHexString(proposedZxid));//向所有投票节点发送⾃⼰的投票信息sendNotifications();/** Loop in which we exchange notifications until we find a leader*/while ((self.getPeerState() == ServerState.LOOKING) &&(!stop)){/** Remove next notification from queue, times out after 2 times* the termination time*///读取各个节点返回的投票信息Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS);/** Sends more notifications if haven't received enough.* Otherwise processes new notification.*/// ⽆法获取选票 超时重发if(n == null){//如果前⾯待发送的消息已经全部发送,则重新发送if(manager.haveDelivered()){sendNotifications();} else {//否则尝试与各个节点建⽴连接manager.connectAll();}/** Exponential backoff*///退避算法修改下次等待时间int tmpTimeOut = notTimeout*2;notTimeout = (tmpTimeOut < maxNotificationInterval?tmpTimeOut : maxNotificationInterval);LOG.info("Notification time out: " + notTimeout);} else if (validVoter(n.sid) && validVoter(n.leader)) {/** Only proceed if the vote comes from a replica in the current or next* voting view for a replica in the current or next voting view.*/switch (n.state) {case LOOKING:// If notification > current, replace and send messages out//如果节点的周期⼤于⾃⼰的if (n.electionEpoch > logicalclock.get()) {// 重新赋值逻辑时钟logicalclock.set(n.electionEpoch);//清除已收到的投票信息recvset.clear();//两个节点根据epoch,zxid,serverId来判断新的投票信息if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {// 选出较优的服务器 更新选票updateProposal(n.leader, n.zxid, n.peerEpoch);} else {// ⽆法选出较优的服务器 更新选票updateProposal(getInitId(),getInitLastLoggedZxid(),getPeerEpoch());}//修改选举周期以及投票信息,发起新⼀轮投票sendNotifications();} else if (n.electionEpoch < logicalclock.get()) {// 选举周期⼩于逻辑时钟,不做处理,直接忽略if(LOG.isDebugEnabled()){LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"+ Long.toHexString(n.electionEpoch)+ ", logicalclock=0x" + Long.toHexString(logicalclock.get()));}break;} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)) {//如果对⽅的epoch,zxid,serverId⽐⾃⼰⼤//则更新⾃⼰的投票给n的投票节点updateProposal(n.leader, n.zxid, n.peerEpoch);//重新发送⾃⼰新的投票信息sendNotifications();}if(LOG.isDebugEnabled()){LOG.debug("Adding vote: from=" + n.sid +", proposed leader=" + n.leader +", proposed zxid=0x" + Long.toHexString(n.zxid) +", proposed election epoch=0x" + Long.toHexString(n.electionEpoch));}// don't care about the version if it's in LOOKING state//把节点的投票信息记录下recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));//统计投票信息,判断当前选举是否可以结束,也就是收到的票数信息已经⾜够确认Leaderif (termPredicate(recvset,new Vote(proposedLeader, proposedZxid,logicalclock.get(), proposedEpoch))) {// 若能选出leader// Verify if there is any change in the proposed leader// 遍历已经接收的投票集合while((n = recvqueue.poll(finalizeWait,TimeUnit.MILLISECONDS)) != null){if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)){// 选票有变更,⽐之前提议的Leader有更好的选票加⼊// 将更优的选票放在recvset中recvqueue.put(n);break;}}/** This predicate is true once we don't read any new* relevant message from the reception queue*///如果没有多余的投票信息则可以结束本次选举周期if (n == null) {//根据serverId修改当前节点的类型self.setPeerState((proposedLeader == self.getId()) ?ServerState.LEADING: learningState());// 最终的选票Vote endVote = new Vote(proposedLeader,proposedZxid, logicalclock.get(), proposedEpoch);//清空接收消息队列leaveInstance(endVote);//返回最终的投票信息return endVote;}}break;case OBSERVING://Observer节点不参与投票,忽略LOG.debug("Notification from observer: " + n.sid);break;case FOLLOWING:case LEADING:/** Consider all notifications from the same epoch* together.*///如果周期相同,说明当前节点参与了这次选举if(n.electionEpoch == logicalclock.get()){recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));//判断当前节点收到的票数是否可以结束选举if(termPredicate(recvset, new Vote(n.version, n.leader,n.zxid, n.electionEpoch, n.peerEpoch, n.state))&& checkLeader(outofelection, n.leader, n.electionEpoch)) {self.setPeerState((n.leader == self.getId()) ?ServerState.LEADING: learningState());Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);leaveInstance(endVote);return endVote;}}/** Before joining an established ensemble, verify that* a majority are following the same leader.*///把Leader跟Follower的投票信息加⼊outofelection,确认下它们的信息是否⼀致outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));if (termPredicate(outofelection, new Vote(n.version, n.leader,n.zxid, n.electionEpoch, n.peerEpoch, n.state))&& checkLeader(outofelection, n.leader, n.electionEpoch)) {synchronized(this){logicalclock.set(n.electionEpoch);self.setPeerState((n.leader == self.getId()) ?ServerState.LEADING: learningState());}Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);leaveInstance(endVote);return endVote;}break;default:LOG.warn("Notification state unrecoginized: " + n.state+ " (n.state), " + n.sid + " (n.sid)");break;}} else {if (!validVoter(n.leader)) {LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);}if (!validVoter(n.sid)) {LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);}}}return null;} finally {try {if(self.jmxLeaderElectionBean != null){MBeanRegistry.getInstance().unregister(self.jmxLeaderElectionBean);}} catch (Exception e) {LOG.warn("Failed to unregister with JMX", e);}self.jmxLeaderElectionBean = null;LOG.debug("Number of connection processing threads: {}",manager.getConnectionThreadCount());}
}