一篇文章看懂ZooKeeper内部原理

  • 本文不会讲解任何关于如何通过Zookeeper构建一个应用程序相关的知识,因为这种教程网上非常多,相信大家可以轻而易举的搭建.
  • 本文主要是介绍ZooKeeper内部是如何运行的,通过从高层次介绍其使用的协议,以及ZooKeeper所采用的在提供高性能的同时还具有容错能力的机制.

请求、事务和标识符

首先我们先明确下,在ZooKeeper服务器中的几个基本概念。

请求:请求表示源自客户端发起的操作,事务通常包含节点中新的数据字段值和该节点新的版本号
事务:包含了对应请求处理而改变ZooKeeper状态所需执行的步骤
标识符:当群首收到了一个客户端改变ZooKeeper状态的请求,便会产生一个事务,就会为该事务分配一个标识符,即ZooKeeper会话ID(zxid),通过Zxid对事务进行标识,就可以按照群首所指定的顺序在各个服务器中按序执行.

zxid是一个long型(64)位整数,分为两部分:时间戳(epoch)部分和计数器(counter)部分.

群首选举

群首为集群中的服务器选择出来的一个服务器,并会被集群所认可.设置群首的目的是为了对客户端所发起的ZooKeeper状态变更请求进行排序,包括:createsetData、和delete操作. 群首将每一个请求转换为一个事务,将这些事务发送给追随者,确保集群按照群首确定的顺序接受并处理事务.

这里为了了解管理权的原理,一个服务器必须被仲裁的法定数量的服务器所认可. 法定数量必须集群数量是指能够交错在一起,以避免我们常说的脑裂问题:即两个集合的服务器分别独立运行,形成了两个集群,这种情况将导致整个系统状态不一致.

接下来我们仔细探究下群首选举的过程:

每个服务器在启动后进行LOOKING状态,开始选举一个新的群首或查找已经存在的群首,如果群首已经存在,其他服务器就会通知这个新启动的服务器,告知哪个服务器是群首,与此同时,新的服务器会与群首建立连接,确保自己的状态与群首一致.

如果集群中所有的服务器均处LOOKING状态,这些服务器之间就会进行通信来选举一个群首,通过信息交换对群首选举达成共识的选择.在本次选举中胜出的服务器将进入LEADING状态,而集群中其他服务器将进入FOLLOWING状态.

对于群首选举的通知协议其实非常简单:

当一个服务器进入LOOKING状态就会向集群中每个服务器发送一个通知消息,该消息中包括该服务器的投票信息,投票中包含 服务器标识符(sid)最近执行的事务zxis信息 ,比如,一个服务器所发送的投票信息为(1,5) ,表示该服务器的sid为1, 最近执行的事务的zxid为5,(因为群首选举的目的,zxid只有一个数字,而在其他协议中,zxid则由时间戳和计数器组成).

当一个服务器收到一个投票信息,该服务器将会根据以下规则修改自己的投票信息:

  1. 将接收的voteId和voteZxid作为一个标识符,并获取当前自己投票中的zxid,用myZxid和mySid表示接收服务器自己的值.
  2. 如果(voteZxid < myZxid) 或者(voteZxid = myZxid 且 voteId < mySid),保留当前的投票信息
  3. 否则,修改自己的投票信息,将voteZxid 赋值给 myZxid,将voteId赋值给muSid;

简而言之,只有最新的服务器将赢得选举,因为其拥有最近一次的zxid.

在ZooKeeper中对应的实现选举的Java类为QuorumPeer,其中的run方法实现了服务器的主要工作循环.当进入LOOKING状态,将会执行looForLeader方法来进行群首的选举,该方法主要执行我们上面所讨论的协议,该方法返回前,在该方法中会将服务器状态设置为LEADING 或 FOLLOWING状态,当然还可能为OBSERVING状态.

下面我们通过一副来重温这个协议的执行过程:

群首选举时的长延迟

并不是所有执行过程都如上图所示的那样,在下图中我们,展示了另一种情况的例子.
服务器s2做出了错误的判断,选举了另一个服务器s1而不是服务器s3,虽然s3的zxid值更高,但在从服务器s3向s2传送消息时发生了网络故障导致长时间延迟,与此同时,服务器s2选择了服务器s1作为群首,最终,服务器s3和服务器S1组成了仲裁数量(quorum),并将忽略服务器s2.

从这里可以看出,如果服务器s2在进行群首选举时多等待一会,它就能做出正确的判断.

如果想实现一个新的群首选举算法,我们需要实现一个quorum包中的Election接口.
ZooKeeper包中已经有了让我们自己选择群首选举实现的类,具体可以查看org.apache.zookeeper.server.quorum.QuorumPeer#createElectionAlgorithm方法;

Zab: ZooKeeper 状态更新的广播协议

Zab: ZooKeeper原子广播协议(ZooKeeper Atomic Broadcast protocol),用于服务器提交确认一个更新事务的提交.

下面举个例子,假设我们现在有一个活动的群首服务器,并拥有仲裁数量的追随者支持该群首的管理权,通过该协议提交一个事务,类似于一个两阶段提交:

  1. 群首向所有追随者发送一个PROPOSAL消息p.
  2. 当一个追随者收到消息p后,会响应群首一个ACK消息,通知群首其已接受该提案(proposal)
  3. 当收到仲裁数量的服务器发送确认消息后(该仲裁包括群首自己),群首就会发送消息通知追随者进行提交(COMMIT)操作.

下图说明了这一过程的具体步骤顺序,我们假设群首通过隐式方式给自己发送消息.

提交提案的常规消息模式

在应答消息之前,追随者还需要执行一些检查操作.追随者将会检查所发送的提案消息是否属于其所追随的群首,并确认群首所广播的提案消息和提交事务消失的顺序正确;

Zab保障了以下几个重要属性:
- 如果群首按顺序广播了事务T和事务T’,那么每个服务器在提交T’事务前保证事务T已经提交完成;
- 如果某个服务器按照事务T、事务T’的顺序提交事务,所有其他服务器也必然会在提交事务T’前提交事务T;

第一个属性保证事务在服务器之间的传送顺序一致,而第二个竖向地保证服务器不会跳过任何事务.假设事务为状态变更操作,每个状态变更操作又依赖前一个状态变更操作的结果,如果跳过事务就会导致结果不一致性,而两阶段提交保证了事务的顺序.

深入代码:

大部分Zab的代码存在于Leader、LearnerHandler和Follower。Leader和LearderHandler的实例由群首服务器执行,而Follower的实例由追随者执行。Leader.leadFollower.followLeader 是两个重要方法,他们在服务器在QuorumPeer 中从 LOOKING转换到LEADING或者FOLLOWING时得到调用.

观察者

观察者和追随者之间有一些共同点,具体来说,它们提交来自群首的提议. 不同于追随者的是,观察者不参与选举过程,它们仅仅学习经由INFORM消息提交的提议. 由于群首将状态变化发送给追随者和观察者,这两种服务器也都被称为学习者.

服务器的构成

群首,追随者基本上都是服务器.我们在实现服务器时使用的主要抽象概念是请求处理器. 请求处理器是对处理流水线上不同阶段的抽象. 每一个服务器实现了一个请求处理器的序列.我们可以把一个处理器想象成添加到请求处理的一个元素. 一条请求经过服务器流水线上所有处理器的处理后被称为得到完全处理.

注意: 请求处理器
ZooKeeper代码里有一个交RequestProcessor的接口.这个接口的主要方法是processRequest,它接受一个Request的参数.在一条请求处理器的流水线上,对相邻处理器的请求的处理通常通过队列实现解耦合.当一个处理器有一条请求需要下一个处理器进行处理时,它将这条请求加入队列.然后,它将处于等待状态直到下一个处理器处理完此消息;

独立服务器

ZooKeeper中最简单的流水线式独立服务器(ZooKeeperServer类).下图描述了此类服务器的流水线:

一个独立服务器的流水线

PrepRequestProcessor接口客户端的请求并执行这个请求,处理结果则是生成一个事务. 事务是执行一个操作的结果, 该操作会反映到ZooKeeper的数据树上. 事务信息将会以头部记录和事务记录的方式添加到Request对象中. 同时还要注意,只有改变ZooKeeper状态的操作才会产生事务,对于读操作并不会产生任何事务. 因此,对于读请求的Request对象中,事务成员属性的引用值为null.

下一个请求处理器为SynRequestProcessor. 它负责将事务持久化到磁盘上. 实际上就是将事务数据按照顺序追加到事务日志中, 并生成快照数据.

在下一个处理器也是最后一个为FinalRequestProcessor.如果Request对象包含事务数据,该处理器将会接受对ZooKeeper数据树的修改,否则该处理器会从数据树中读取数据并返回给客户端.

群首服务器

当切换到仲裁模式时,服务器的流水线则有一些变化,下图展示了群首操作流水线(类LeaderZooKeeper)

群首服务器流水线

第一个处理器同样是PrepRequestProcessor,而之后的处理器则为ProposalRequestProcessor.该处理器会准备一个提议,并将该提议发送给跟随者. ProposalRequestProcessor将会把所有请求都转发给CommitRequestProcessor,而且对于写操作请求,还会将请求转发给SyncRequestProcessor 处理器.

SyncRequestProcessor 处理器所执行的操作与独立服务器一样,即持久化操作. 执行完之后会触发AckRequestProcessor处理器,它是一个简单请求处理器,它仅仅生成确认消息并返回给自己. 此处便是上文中提到的,在仲裁模式下,群首需要收到每个服务器的确认消息,也包括自己,而AckRequestProcessor 处理器就负责这个;

CommitRequestProcessor 会将收到足够多的确认消息的提议进行提交. 实际上,确认消息是由Leader类处理的(Leader.processAck()方法),这个方法会将提交的请求加入到CommitRequestProcessor类中的一个队列中.这个队列会由请求处理器线程进行处理.

FinalRequestProcessor处理器,作用与独立服务器一样,不过在它之前还有一个简单的请求处理器,这个处理器会从提议列表中删除那些待接受的提议,这个处理器的名字叫ToBeAppliedRequestProcessor,待接受请求列表包括那些已经被仲裁法定人数所确认的请求,并等待执行. 群首使用这个列表与追随者之间进行同步,并将收到确认消息的请求加入到这个列表中. 之后 ToBeAppliedRequestProcessor 处理器就会在 FinalRequestProcessor处理器执行后删除这个列表中的元素;

注意: 只有更新请求才会加入到待接受请求列表中,然后由ToBeAppliedRequestProcessor处理器从该列表删除. ToBeAppliedRequestProcessor处理器并不会对读取请求进行任何额外的处理操作,而是由FinalRequestProcessor处理器进行操作.

追随者服务器

最后来看一下追随者(FollowerRequestProcessor类),下图展示了一个追随者服务器中会用到的请求处理器:

追随者服务器流水线

首先从FollowerRequestProcessor处理器开始,该处理器接收并处理客户端请求. FollowerRequestProcessor 处理器之后转发请求给CommitRequestProcessor,同时也会转发写请求到群首服务器. CommitRequestProcessor 会直接转发读请求到FinalRequestProcessor 处理器, 而且对于写请求, CommitRequestProcessor在转发给 FinalRequestProcessor处理器之前会等待提交事务.

当群首接收到一个新的写请求操作时,直接地或通过其他追随者服务器生成一个提议(proposal),之后转发到追随者服务器. 当收到一个提议,追随者会发送这个提议道SyncRequestProcessor 处理器,SendRequestProcessor会向群首发送确认消息. 当群首收到足够确认消息来提交这个提议时,群首就会发送提交事务消息给追随者(同时也会发哦少年宫INFORM消息给观察者服务器). 当接收到提交事务消息时,追随者就通过CommitRequestProcessor 处理器进行处理.

为了保证执行的顺序,CommitRequestProcessor 处理器会在收到一个写请求处理器时暂停后续的请求处理. 这就意味着, 在一个写请求之后接收到的任何读取请求都将被阻塞,直到读取请求转给CommitRequestProcessor处理器. 通过等待的方式, 请求可以被保证按照接收的顺序来执行.

小结:

本文讨论了ZooKeeper核心机制问题. 群首竞选机制是可用性的关键因素,没有这个机制,ZooKeeper套件将无法保持可靠性. Zookeeper还需要Zab协议来传播状态的更新. 紧接着 阐述了多种服务器类型:包括独立服务器,群首服务器,追随者服务器. 这些服务器之间因运转机制和执行协议的不同而不同. 在不同的部署场景中,各个服务器可以发挥不同的作用, 比如增加观察者服务器可以提供更高的读吞吐量,而且还不会影响写吞吐量. 不过增加观察者服务器并不会增加整个系统的高可用性.

猜你喜欢

转载自blog.csdn.net/taurus_7c/article/details/81143830