Zookeeper深入进阶
ZAB协议
概念
在深入了解zookeeper之前,很多同学可能会认为zookeeper就是paxos算法的一个实现,但事实上,z0okeeper并没有完全采用paxos算法,而是使用了一种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)的协议作为其数据一致性的核心算法。 ZAB协议并不像Paxos算法那样是一种通用的分布式一致性算法,它是一种特别为z0okeeper专门设计的一种支持崩溃恢复的原子广播协议
在zookeeper中,主要就是依赖ZAB协议来实现分布式数据的一致性,基于该协议,Zookeeper实现了一种主备模式的系统架构来保持集群中各副本之间的数据的一致性,表现形式就是 使用一个单一的主进程来接收并处理客户端的所有事务请求, 并采用ZAB的原子广播协议。将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程中,ZAB协议的主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求。但是,也要考虑到主进程在任何时候都有可能出现崩溃退出或重启现象,因此,ZAB协议还需要做到当前主进程当出现上述异常情况的时候,依旧能正常工作。
ZAB核心
ZAB协议的核心是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理方式
即∶所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为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服务器。
接下来我们就重点讲解一下ZAB协议的消息广播过程和崩溃恢复过程
-
消息广播
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协议的这个基于原子广播协议的消息广播过程,在正常情况下运行非常良好,但是一旦在Leader服务器出现崩溃,或者由于网络原因导致Leader服务器失去了与过半Follower的联系,那么就会进入峁溃恢复模式。在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader 服务器,因此,ZAB协议需要一个高效且可靠的Leader选举算法,从而保证能够快速地选举出新的Leader,同时,Leader选举算法不仅仅需要让Leader自身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产生出来的新Leader服务器。-
基本特性
根据上面的内容,我们了解到,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服务器会首先确认事务日志中的所有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算法则用干构建一个分布式的一致性状态机系统
服务器角色
Leader
Leader服务器是Zookeeper集群工作的核心,其主要工作有以下两个∶
1. 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
2. 集群内部各服务器的调度者。
-
请求处理链
使用责任链来处理每个客户端的请求是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的请求处理链路和Follower服务器也非常相近,其处理链如下
另外需要注意的一点是,虽然在图中可以看到,Observer 服务器在初始化阶段会将
SyncRequestProcessor处理器也组装上去,但是在实际运行过程中,Leader服务器不会将事务请求的投票发送给Observer服务器。
服务器启动
服务端整体架构图
Zookeeper服务器启动,大致可以分为以下五个步骤:
- 配置文件解析
- 初始化数据管理器
- 初始化网络I/0管理器
- 数据恢复
- 对外服务
单机版服务器启动
单机版服务器的启动其流程图如下
上图的过程可以分为预启动和初始化过程。
-
预启动 1
1.统一由QuorumPeerMain作为启动类。无论单机或集群,在zkServer.cmd和zkServer.sh 中都配置了QuorumPeerMain作为启动入口类。 2.解析配置文件zoo.cfg。zoo.cfg配置运行时的基本参数,如tickTime、dataDir、 clientPort等参数。 3.创建并启动历史文件清理器DatadirCleanupManager。对事务日志和快照数据文件进行定 时清理。 4.判断当前是集群模式还是单机模式启动。若是单机模式,则委托给zooKeeperServerMain进行启动。 5.再次进行配置文件zoo.cfg的解析。 6.创建服务器实例zooKeeperServer。zookeeper服务器首先会进行服务器实例的创建,然后对该服务器实例进行初始化,包括连接器、内存数据库、请求处理器等组件的初始化。
-
初始化
1.创建服务器统计器ServerStats。ServerStats是zookeeper服务器运行时的统计器。 2.创建zookeeper数据管理器FileTxnSnapLog。FileTxnSnapLog是zookeeper上层服务器和底层数据存储之间的对接层,提供了一系列操作数据文件的接口,如事务日志文件和快照数据文件。zookeeper根据zoo.cfg文件中解析出的快照数据目录dataDir和事务日志目录dataLogDir来创建FileTxnSnapLog。 3.设置服务器tickTime和会话超时时间限制。 4.创建ServerCnxnFactory。通过配置系统属性zookeper.serverCnxnFactory来指定使用zookeeper自己实现的NIO还是使用Netty框架作为zookeeper服务端网络连接工厂。 5.初始化ServerCnxnFactory。Zookeeper会初始化Thread作为ServerCnxnFactory的主线程,然后再初始化NIO服务器。 6.启动ServerCnxnFactory主线程。进入Thread的run方法,此时服务端还不能处理客户端请求。 7.恢复本地数据。启动时,需要从本地快照数据文件和事务日志文件进行数据恢复。 8.创建并启动会话管理器。zookeeper会创建会话管理器SessionTracker进行会话管理。 9.初始化zookeeper的请求处理链。zookeeper请求处理方式为责任链模式的实现。会有多个请求处理器依次处理一个客户端请求,在服务器启动时,会将这些请求处理器串联成一个请求处理链。 10.注册JMx服务。zookeeper会将服务器运行时的一些信息以JMXx的方式暴露给外部。 11.注册zookeeper服务器实例。将zookeeper服务器实例注册给ServerCnxnFactory,之后zookeeper就可以对外提供服务。
至此,单机版的Zookeeper服务器启动完毕。
集群服务器启动
单机和集群服务器的启动在很多地方是一致的,其流程图如下∶
上图的过程可以分为预启动、初始化、Leader选举、Leader与Follower启动期交互、Leader与Follower启动等过程
-
预启动
1.统一由QuorumPeerMain作为启动类。 2.解析配置文件zoo.cfg。 3.创建并启动历史文件清理器DatadirCleanupFactory。 4.判断当前是集群模式还是单机模式的启动。在集群模式中,在zoo.cfg文件中配置了多个服务器地址,可以选择集群启动。
-
初始化
1.创建ServerCnxnFactory。 2.初始化ServerCnxnFactory。 3.创建zookeeper数据管理器FileTxnSnapLog。 4.创建QuorumPeer实例。Quorum是集群模式下特有的对象,是zookeeper服务器实例(zooKeeperServer)的托管者,QuorumPeer代表了集群中的一台机器,在运行期间,QuorumPeer会不断检测当前服务器实例的运行状态,同时根据情况发起Leader选举。 5.创建内存数据库zKDatabase。zKDatabase负责管理zooKeeper的所有会话记录以及DataTree和事务日志的存储。 6.初始化QuorumPeer。将核心组件如FileTxnSnapLog、ServerCnxnFactory、ZKDatabase 注册到QuorumPeer中,同时配置QuorumPeer的参数,如服务器列表地址、Leader选举算法和会话超时时间限制等。 7.恢复本地数据。 8. 启动serverCnxnFactory主线程
-
Leader选举
1.初始化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选举端口的监听,等待集群中其他服务器创建连接。 2.注册JMX服务。 3.检测当前服务器状态 运行期间,QuorumPeer会不断检测当前服务器状态。 在正常情况下,zookeeper服务器的状态在LOOKING、LEADING、FOLLOWING/OBSERVING之间进行切换。 在启动阶段,OuorumPeer的初始状态是LOOKING,因此开始进行Leader选举 4.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服务器的连接请求。
- Learner服务器开始和Leader建立连接。所有Learner会找到Leader服务器.并与其建立连接。
- Leader服务器创建LearnerHandler。Leader接收到来自其他机器连接创建请求后,会创建一个LearnerHandler实例,每个LearnerHandler实例都对应一个Leader与Learner服务器之间的连接,其负责Leader和Learner服务器之间几乎所有的消息通信和数据同步。
- 向Leader注册。Learner完成和Leader的连接后,会向Leader进行注册,即将Learner服务器的基本信息(Learnerlnfo),包括SID和ZXID,发送给Leader服务器。
- Leader解析Learner信息,计算新的epoch。Leader接收到Learner服务器基本信息后,会解析出该Learner的SID和ZXID,然后根据ZXID解析出对应的epoch of learner,并和当前Leader服务器的epoch_of_leader进行比较,如果该Learner的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的响应。
- Learner发送ACK消息。Learner接收到LEADERINFO后,会解析出epoch和ZXID,然后向Leader反馈一个ACKEPOCH响应。
- 数据同步。Leader收到Learner的ACKEPOCH后,即可进行数据同步。
- 启动Leader和Learner服务器。当有过半Learner已经完成了数据同步,那么Leader和Learner服务器实例就可以启动了
-
Leader和Follower启动
- 创建启动会话管理器。
- 初始化Zookeeper请求处理链,集群模式的每个处理器也会在启动阶段串联请求处理链。
- 注册JMX服务。
至此,集群版的Zookeeper服务器启动完毕
leader选举
Leader选举概述
Leader选举是zookeeper最重要的技术之一,也是保证分布式数据一致性的关键所在。当Zookeeper集群中的一台服务器出现以下两种情况之一时,需要进入Leader选举。
(1)服务器初始化启动。
(2)服务器运行期间无法和Leader保持连接。
下面就两种情况进行分析讲解。
服务器启动时期的Leader选举
若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器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和Server2实际是如何进行投票处理的。对于Server1来说,它自己的投票是(1,0),而接收到的投票为(2,0)。首先会对比两者的ZXID,因为都是0,所以无法决定谁是Leader。接下来会对比两者的mvid,很显然,Server1发现接收到的投票中的mvid是2.大于自己,于是就会更新自己的投票为(2,0),然后重新将投票发出去。而对干Server2来说,不需要更新自己的投票
-
统计投票
每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息。 对于Server1和Server2服务器来说,都统计出集群中已经有两台机器接受了(2,0)这个投票信息。 这里我们需要对"过半"的概念做一个简单的介绍。 所谓"过半"就是指大干集群机器数量的一半,即大干或等干(n/2+1)。 对于这里由3台机器构成的集群,大于等于2台即为达到"过半"要求。
那么,当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\
- 统计投票。与启动时过程相同
- 改变服务器的状态。与启动时过程相同
Zookeeper源码分析
源码环境搭建
将准备好的zookeeper-release-3.5.4导入idea中
启动服务端
运行主类 org.apache.zookeeper.server.zooKeeperServerMain,将z00.cfg的完整路径配置在Program arguments。
在VM options配置,即指定到conf目录下的log4j.properties∶
```xml
-Dlog4j.configuration=file:/Users/ericsun/Desktop/zookeeper-release-3.5.4/conf/log4j.properties
```
运行输出日志如下
可以得知单机版启动成功,单机版服务端地址为127.0.0.1∶2182。
运行客户端
通过运行 zooKeeperServerMain得到的日志,可以得知ZooKeeper服务端已经启动,服务的地址为 127.0.0.1∶2181。启动客户端来进行连接测试。
客户端的启动类为org.apache.zookeeper.zooKeeperMain,进行如下配置∶
即客户端连接127.0.0.1∶2182,获取节点/lg的信息。
Zookeeper源码分析之单机模式服务端启动
执行过程概述
单机模式的ZK服务端逻辑写在ZooKeeperServerMain类中,由里面的main函数启动,整个过程可以分为如下几步∶
- 第一步,配置解析∶ 解析配置(可以是指定配置文件路径也可以由启动参数设置),比如快照文件,日志文件保存路径,监听端口等等。
- 第二步,启动IO监听线程∶以NIO为例,ZK构建了一套IO模型,一个acceptThread,通过CPU个数计算出来的selectorThread以及一个worker线程池。其中acceptThread收到连接以后按照轮训策略交给selectorThread处理,selectorThread读取完数据以后交给worker线程池进行处理。注∶在ZK状态没有修改为RUNNING之前,IO线程虽然启动监听但不会真正接收请求。
- 第三步,加载数据∶我们知道ZK会定期把数据dump到磁盘,因此每次启动时都会根据第一步中配置的文件路径去读取数据文件,如果存在的话就加载配置,这样就可以用于数据恢复。
- 第四步,构造处理链 单机模式下,ZK的请求处理链路为PrepRequestProcessor ->
SyncRequestProcessor->FinalRequestProcessor 它们的职责如下∶PrepRequestProcessor处理器用于构造请求对象,校验session合法性等。SyncRequestProcessor处理器用于向磁盘中写入事务日志跟快照信息。FinalRequestProcesSor处理器用于修改ZK内存中的数据结构并触发watcher。
第五步,启动服务 修改服务端运行状态,表示服务正式启动,IO线程开始接受请求。
单机模式的委托启动类为∶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);
}
//省略部分代码,只保留了核⼼逻辑
public void runFromConfig(ServerConfig config) throws IOException,AdminServerException
{
FileTxnSnapLog txnLog = null;
try {
//初始化⽇志⽂件
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());
if (config.getClientPortAddress() != null) {
//初始化server端IO对象,默认是NIOServerCnxnFactory
cnxnFactory = ServerCnxnFactory.createFactory();
//初始化配置信息
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns(), false);
//启动服务
cnxnFactory.startup(zkServer);
}
//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();
//省略关闭逻辑
} catch (InterruptedException e) {
LOG.warn("Server interrupted", e);
} finally {
if (txnLog != null) {
txnLog.close();
}
}
小结∶
zk单机模式启动主要流程∶
- 注册jmx
- 解析ServerConfig配置对象
- 根据配置对象,运行单机zk服务
- 创建管理事务日志和快照FileTxnSnapLog对象,zookeeperServer对象,并设置zkServer的统计对象
- 设置zk服务钩子,原理是通过设置CountDownLatch,调用ZooKeeperServerShutdownHandler的handle方法,可以将触发shutdownLatch.await方法继续执行,即调用shutdown关闭单机服务
- 基于jetty创建zk的admin服务
- 创建连接对象cnxnFactory和secureCnxnFactory(安全连接才创建该对象),用于处理客户端的请求
- 创建定时清除容器节点管理器. 用干处理容器节点下不存在子节点的清理容器节点工作等 可以看到关键点在于解析配置跟启动两个方法,先来看下解析配置逻辑,对应上面的configure方法∶
//依旧省略掉了部分逻辑
public void configure(InetSocketAddress addr, int maxcc, boolean secure) throws IOException
{
maxClientCnxns = maxcc;
//会话超时时间
sessionlessCnxnTimeout = Integer.getInteger(ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000);
//过期队列
cnxnExpiryQueue = new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);
//过期线程,从cnxnExpiryQueue中读取数据,如果已经过期则关闭
expirerThread = new ConnectionExpirerThread();
//根据CPU个数计算selector线程的数量
int numCores = Runtime.getRuntime().availableProcessors();
numSelectorThreads = 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);
//初始化selector线程
for(int i=0; i<numSelectorThreads; ++i) {
selectorThreads.add(new SelectorThread(i));
}
this.ss = ServerSocketChannel.open();
ss.socket().setReuseAddress(true);
ss.socket().bind(addr);
ss.configureBlocking(false);
//初始化accept线程,这⾥看出accept线程只有⼀个,⾥⾯会注册监听ACCEPT事件
acceptThread = new AcceptThread(ss, addr, selectorThreads);
}
再来看下启动逻辑
public void startup(ZooKeeperServer zkServer) throws IOException, InterruptedException
{
startup(zkServer, true);
}
//启动分了好⼏块,⼀个⼀个看
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;
//初始化worker线程池
if (workerPool == null) {
workerPool = new WorkerService("NIOWorker", numWorkerThreads, false);
}
//挨个启动select线程
for(SelectorThread thread : selectorThreads) {
if (thread.getState() == Thread.State.NEW) {
thread.start();
}
}
//启动acceptThread线程
if (acceptThread.getState() == Thread.State.NEW) {
acceptThread.start();
}
//启动expirerThread线程
if (expirerThread.getState() == Thread.State.NEW) {
expirerThread.start();
}
}
//初始化数据结构
public void startdata() throws IOException, InterruptedException {
//初始化ZKDatabase,该数据结构⽤来保存ZK上⾯存储的所有数据
if (zkDb == null) {
//初始化数据数据,这⾥会加⼊⼀些原始节点,例如/zookeeper
zkDb = new ZKDatabase(this.txnLogFactory);
}
//加载磁盘上已经存储的数据,如果有的话
if (!zkDb.isInitialized()) {
loadData();
}
}
//启动剩余项⽬
public synchronized void startup() {
//初始化session᭄᪵器
if (sessionTracker == null) {
createSessionTracker();
}
//启动session᭄᪵器
startSessionTracker();
//建⽴请求处理链路
setupRequestProcessors();
registerJMX();
setState(State.RUNNING);
notifyAll();
}
//这⾥可以看出,单机模式下请求的处理链路为:
//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();
}
源码分析之Leader选举(一)
分析Zookeeper中一个核心的模块,Leader选举。
总结框架图
对于Leader选举,其总体框架图如下图所示
Election源码分析
public interface Election {
public Vote lookForLeader() throws InterruptedException;
public void shutdown();
}
说明:
选举的父接口为Election,其定义了lookForLeader和shutdown两个方法,lookForLeader表示寻找Leader,shutdown则表示关闭,如关闭服务端之间的连接。
AuthFastLeaderElection,同FastLeaderElection算法基本一致,只是在消息中加入了认证信息,其在3.4.0之后的版本中已经不建议使用。
FastLeaderElection,其是标准的fast paxos算法的实现,基于TCP协议进行选举。
源码分析之Leader选举(二)之FastLeaderElection
刚刚学习了Leader选举的总体框架,接着来学习Zookeeper中默认的选举策略,FastLeaderElection。
FastLeaderElection源码分析
类的继承关系
public class FastLeaderElection implements Election {}
说明∶
FastLeaderElection实现了Election接口,其需要实现接口中定义的lookForLeader方法和shutdown方法,其是标准的Fast Paxos算法的实现,各服务器之间基于TCP协议进行选举类的内部类
FastLeaderElection有三个较为重要的内部类,分别为Notification、ToSend、Messenger
-
Notification类
static public class Notification { /* * Format version, introduced in 3.4.14 */ public final static int CURRENTVERSION = 0x1; int version; /* * Proposed leader */ // 被推选的leader的id long leader; /* * zxid of the proposed leader */ // 被推选的leader的事务id long zxid; /* * Epoch */ // 推选者的选举周期 long electionEpoch; /* * current state of sender */ // 推选者的状态 QuorumPeer.ServerState state; /* * Address of sender */ // 推选者的id long sid; /* * epoch of the proposed leader */ // 被推选者的选举周期 long peerEpoch; @Override public String toString() { return new String(Long.toHexString(version) + " (message format version), " + leader + " (n.leader), 0x" + Long.toHexString(zxid) + " (n.zxid), 0x" + Long.toHexString(electionEpoch) + " (n.round), " + state + " (n.state), " + sid + " (n.sid), 0x" + Long.toHexString(peerEpoch) + " (n.peerEpoch) "); } } static ByteBuffer buildMsg(int state, long leader, long zxid, long electionEpoch, long epoch) { byte requestBytes[] = new byte[40]; ByteBuffer requestBuffer = ByteBuffer.wrap(requestBytes); /* * Building notification packet to send */ requestBuffer.clear(); requestBuffer.putInt(state); requestBuffer.putLong(leader); requestBuffer.putLong(zxid); requestBuffer.putLong(electionEpoch); requestBuffer.putLong(epoch); requestBuffer.putInt(Notification.CURRENTVERSION); return requestBuffer; }
说明∶Notification表示收到的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的id、zxid、选举周期等信息,其buildMsg方法将选举信息封装至ByteBuffer中再进行发送。
-
ToSend类
static public class ToSend { static enum mType {crequest, challenge, notification, ack} ToSend(mType type, long leader, long zxid, long electionEpoch, ServerState state, long sid, long peerEpoch) { this.leader = leader; this.zxid = zxid; this.electionEpoch = electionEpoch; this.state = state; this.sid = sid; this.peerEpoch = peerEpoch; } /* * Proposed leader in the case of notification */ //被推举的leader的id long leader; /* * id contains the tag for acks, and zxid for notifications */ // 被推举的leader的最⼤事务id long zxid; /* * Epoch */ // 推举者的选举周期 long electionEpoch; /* * Current state; */ // 推举者的状态 QuorumPeer.ServerState state; /* * Address of recipient */ // 推举者的id long sid; /* * Leader epoch */ // 被推举的leader的选举周期 long peerEpoch; }
说明∶ToSend表示发送给其他服务器的选举投票信息,也包含了被选举者的id、zxid、选举周期等信息
-
Messenger类
类的内部类
Messenger包含了WorkerReceiver和WorkerSender两个内部类WorkerReceiver
class WorkerReceiver implements Runnable { // 是否终⽌ volatile boolean stop; // 服务器之间的连接 QuorumCnxManager manager; WorkerReceiver(QuorumCnxManager manager) { this.stop = false; this.manager = manager; } public void run() { // 响应 Message response; while (!stop) { // 不终⽌ // Sleeps on receive try{ // 从recvQueue中取出⼀个选举投票消息(从其他服务器发送过来) response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS); // ⽆投票,跳过 if(response == null) continue; /* * If it is from an observer, respond right away. * Note that the following predicate assumes that * if a server is not a follower, then it must be * an observer. If we ever have any other type of * learner in the future, we'll have to change the * way we check for observers. */ if(!self.getVotingView().containsKey(response.sid)) { // 当前的投票者集合不包含服务器 // 获取⾃⼰的投票 Vote current = self.getCurrentVote(); // 构造ToSend消息 ToSend notmsg = new ToSend(ToSend.mType.notification, current.getId(), current.getZxid(), logicalclock, self.getPeerState(), response.sid, current.getPeerEpoch()); // 放⼊sendqueue队列,等待发送 sendqueue.offer(notmsg); } else { // 包含服务器,表示接收到该服务器的选票消息 // Receive new message if (LOG.isDebugEnabled()) { LOG.debug("Receive new notification message. My id = " + self.getId()); } /* * We check for 28 bytes for backward compatibility */ // 检查向后容性 if (response.buffer.capacity() < 28) { LOG.error("Got a short response: " + response.buffer.capacity()); continue; } // 若容量为28,则表示可向后容 boolean backCompatibility = (response.buffer.capacity() == 28); // 设置buffer中的position、limit等属性 response.buffer.clear(); // Instantiate Notification and set its attributes // 创建接收通知 Notification n = new Notification(); // State of peer that sent this message // 推选者的状态 QuorumPeer.ServerState ackstate = QuorumPeer.ServerState.LOOKING; switch (response.buffer.getInt()) { // 读取状态 case 0: ackstate = QuorumPeer.ServerState.LOOKING; break; case 1: ackstate = QuorumPeer.ServerState.FOLLOWING; break; case 2: ackstate = QuorumPeer.ServerState.LEADING; break; case 3: ackstate = QuorumPeer.ServerState.OBSERVING; break; default: continue; } // 获取leader的id n.leader = response.buffer.getLong(); // 获取zxid n.zxid = response.buffer.getLong(); // 获取选举周期 n.electionEpoch = response.buffer.getLong(); n.state = ackstate; // 设置服务器的id n.sid = response.sid; if(!backCompatibility){ // 不向后ّ容 n.peerEpoch = response.buffer.getLong(); } else { // 向后ّ容 if(LOG.isInfoEnabled()){ LOG.info("Backward compatibility mode, server id=" + n.sid); } // 获取选举周期 n.peerEpoch = ZxidUtils.getEpochFromZxid(n.zxid); } /* * Version added in 3.4.14 */ // 确定版本号 n.version = (response.buffer.remaining() >= 4) ? response.buffer.getInt() : 0x0; /* * Print notification info */ if(LOG.isInfoEnabled()){ printNotification(n); } /* * If this server is looking, then send proposed leader */ if(self.getPeerState() == QuorumPeer.ServerState.LOOKING) { // 本服务器为LOOKING状态 // 将消息放⼊recvqueue中 recvqueue.offer(n); /* * Send a notification back if the peer that sent this * message is also looking and its logical clock is * lagging behind. */ if((ackstate == QuorumPeer.ServerState.LOOKING) // 推选者服务器为LOOKING状态 && (n.electionEpoch < logicalclock)) { // 选举周期⼩于逻辑时钟 // 创建新的投票 Vote v = getVote(); // 构造新的发送消息(本服务器⾃⼰的投票) ToSend notmsg = new ToSend(ToSend.mType.notification, v.getId(), v.getZxid(), logicalclock, self.getPeerState(), response.sid, v.getPeerEpoch()); // 将发送消息放置于队列,等待发送 sendqueue.offer(notmsg); } } else { // 推选服务器状态不为LOOKING /* * If this server is not looking, but the one that sent the ack * is looking, then send back what it believes to be the leader. */ // 获取当前投票 Vote current = self.getCurrentVote(); if(ackstate == QuorumPeer.ServerState.LOOKING) { // 为LOOKING状态 if(LOG.isDebugEnabled()){ LOG.debug("Sending new notification. My id = " + self.getId() + " recipient=" + response.sid + " zxid=0x" + Long.toHexString(current.getZxid()) + " leader=" + current.getId()); } ToSend notmsg; if(n.version > 0x0) { // 版本号⼤于0 // 构造ToSend消息 notmsg = new ToSend( ToSend.mType.notification, current.getId(), current.getZxid(), current.getElectionEpoch(), self.getPeerState(), response.sid, current.getPeerEpoch()); } else { // 版本号不⼤于0 // 构造ToSend消息 Vote bcVote = self.getBCVote(); notmsg = new ToSend( ToSend.mType.notification, bcVote.getId(), bcVote.getZxid(), bcVote.getElectionEpoch(), self.getPeerState(), response.sid, bcVote.getPeerEpoch()); } // 将发送消息放置于队列,等待发送 sendqueue.offer(notmsg); } } } } catch (InterruptedException e) { System.out.println("Interrupted Exception while waiting for new message" + e.toString()); } } LOG.info("WorkerReceiver is down"); } }
说明∶WorkerReceiver实现了Runnable接口,是选票接收器。其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue中,在选票接收过程中,如果发现该外部选票的选举轮次小于当前服务器的,那么忽略该外部投票,同时立即发送自己的内部投票。其是将QuorumCnxManager的Message转化为FastLeaderElection的Notification。
其中,WorkerReceiver的主要逻辑在run方法中,其首先会从QuorumCnxManager中的
recvQueue队列中取出其他服务器发来的选举消息,消息封装在Message数据结构中。然后判断消息中的服务器id是否包含在可以投票的服务器集合中,若不是,则会将本服务器的内部投票发送给该服务器,其流程如下if(!self.getVotingView().containsKey(response.sid)){ // 当前的投票者集合不包含服务器 // 获取⾃⼰的投票 Vote current = self.getCurrentVote(); // 构造ToSend消息 ToSend notmsg = new ToSend(ToSend.mType.notification, current.getId(), current.getZxid(), logicalclock, self.getPeerState(), response.sid, current.getPeerEpoch()); // 放⼊sendqueue队列,等待发送 sendqueue.offer(notmsg); }
若包含该服务器,则根据消息(Message)解析出投票服务器的投票信息并将其封装为Notification,然后判断当前服务器是否为LOOKING,若为LOOKING,则直接将Notification放入FastLeaderElection 的reCVqueue (区别于recvOueue)中。然后判断投票服务器是否为LOOKING状态,并目其选举周期小干当前服务器的逻辑时钟,则将本(当前)服务器的内部投票发送给该服务器,否则,直接忽略掉该投票。其流程如下
if(self.getPeerState() == QuorumPeer.ServerState.LOOKING) { // 本服务器为LOOKING 状态 // 将消息放⼊recvqueue中 recvqueue.offer(n); /* * Send a notification back if the peer that sent this * message is also looking and its logical clock is * lagging behind. */ if((ackstate == QuorumPeer.ServerState.LOOKING) // 推选者服务器为LOOKING状态 && (n.electionEpoch < logicalclock)) { // 选举周期⼩于逻辑时钟 // 创建新的投票 Vote v = getVote(); // 构造新的发送消息(本服务器⾃⼰的投票) ToSend notmsg = new ToSend(ToSend.mType.notification, v.getId(), v.getZxid(), logicalclock, self.getPeerState(), response.sid, v.getPeerEpoch()); // 将发送消息放置于队列,等待发送 sendqueue.offer(notmsg); } }
若本服务器的状态不为LOOKING,则会根据投票服务器中解析的version信息来构造ToSend消息,放入Ssendqueue,等待发送,起流程如下
else { // 本服务器状态不为LOOKING /* * If this server is not looking, but the one that sent the ack * is looking, then send back what it believes to be the leader. */ // 获取当前投票 Vote current = self.getCurrentVote(); if(ackstate == QuorumPeer.ServerState.LOOKING) { // 为LOOKING状态 if(LOG.isDebugEnabled()){ LOG.debug("Sending new notification. My id = " + self.getId() + " recipient=" + response.sid + " zxid=0x" + Long.toHexString(current.getZxid()) + " leader=" + current.getId()); } ToSend notmsg; if(n.version > 0x0) { // 版本号⼤于0 // 构造ToSend消息 notmsg = new ToSend( ToSend.mType.notification, current.getId(), current.getZxid(), current.getElectionEpoch(), self.getPeerState(), response.sid, current.getPeerEpoch()); } else { // 版本号不⼤于0 // 构造ToSend消息 Vote bcVote = self.getBCVote(); notmsg = new ToSend( ToSend.mType.notification, bcVote.getId(), bcVote.getZxid(), bcVote.getElectionEpoch(), self.getPeerState(), response.sid, bcVote.getPeerEpoch()); } // 将发送消息放置于队列,等待发送 sendqueue.offer(notmsg); } }
-
WorkerSender
class WorkerSender implements Runnable { // 是否终⽌ volatile boolean stop; // 服务器之间的连接 QuorumCnxManager manager; // 构造器 WorkerSender(QuorumCnxManager manager){ // 初始化属性 this.stop = false; this.manager = manager; } public void run() { while (!stop) { // 不终⽌ try { // 从sendqueue中取出ToSend消息 ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS); // 若为空,则跳过 if(m == null) continue; // 不为空,则进⾏处理 process(m); } catch (InterruptedException e) { break; } } LOG.info("WorkerSender is down"); } /** * Called by run() once there is a new message to send. * * @param m message to send */ void process(ToSend m) { // 构建消息 ByteBuffer requestBuffer = buildMsg(m.state.ordinal(), m.leader, m.zxid, m.electionEpoch, m.peerEpoch); // 发送消息 manager.toSend(m.sid, requestBuffer); } }
说明∶WorkerSender也实现了Runnable接口,为选票发送器,其会不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中,其过程是将FastLeaderElection的ToSend转化为QuorumCnxManager的Message。
类的属性
protected class Messenger { // 选票发送器 WorkerSender ws; // 选票接收器 WorkerReceiver wr; }
说明∶Messenger中维护了一个WorkerSender和WorkerReceiver,分别表示选票发送器和选票接收器
类的构造函数
Messenger(QuorumCnxManager manager) { // 创建WorkerSender this.ws = new WorkerSender(manager); // 新创建线程 Thread t = new Thread(this.ws, "WorkerSender[myid=" + self.getId() + "]"); // 设置为守护线程 t.setDaemon(true); // 启动 t.start(); // 创建WorkerReceiver this.wr = new WorkerReceiver(manager); // 创建线程 t = new Thread(this.wr, "WorkerReceiver[myid=" + self.getId() + "]"); // 设置为守护线程 t.setDaemon(true); // 启动 t.start(); }
说明∶会启动WorkerSender和WorkerReceiver,并设置为守护线程。
类的属性
public class FastLeaderElection implements Election { // ⽇志 private static final Logger LOG = LoggerFactory.getLogger(FastLeaderElection.class); /** * Determine how much time a process has to wait * once it believes that it has reached the end of * leader election. */ // 完成Leader选举之后需要等待时⻓ final static int finalizeWait = 200; /** * Upper bound on the amount of time between two consecutive * notification checks. This impacts the amount of time to get * the system up again after long partitions. Currently 60 seconds. */ // 两个连续通知检查之间的最⼤时⻓ final static int maxNotificationInterval = 60000; /** * Connection manager. Fast leader election uses TCP for * communication between peers, and QuorumCnxManager manages * such connections. */ // 管理服务器之间的连接 QuorumCnxManager manager; // 选票发送队列,⽤于保存待发送的选票 LinkedBlockingQueue<ToSend> sendqueue; // 选票接收队列,⽤于保存接收到的外部投票 LinkedBlockingQueue<Notification> recvqueue; // 投票者 QuorumPeer self; Messenger messenger; // 逻辑时钟 volatile long logicalclock; /* Election instance */ // 推选的leader的id long proposedLeader; // 推选的leader的zxid long proposedZxid; // 推选的leader的选举周期 long proposedEpoch; // 是否停⽌选举 volatile boolean stop; }
说明∶其维护了服务器之间的连接(用于发送消息)、发送消息队列、接收消息队列、推选者的一些信息(zxid、id)、是否停止选举流程标识等。
类的构造函数
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){ // 字段赋值 this.stop = false; this.manager = manager; // 初始化其他信息 starter(self, manager); }
说明∶构造函数中初始化了stop字段和manager字段,并且调用了starter函数,其源码如下
private void starter(QuorumPeer self, QuorumCnxManager manager) { // 赋值,对Leader和投票者的ID进⾏初始化操作 this.self = self; proposedLeader = -1; proposedZxid = -1; // 初始化发送队列 sendqueue = new LinkedBlockingQueue<ToSend>(); // 初始化接收队列 recvqueue = new LinkedBlockingQueue<Notification>(); // 创建Messenger,会启动接收器和发送器线程 this.messenger = new Messenger(manager); }
说明∶其完成在构造函数中未完成的部分,如会初始化FastLeaderElection的sendqueue和recvqueue,并且启动接收器和发送器线程。
核心函数分析
1.sendNotifications函数private void sendNotifications() { for (QuorumServer server : self.getVotingView().values()) { // 遍历投票参与者集合 long sid = server.id; // 构造发送消息 ToSend notmsg = new ToSend(ToSend.mType.notification, proposedLeader, proposedZxid, logicalclock, QuorumPeer.ServerState.LOOKING, sid, proposedEpoch); if(LOG.isDebugEnabled()){ LOG.debug("Sending Notification: " + proposedLeader + " (n.leader), 0x" + Long.toHexString(proposedZxid) + " (n.zxid), 0x" + Long.toHexString(logicalclock) + " (n.round), " + sid + " (recipient), " + self.getId() + " (myid), 0x" + Long.toHexString(proposedEpoch) + " (n.peerEpoch)"); } // 将发送消息放置于队列 sendqueue.offer(notmsg); } }
说明∶其会遍历所有的参与者投票集合,然后将自己的选票信息发送至上述所有的投票者集合,其并非同步发送,而是将ToSend消息放置于sendqueue中,之后由WorkerSender进行发送。
2.totalOrderPredicate函数
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) { LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: 0x" + Long.toHexString(newZxid) + ", proposed zxid: 0x" + Long.toHexString(curZxid)); if(self.getQuorumVerifier().getWeight(newId) == 0){ // 使⽤计票器判断当前 服务器的权重是否为0 return false; } /* * We return true if one of the following three cases hold: * 1- New epoch is higher * 2- New epoch is the same as current epoch, but new zxid is higher * 3- New epoch is the same as current epoch, new zxid is the same * as current zxid, but server id is higher. */ // 1. 判断消息⾥的epoch是不是⽐当前的⼤,如果⼤则消息中id对应的服务器就是leader // 2. 如果epoch相等则判断zxid,如果消息⾥的zxid⼤,则消息中id对应的服务器就是 leader // 3. 如果前⾯两个都相等那就⽐较服务器id,如果⼤,则其就是leader return ((newEpoch > curEpoch) || ((newEpoch == curEpoch) && ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId))))); }
说明∶该函数将接收的投票与自身投票进行PK,查看是否消息中包含的服务器id是否更优,其按照epoch、zxid、id的优先级进行PK。
3.termPredicate函数
protected boolean termPredicate( HashMap<Long, Vote> votes, Vote vote) { HashSet<Long> set = new HashSet<Long>(); /* * First make the views consistent. Sometimes peers will have * different zxids for a server depending on timing. */ for (Map.Entry<Long,Vote> entry : votes.entrySet()) { // 遍历已经接收的投 票集合 if (vote.equals(entry.getValue())){ // 将等于当前投票的项放⼊set set.add(entry.getKey()); } } //统计set,查看投某个id的票数是否超过⼀半 return self.getQuorumVerifier().containsQuorum(set); }
说明∶该函数用干判断Leader选举是否结束,即是否有一半以上的服务器选出了相同的Leader,其过程是将收到的选票与当前选票进行对比,选票相同的放入同一个集合,之后判断选票相同的集合是否超过了半数。
4.cheackLeader函数
protected boolean checkLeader( HashMap<Long, Vote> votes, long leader, long electionEpoch){ boolean predicate = true; /* * If everyone else thinks I'm the leader, I must be the leader. * The other two checks are just for the case in which I'm not the * leader. If I'm not the leader and I haven't received a message * from leader stating that it is leading, then predicate is false. */ if(leader != self.getId()){ // ⾃⼰不为leader if(votes.get(leader) == null) predicate = false; // 还未选出leader else if(votes.get(leader).getState() != ServerState.LEADING) predicate = false; // 选出的leader还未给出ack信号,其他服务器还不知道leader } else if(logicalclock != electionEpoch) { // 逻辑时钟不等于选举周期 predicate = false; } return predicate; }
说明∶该函数检查是否已经完成了Leader的选举,此时Leader的状态应该是LEADING状态。
5.lookForLeader函数
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 = System.currentTimeMillis(); } try { HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>(); int notTimeout = finalizeWait; synchronized(this){ // 更新逻辑时钟,每进⾏⼀轮选举,都需要更新逻辑时钟 logicalclock++; // 更新选票 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)) { // 本服务器状态为LOOKING并且还未选出leader /* * Remove next notification from queue, times out after 2 times * the termination time */ // 从recvqueue接收队列中取出投票 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()){ // manager已经发送了所有选票消息 // 向所有其他服务器发送消息 sendNotifications(); } else { // 还未发送所有消息 // 连接其他每个服务器 manager.connectAll(); } /* * Exponential backoff */ int tmpTimeOut = notTimeout*2; notTimeout = (tmpTimeOut < maxNotificationInterval? tmpTimeOut : maxNotificationInterval); LOG.info("Notification time out: " + notTimeout); } else if(self.getVotingView().containsKey(n.sid)) { // 投票者集合 中包含接收到消息中的服务器id /* * Only proceed if the vote comes from a replica in the * voting view. */ switch (n.state) { // 确定接收消息中的服务器状态 case LOOKING: // If notification > current, replace and send messages out if (n.electionEpoch > logicalclock) { // 其选举周期⼤于逻 辑时钟 // 重新赋值逻辑时钟 logicalclock = n.electionEpoch; recvset.clear(); 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) { // 选举周期 ⼩于逻辑时钟,不做处理 if(LOG.isDebugEnabled()){ LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x" + Long.toHexString(n.electionEpoch) + ", logicalclock=0x" + Long.toHexString(logicalclock)); } break; } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { // 等于,并且能选出较优的服务器 // 更新选票 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)); } // recvset⽤于记录当前服务器在本轮次的Leader选举中收到的所有外 部投票 recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock, 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)){ // 能够选出较优的服务器 recvqueue.put(n); break; } } /* * This predicate is true once we don't read any new * relevant message from the reception queue */ if (n == null) { self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState()); Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock, proposedEpoch); leaveInstance(endVote); return endVote; } } break; case OBSERVING: LOG.debug("Notification from observer: " + n.sid); break; case FOLLOWING: case LEADING: // 处于LEADING状态 /* * Consider all notifications from the same epoch * together. */ if(n.electionEpoch == logicalclock){ // 与逻辑时钟相等 // 将该服务器和选票信息放⼊recvset中 recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); if(ooePredicate(recvset, outofelection, n)) { // 判 断是否完成了leader选举 // 设置本服务器的状态 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 * a majority is following the same leader. */ outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state)); if(ooePredicate(outofelection, outofelection, n)) { synchronized(this){ logicalclock = 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 unrecognized: {} (n.state), {} (n.sid)", n.state, n.sid); break; } } else { LOG.warn("Ignoring notification from non-cluster member " + 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; } }
说明∶该函数用于开始新一轮的Leader选举,其首先会将逻辑时钟自增,然后更新本服务器的选票信息(初始化选票),之后将选票信息放入sendqueue等待发送给其他服务器,其流程如下
synchronized(this){ // 更新逻辑时钟,每进⾏⼀轮新的leader选举,都需要更新逻辑时钟 logicalclock++; // 更新选票(初始化选票) updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } LOG.info("New election. My id = " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid)); // 向其他服务器发送⾃⼰的选票(已更新的选票) sendNotifications();
之后每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现无法获取到任何外部投票,就立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票,其流程如下
// 从recvqueue接收队列中取出投票 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()){ // manager已经发送了所有选票消息 (表示有连接) // 向所有其他服务器发送消息 sendNotifications(); } else { // 还未发送所有消息(表示⽆连接) // 连接其他每个服务器 manager.connectAll(); } /* * Exponential backoff */ int tmpTimeOut = notTimeout*2; notTimeout = (tmpTimeOut < maxNotificationInterval? tmpTimeOut : maxNotificationInterval); LOG.info("Notification time out: " + notTimeout); }
在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进行不同的处理。
- 外部投票的选举轮次大干内部投票。
若服务器自身的选先举轮次落后干1亥外部投票对应服务器 的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。 - 外部投票的选举轮次小于内部投票。
若服务器接收的外选票的选举轮次落后干自身的选举轮 ,那么Zookeeper就会直接忽略该外部投票,不做任何处理。 - 外部投票的选举轮次等于内部投票。
此时可以开始进行选票PK,如果消息中的选票更优,则 需要更新本服务器内部选票,再发送给其他服务器。
之后再对选票进行归档操作,无论是否变更了投票,都会将刚刚收到的那份外部投票放入选票集合recVset中进行归档,其中recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票,然后开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,然后再进行最后一次确认,判断是否又有更优的选票产生,若无,则终止投票,然后最终的选票,其流程如下
if (n.electionEpoch > logicalclock) { // 其选举周期⼤于逻辑时钟 // 重新赋值逻辑时钟 logicalclock = n.electionEpoch; // 清空所有接收到的所有选票 recvset.clear(); if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { // 进⾏PK,选出较优的服务器 // 更新选票 updateProposal(n.leader, n.zxid, n.peerEpoch); } else { // ⽆法选出较优的服务器 // 更新选票 updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } // 发送本服务器的内部选票消息 sendNotifications(); } else if (n.electionEpoch < logicalclock) { // 选举周期 ⼩于逻辑时钟,不做处理,直接忽略 if(LOG.isDebugEnabled()){ LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x" + Long.toHexString(n.electionEpoch) + ", logicalclock=0x" + Long.toHexString(logicalclock)); } break; } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { // PK,选出较优的服务器 // 更新选票 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)); } // recvset⽤于记录当前服务器在本轮次的Leader选举中收到的所有外 部投票 recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock, 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) { // 表示之前提议的Leader已经是最优的 // 设置服务器状态 self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState()); // 最终的选票 Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock, proposedEpoch); // 清空recvqueue队列的选票 leaveInstance(endVote); // 返回选票 return endVote; } }
若选票中的服务器状态为FOLLOWING或者LEADING时,其大致步骤会判断选举周期是否等于逻辑时钟,归档选票,是否已经完成了Leader选举,设置服务器状态,修改逻辑时钟等于选举周期,返回最终选票,其流程如下
if(n.electionEpoch == logicalclock){ // 与逻辑时钟相等 // 将该服务器和选票信息放⼊recvset中 recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); if(ooePredicate(recvset, outofelection, n)) { // 已 经完成了leader选举 // 设置本服务器的状态 self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState()); // 最终的选票 Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch); // 清空recvqueue队列的选票 leaveInstance(endVote); return endVote; } } /* * Before joining an established ensemble, verify * a majority is following the same leader. */ outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state)); if(ooePredicate(outofelection, outofelection, n)) { // 已经完成了leader选举 synchronized(this){ // 设置逻辑时钟 logicalclock = n.electionEpoch; // 设置状态 self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING: learningState()); } // 最终选票 Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch); // 清空recvqueue队列的选票 leaveInstance(endVote); // 返回选票 return endVote; }
- 外部投票的选举轮次大干内部投票。
zookeeper源码分析之集群模式服务端(上)
整体来说ZK集群模式运行时分为两个阶段一个是选举,—个是处理请求。首先先来分析下—个ZK集群从刚启动到对外提供服务这段时间发生了什么
执行流程概述
首先在ZK集群中,不管是什么类型的节点,刚刚启动时都是LOOKING状态然后发起选举寻找Leader,只有确定Leader以后才最终确定自己以Leader,Follower,Observer中哪种方式启动对外提供服务
ZK整个恢复过程分为三步∶
- 选取Leader,一个节点想要成为Leader首先它的epoch要大,已处理的事务要最多,如果有这两个条件都相同的多个节点则Serverld最大的节点成为Leader,之所以要这样是因为其他节点都会按照Leader节点的事务同步数据,如果Leader不是最新的就会造成数据的丢失。
- 数据同步,Leader选出来以后各个节点会以Leader为标准更新自己的事务。
- 提供服务,数据同步完成以后开始正式对外提供服务,接收客户端连接,处理读写请求。
源码分析
集群模式下启动所有的ZK节点启动入口都是QuorumPeerMain类的main方法。main方法加载配置文件以后,最终会调用到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节点的事务信息进行同步,最后开始对外提供服务,这里我们先来看下初始化选举的逻辑,即上面的startLeaderElection方法∶
synchronized public void startLeaderElection() {
try {
//所有节点启动的初始状态都是LOOKING,因此这⾥都会是创建⼀张投⾃⼰为Leader的票
if (getPeerState() == ServerState.LOOKING)
{
currentVote = new Vote(myid, getLastLoggedZxid(),
getCurrentEpoch());
}
} catch(IOException e) {
//异常处理
}
//初始化选举算法,electionType默认为3
this.electionAlg = createElectionAlgorithm(electionType);
}
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le = null;
switch (electionAlgorithm) {
case 1:
//忽略
case 2:
//忽略
case 3:
//electionAlgorithm默认是3,直接ᩳ到这⾥
qcm = createCnxnManager();
//监听选举事件的listener
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:
//忽略
}
return le;
}
初始化选举的地方一下开启两个线程,一个是Listener,一个是FastLeaderElection
接下来,回到QuorumPeer类中start方法的最后一行super.start(),QuorumPeer本身也是一个线程类,一起来看下它的run方法∶
public void run() {
try {
while (running) {
//根据当前节点的状态执⾏不同流程
switch (getPeerState()) {
case LOOKING:
try {
//寻找Leader节点
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
setPeerState(ServerState.LOOKING);
}
break;
case OBSERVING:
try {
//当前节点启动模式为Observer
setObserver(makeObserver(logFactory));
//与Leader节点进⾏数据同步
observer.observeLeader();
} catch (Exception e) {
} finally {
}
break;
case FOLLOWING:
try {
//当前节点启动模式为Follower
setFollower(makeFollower(logFactory));
//与Leader节点进⾏数据同步
follower.followLeader();
} catch (Exception e) {
} finally {
}
break;
case LEADING:
try {
//当前节点启动模式为Leader
setLeader(makeLeader(logFactory));
//发送⾃⼰成为Leader的通知
leader.lead();
setLeader(null);
} catch (Exception e) {
} finally {
}
break;
}
}
}
}
节点初始化的状态为LOOKING,因此启动时直接会调用lookForLeader方法发起Leader选举,一起看下∶
public Vote lookForLeader() throws InterruptedException {
try {
Map<Long, Vote> recvset = new HashMap<Long, Vote>();
Map<Long, Vote> outofelection = new HashMap<Long, Vote>();
//向所有投票节点发送⾃⼰的投票信息
sendNotifications();
while ((self.getPeerState() == ServerState.LOOKING) && (!stop))
{
//读取各个节点返回的投票信息
Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);
//超时重发
if(n == null)
{
//如果前⾯待发送的消息已经全部发送,则重新发送
if(manager.haveDelivered()){
sendNotifications();
} else {
//否则尝试与各个节点建⽴连接
manager.connectAll();
}
//退避算法修改下次等待时间
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval?
tmpTimeOut : maxNotificationInterval);
}
else if (validVoter(n.sid) && validVoter(n.leader))
{
switch (n.state) {
case LOOKING:
//如果节点的周期⼤于⾃⼰的
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()) {
//这⾥的break是跳出switch语ݙ,别跟环Ⴐ
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();
}
//把节点的投票信息记录下
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
//统计投票信息,判断当前选举是否可以结束,也就是收到的票数信息已 经⾜够确认Leader
if (termPredicate(recvset, new Vote(proposedLeader,
proposedZxid,
logicalclock.get(), proposedEpoch))) {
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)){
recvqueue.put(n);
break;
}
}
//如果没有多余的投票信息则可以结束本次选举周期
if (n == null) {
//根据serverId修改当前节点的类型
self.setPeerState((proposedLeader ==
self.getId()) ? ServerState.LEADING: learningState());
Vote endVote = new Vote(proposedLeader,
proposedZxid, proposedEpoch);
//清空接收消息队列
leaveInstance(endVote);
//返回最终的投票信息
return endVote;
}
}
break;
case OBSERVING:
//Observer节点不参与投票,忽略
break;
case FOLLOWING:
case LEADING:
//如果周期相同,说明当前节点参与了这次选举
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.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.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
//把Leader跟Follower的投票信息加⼊outofelection,确认下它们 的信息是否⼀致
outofelection.put(n.sid, new Vote(n.leader,
IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state));
if (termPredicate(outofelection, new Vote(n.leader,
IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state))
&& checkLeader(outofelection, n.leader,
IGNOREVALUE)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
}
Vote endVote = new Vote(n.leader, n.zxid, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
default:
break;
}
}
}
return null;
}
经过上面的发起投票,统计投票信息最终每个节点都会确认自己的身份,节点根据类型的不同会执行以下逻辑∶
- 如果是Leader节点,首先会想其他节点发送一条NEWLEADER信息,确认自己的身份,等到各个节
点的ACK消息以后开始正式对外提供服务,同时开启新的监听器,处理新节点加入的逻辑。 - 如果是Follower节点,首先向Leader节点发送一条FOLLOWERINFO信息,告诉Leader节点自己已处理的事务的最大Zxid,然后Leader节点会根据自己的最大Zxid与Follower节点进行同步,如果Follower节点落后的不多则会收到Leader的DIFF信息通过内存同步,如果Follower节点落后的很多则会收到SNAP通过快照同步,如果Follower节点的Zxid大于Leader节点则会收到TRUNC信息忽略多余的事务。
3.如果是Observer节点,则与Follower节点相同
zookeeper源码分析之集群模式服务端(下)
接下来看看当ZK集群选举成功,达到一个稳定状态时各个不同类型的节点在处理请求时各自的流程。
ZK集群的节点类型有三种,Leader,Follower,Observer。从大方向看,各个节点各自处理客户端发给自己的读请求,对于写请求则统一交给Leader节点处理,Leader会先把写请求包装成proposal发给目口眼参与投票的节点,即Folower节点,等到收到多数节点的ACK消息以后则提交该请求并日想Follower节点发送commit通知以及向Observer节点发送inform通知。
下面我们就一个一个的进行源码解读。
Leader节点
Leader节点所在类∶LeaderZooKeeperServer
所有的请求都是交给一个处理链进行的, 因此我们先来看看Leader节点的处理链路由哪些处理器构成. 如下∶
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor toBeAppliedProcessor = new
Leader.ToBeAppliedRequestProcessor(finalProcessor, getLeader());
commitProcessor = new CommitProcessor(toBeAppliedProcessor,
Long.toString(getServerId()), false, getZooKeeperServerListener());
//start表示是异步线程,需要重点关注其run⽅法
commitProcessor.start();
ProposalRequestProcessor proposalProcessor = new
ProposalRequestProcessor(this, commitProcessor);
//该⽅法重点关注,会启动内部的⼏个处理器
proposalProcessor.initialize();
prepRequestProcessor = new PrepRequestProcessor(this, proposalProcessor);
//start表示是异步线程,需要重点关注其run⽅法
prepRequestProcessor.start();
//第⼀个处理器
firstProcessor = new LeaderRequestProcessor(this, prepRequestProcessor);
setupContainerManager();
}
按上面的代码,Leader节点请求首先交给firstProcessor,即我们的LeaderRequestProcessor,所有的处理器核心方法都是processRequest,那就从这里入手吧∶
public void processRequest(Request request) throws RequestProcessorException {
//这⾥省略掉对session的校验逻辑...
//真正处理的请求
nextProcessor.processRequest(request);
}
LeaderRequestProcessor除了校验session的逻辑以外,就是把请求转交给下一个处理器进行处理,即PrepRequestProcessor处理器,与单机模式不同,PrepRequestProcessor紧接着会把请求转交给ProposalRequestProcessor进行处理,这个处理器比较复杂,首先看下构造方法∶
//传⼊的nextProcessor就是CommitProcessor
public ProposalRequestProcessor(LeaderZooKeeperServer zks, RequestProcessor
nextProcessor) {
this.zks = zks;
//把下⼀个处理器设置为CommitProcessor
this.nextProcessor = nextProcessor;
//⾃⼰内部⼜初始化了⼀个AckRequestProcessor处理器
AckRequestProcessor ackProcessor = new
AckRequestProcessor(zks.getLeader());
//同时初始化SyncRequestProcessor处理器
syncProcessor = new SyncRequestProcessor(zks, ackProcessor);
}
//顺带着⼀起看下SyncRequestProcessor的构造⽅法
public SyncRequestProcessor(ZooKeeperServer zks,
RequestProcessor nextProcessor) {
super("SyncThread:" + zks.getServerId(), zks
.getZooKeeperServerListener());
this.zks = zks;
//nextProcessor就是AckRequestProcessor处理器
this.nextProcessor = nextProcessor;
running = true;
}
这里主要关注点在于除了初始化的时候调用构造方法传入CommitProcessor作为它之后的处理器以外,它内部创建了AckRequestProcessor 跟SyncRequestProcessor两个处理器,并且把前者作为后者的nextPrOcessor。
搞清楚关系以后,先来看下ProposalRequestProcesSor的处理逻辑∶
public void processRequest(Request request) throws RequestProcessorException {
//如果请求是由ZK集群中其他节点发过来的,则由各⾃的handler处理
if (request instanceof LearnerSyncRequest){
zks.getLeader().processSync((LearnerSyncRequest)request);
} else {
//普通请求直接调⽤下⼀个处理器,这⾥的下⼀个处理器是CommitProcessor
//注意这⾥提交给CommitProcessor的既有读请求也有写请求
nextProcessor.processRequest(request);
//header不为null说明是写请求
if (request.getHdr() != null) {
try {
写请求发起proposal
zks.getLeader().propose(request);
} catch (XidRolloverException e) {
throw new RequestProcessorException(e.getMessage(), e);
}
//请求同步磁盘
syncProcessor.processRequest(request);
}
}
}
我们顺着思路来看,首先该处理内部直接把请求转交给了CommitProcessor,上代码∶
public void processRequest(Request request) {
if (stopped) {
return;
}
queuedRequests.add(request);
wakeup();
}
很清楚的看到该方法就是把请求放进队列就返回了,首先我们要注意的是这里提交给CommitProceSSor 处理的请求既有读请求也有写请求,如果是写请求,我们可以想到该处理器会等到多数派节点都响应以后再执行commit操作,而读请求的处理逻辑我们后面跟着代码一起看。
回到上面ProposalRequestProcessor的代码,对于写请求,它在提交给CommitProcessor之后马上就发起了propose,所以我们先来看下这段代码∶
public Proposal propose(Request request) throws XidRolloverException {
//zxid已经⽤完,触发⼀次重新选举
if ((request.zxid & 0xffffffffL) == 0xffffffffL) {
String msg = "zxid lower 32 bits have rolled over, forcing reelection, and therefore new epoch start";
//关闭当前服务
shutdown(msg);
throw new XidRolloverException(msg);
}
//封装发送的数据包
byte[] data = SerializeUtils.serializeRequest(request);
proposalStats.setLastProposalSize(data.length);
QuorumPacket pp = new QuorumPacket(Leader.PROPOSAL, request.zxid, data,
null);
Proposal p = new Proposal();
p.packet = pp; p.request = request;
synchronized(this) {
//简单起⻅,这⾥省略掉部分逻辑...
lastProposed = p.packet.getZxid();
//保存正在处理的Proposal到内存
outstandingProposals.put(lastProposed, p);
//发送数据包给各个节点
sendPacket(pp);
}
return p;
}
上面把请求发送给集群中其他节点以后就开始把请求交给SyncRequestProcessor处理了,在集群模式下,对应AckRequestProcessor,也就是说对于当前节点来说,当请求成功落盘以后,也要作为多数派中的一员响应ACK,那么接下来就来看下AckRequestProcessor的处理逻辑∶
public void processRequest(Request request) {
QuorumPeer self = leader.self;
if(self != null)
//ACK的参数是⽐较少的
leader.processAck(self.getId(), request.zxid, null);
else
LOG.error("Null QuorumPeer");
}
synchronized public void processAck(long sid, long zxid, SocketAddress
followerAddr) {
if (!allowedToCommit) return;
//如果是⼀个特殊的ACK信息(NEWLEADER)则忽略
if ((zxid & 0xffffffffL) == 0) {
return;
}
//Proposal会被加⼊这⾥
if (outstandingProposals.size() == 0) {
return;
}
//已经提交过,忽略
if (lastCommitted >= zxid) {
return;
}
Proposal p = outstandingProposals.get(zxid);
if (p == null) {
return;
}
//把响应节点的serverId加⼊到该Proposal中 p.addAck(sid);
//每次收到ACK都会尝试commit
boolean hasCommitted = tryToCommit(p, zxid, followerAddr);
//简单起⻅这⾥省略掉对于reconfig指令的处理
}
好了,接下来的重点应该是上面的tryToCommit方法了,一起看下∶
synchronized public boolean tryToCommit(Proposal p, long zxid, SocketAddress
followerAddr)
{
//保证按顺序commit
if (outstandingProposals.containsKey(zxid - 1)) return false;
//是否满⾜提交要求,即多数派达成⼀致
//由于上⾯是只要收到ACK就会调⽤该⽅法
//因此这⾥要判断是否响应过半,否则不执⾏真正的commit
if (!p.hasAllQuorums()) {
return false;
}
//从待处理的Proposal中删除
outstandingProposals.remove(zxid);
if (p.request != null) {
//加⼊到待执⾏列表中
toBeApplied.add(p);
}
if (p.request == null) {
//忽略
} else if (p.request.getHdr().getType() == OpCode.reconfig) {
//继续忽略reconfig指令
} else {
//发送commit通知给follower
commit(zxid);
//发送inform通知给observer
inform(p);
}
//本地先commit该事务
zk.commitProcessor.commit(p.request);
//如果正在等待的同步请求包含该请求,则执⾏发送同步消息
if(pendingSyncs.containsKey(zxid)){
for(LearnerSyncRequest r: pendingSyncs.remove(zxid)) {
sendSync(r);
}
}
return true;
}
上面执行commit的操作主要两个逻辑,一个是把该proposal加入到toBeApplied列表中,一个是调用commitProcessor的commit方法。结合上面的逻辑这里可以想到一开始ZK仅仅把请求放在commitProcessor队列中,明显是需要阻塞等待ACK的,而这里再调用commit应该是要执行真正的Commit逻辑了,一起来看下∶
public void commit(Request request) {
if (stopped || request == null) {
return;
}
//这⾥是把请求保存在commit队列中
committedRequests.add(request);
wakeup();
}
很清楚的看到该方法就是把请求放进队列就返回了,首先我们要注意的是这里提交给CommitProcessor 处理的请求既有读请求也有写请求,如果是写请求,我们可以想到该处理器会定到多数派节点都响应以后再执行commit操作,而读请求的处理逻辑我们后面跟着代码一起看。
到这里总结一下,最一开始执行的commitProcessor中的processRequest方法是把请求放在queuedRequests队列中,这里放的请求既有读也有写,而这里通过commit方法放在
CommittedRequests队列中的请求只有写。commitProcessor处理器是一个线程,下面重点看下它的run方法∶
public void run() {
try {
int requestsToProcess = 0;
boolean commitIsWaiting = false;
do {
//有待commit的请求(只有写)
commitIsWaiting = !committedRequests.isEmpty();
//有待处理的请求(读写都有)
requestsToProcess = queuedRequests.size();
//如果当前没有任何待处理的请求则阻塞等待
if (requestsToProcess == 0 && !commitIsWaiting){
synchronized (this) {
while (!stopped && requestsToProcess == 0 &&
!commitIsWaiting)
{
wait();
commitIsWaiting = !committedRequests.isEmpty();
requestsToProcess = queuedRequests.size();
}
}
}
//⾸先开始待处理的读写请求
Request request = null;
while (!stopped && requestsToProcess > 0 && (request =
queuedRequests.poll()) != null)
{
requestsToProcess--;
//这⾥的第⼀个条件是写请求⼀加⼊该客户端的队列,按照顺序执⾏
//这⾥的第⼆个条件是要保证如果⼀个客户端先发写请求再发读请求,那么需要读到写之后的值
if (needCommit(request) || pendingRequests.containsKey(request.sessionId))
{
LinkedList<Request> requests = pendingRequests.get(request.sessionId);
if (requests == null) {
requests = new LinkedList<Request>();
pendingRequests.put(request.sessionId, requests);
}
requests.addLast(request);
}
else {
//不是写请求且该客户端没有正在处理的请求则直接跳转下⼀个处理器
ToBeAppliedRequestProcessor
sendToNextProcessor(request);
}
//从上⾯可以看到只有写请求才会加⼊pendingRequests队列
//因此这⾥的意思就是如果有待处理的写请求且已经有已提交的写请求需要处理则跳 出该循环
if (!pendingRequests.isEmpty() &&
!committedRequests.isEmpty()){
commitIsWaiting = true;
break;
}
}
if (commitIsWaiting && !stopped){
waitForEmptyPool();
if (stopped){
return;
}
//处理已提交的请求
if ((request = committedRequests.poll()) == null) {
throw new IOException("Error: committed head is null");
}
LinkedList<Request> sessionQueue = pendingRequests.get(request.sessionId);
if (sessionQueue != null) {
Request topPending = sessionQueue.poll();
if (request.cxid != topPending.cxid) {
sessionQueue.addFirst(topPending);
} else {
topPending.setHdr(request.getHdr());
topPending.setTxn(request.getTxn());
topPending.zxid = request.zxid;
request = topPending;
}
}
//调⽤下⼀个处理器,即ToBeAppliedRequestProcessor
sendToNextProcessor(request);
waitForEmptyPool();
if (sessionQueue != null) {
//⼀旦处理完该客户端的⼀个写请求,则把它后⾯的读请求⼀次性处理完
//因为前⾯说了,为了保证同⼀客户端能读取到最新值,需要保证读在写之后
while (!stopped && !sessionQueue.isEmpty()
&& !needCommit(sessionQueue.peek())) {
sendToNextProcessor(sessionQueue.poll());
}
if (sessionQueue.isEmpty()) {
pendingRequests.remove(request.sessionId);
}
}
}
} while (!stoppedMainLoop);
} catch (Throwable e) {
handleException(this.getName(), e);
}
}
commit之后下一个处理器ToBeAppliedRequestProcessor逻辑很简单∶
public void processRequest(Request request) throws RequestProcessorException {
//next指向最后⼀个处理器
next.processRequest(request);
if (request.getHdr() != null) {
long zxid = request.getHdr().getZxid();
Iterator<Proposal> iter = leader.toBeApplied.iterator();
if (iter.hasNext()) {
Proposal p = iter.next();
if (p.request != null && p.request.zxid == zxid) {
iter.remove();
return;
}
}
LOG.error("Committed request not found on toBeApplied: " +request);
}
}
就是调用最终处理器FinalRequestProcessor以及把请求从toBeApplied列表中删除。 关于
FinalRequestProcessor同样在之前的单机版文章中已经分析过了就是真正的修改内存中的数据,保存commitLog以及返回客户端response。
Follower节点
首先接着上面的Leader节点逻辑说,Leader会把写请求包装成proposal发送给各个follower节点处理. 这个请求跟普通的连接到follower节点的客户端的请求不同,它是通过一个专门的端口来接受leader的消息的,这个在选举完成以后就建立了。 先来看下处理器链路的建立∶
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
//与Leader节点相⽐commitProcessor后⾯的处理器删除了
ToBeAppliedRequestProcessor,直接变为FinalRequestProcessor
commitProcessor = new CommitProcessor(finalProcessor,
Long.toString(getServerId()), true, getZooKeeperServerListener());
commitProcessor.start();
//第⼀个处理器改为FollowerRequestProcessor
firstProcessor = new FollowerRequestProcessor(this, commitProcessor);
((FollowerRequestProcessor) firstProcessor).start();
//SyncRequestProcessor之后跟着的是SendAckRequestProcessor,⽽不是
AckRequestProcessor syncProcessor = new SyncRequestProcessor(this, new
SendAckRequestProcessor((Learner)getFollower()));
syncProcessor.start();
}
Follower节点监听Leader节点数据包的逻辑在Follower类中,我们只以proposal跟commit为例说明,如下∶
protected void processPacket(QuorumPacket qp) throws Exception{
switch (qp.getType()) {
case Leader.PROPOSAL:
TxnHeader hdr = new TxnHeader();
Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);
lastQueued = hdr.getZxid();
//依旧忽略reconfig指令的处理...
//处理proposal请求
fzk.logRequest(hdr, txn);
break;
case Leader.COMMIT:
//处理commit请求
fzk.commit(qp.getZxid());
break;
}
}
从上面的代码片段看出,重点就是两个方法,logRequest跟commit,我们挨个来看下∶
public void logRequest(TxnHeader hdr, Record txn) {
Request request = new Request(hdr.getClientId(), hdr.getCxid(),
hdr.getType(), hdr, txn, hdr.getZxid());
if ((request.zxid & 0xffffffffL) != 0) {
//加⼊处理中事务队列
pendingTxns.add(request);
}
//请求落盘
syncProcessor.processRequest(request);
}
通过syncProcessor处理器落盘的逻辑我们就不用多说了,从上面Follower节点初始化处理链路的代码我们知道落盘以后会把请求交给SendAckRequestProcessor来处理,—起看下∶
public void processRequest(Request si) {
if(si.type != OpCode.sync){
//封装ACK包
QuorumPacket qp = new QuorumPacket(Leader.ACK, si.getHdr().getZxid(),
null, null);
try {
//发送给Leader
learner.writePacket(qp, false);
} catch (IOException e) {
//忽略
}
}
}
逻辑很简单,我们接着看commit方法∶
public void commit(long zxid) {
if (pendingTxns.size() == 0) {
return;
}
long firstElementZxid = pendingTxns.element().zxid;
if (firstElementZxid != zxid) {
System.exit(12);
}
Request request = pendingTxns.remove();
//交给commitProcessor进⾏处理
commitProcessor.commit(request);
}
逻辑很简单,这里就不多说了。看完了Follower处理Leader的逻辑,我们最后来看一下Follower处理连接它的客户端发来的请求的逻辑,这显然要回到FollowerRequestProcessor类中,如下∶
public void processRequest(Request request) {
if (!finished) {
//⾸先跟Leader节点⼀样,依旧是校验session的逻辑,这⾥忽略...
//这⾥是把请求加⼊到队列中
queuedRequests.add(request);
}
}
由于FollowerRequestProcessor是一个异步线程,那还是来看下它的run方法∶
public void run() {
try {
while (!finished) {
Request request = queuedRequests.take();
//如果是服务关闭的请求则退出环
if (request == Request.requestOfDeath) {
break;
}
//⽆论读写请求,直接交给下⼀个处理器,即CommitProcessor
nextProcessor.processRequest(request);
switch (request.type) {
case OpCode.sync:
//如果客户端发来的是同步请求,则加⼊pendingSyncs队列,并想leader发送
sync指令
zks.pendingSyncs.add(request);
zks.getFollower().request(request);
break;
case OpCode.create:
case OpCode.create2:
case OpCode.createTTL:
case OpCode.createContainer:
case OpCode.delete:
case OpCode.deleteContainer:
case OpCode.setData:
case OpCode.reconfig:
case OpCode.setACL:
case OpCode.multi:
case OpCode.check:
//这⾥是写请求转发给leader进⾏处理
zks.getFollower().request(request);
break;
case OpCode.createSession:
case OpCode.closeSession:
// Don't forward local sessions to the leader.
//如果是session操作且不是本地session 则转发给Leader
if (!request.isLocalSession()) {
zks.getFollower().request(request);
}
break;
}
}
} catch (Exception e) {
handleException(this.getName(), e);
}
}
可以看到,对于Follower节点来说,连接它的客户端发来的请求,如果是读请求则直接进行处理,如果是写请求则转发给Leader节点处理,Leader节点又会按照上面的流程,先发给Follower节点proposal 请求,再发commit请求。
Observer节点
基于上面的Leader节点跟Follower节点的分析,Observer节点应该是最简单的,对于Leader节点发来的请求它不参与投票,直接给一个等Leader的通知消息处理就好;对于连接它自己的客户端请求跟Follower是一样,一起看下。
首先是Observer节点处理链路的构建∶
protected void setupRequestProcessors() {
//FinalRequestProcessor不变
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
//CommitProcessor不变
commitProcessor = new CommitProcessor(finalProcessor,
Long.toString(getServerId()), true, getZooKeeperServerListener());
commitProcessor.start();
//第⼀个处理器变为ObserverRequestProcessor
firstProcessor = new ObserverRequestProcessor(this, commitProcessor);
((ObserverRequestProcessor) firstProcessor).start();
}
跟Follower节点一样,我们先来看下它跟Leader节点的交互逻辑∶
protected void processPacket(QuorumPacket qp) throws Exception{
switch (qp.getType()) {
case Leader.PROPOSAL:
LOG.warn("Ignoring proposal");
break;
case Leader.COMMIT:
LOG.warn("Ignoring commit");
break;
case Leader.INFORM:
TxnHeader hdr = new TxnHeader();
Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);
Request request = new Request (hdr.getClientId(), hdr.getCxid(),
hdr.getType(), hdr, txn, 0);
ObserverZooKeeperServer obs = (ObserverZooKeeperServer)zk;
//直接提交请求
obs.commitRequest(request);
break;
}
}
可以看到它忽略了PROPOSAL跟COMMIT数据包,而当收到INFORM数据包以后直接进行commit,一起看下∶
public void commitRequest(Request request) {
//直接提交
commitProcessor.commit(request);
}
最后来看下它处理客户端请求的逻辑,即ObserverRequestProcessor,如下∶
public void processRequest(Request request) {
if (!finished) {
//同样忽略校验session的逻辑
//把请求放队列
queuedRequests.add(request);
}
}
//核⼼逻辑依旧是在run⽅法中
public void run() {
try {
while (!finished) {
Request request = queuedRequests.take();
//如果服务关闭则退出环
if (request == Request.requestOfDeath) {
break;
}
//交给commitProcessor处理
nextProcessor.processRequest(request);
switch (request.type) {
case OpCode.sync:
zks.pendingSyncs.add(request);
zks.getObserver().request(request);
break;
case OpCode.create:
case OpCode.create2:
case OpCode.createTTL:
case OpCode.createContainer:
case OpCode.delete:
case OpCode.deleteContainer:
case OpCode.setData:
case OpCode.reconfig:
case OpCode.setACL:
case OpCode.multi:
case OpCode.check:
//写请求转发给Leader
zks.getObserver().request(request);
break;
case OpCode.createSession:
case OpCode.closeSession:
//⾮本地会话转发给leader处理
if (!request.isLocalSession()) {
zks.getObserver().request(request);
}
break;
}
}
} catch (Exception e) {
handleException(this.getName(), e);
}
}