目录
zookeeper的由来
- 各个节点的数据一致性
- 怎么保证任务只在一个节点上执行
- 如果orderservice1挂了,其他节点如何发现并接替任务
- 存在共享资源,安全性、互斥性
zookeeper的发展史
从上面案例可以看出,分布式系统的很多难题,都是由于缺少协调机制造成的。在分布式协调服务这块做的比较好的,由Google的Chubby以及Apache的Zookeeper。
Google Chubby是一个分布式锁服务,通过GoogleChubby来解决分布式写作、Master选举等与分布式锁服务相关的问题。
Zookeeper也是类似,因为当时在雅虎内部的很多系统都需要以来一个系统来进行分布式协调,但是谷歌的Chubby是不开源的,所以后来雅虎基于Chubby的思想开发了zookeeper,并捐赠给了Apache。
在上面这个架构下zookeeper以后,可以用来解决task执行问题,各个服务器先去zookeeper上注册节点,然后获得权限以后再访问task
zookeeper的设计思想
zookeeper主要是解决分布式环境下的服务协调问题而产生的,如果我们要去实现一个zookeeper这样的中间件,我们需要做什么?
1、防止单点故障
如果要防止单点故障zookeeper这个中间件的单点故障,那就势必要做集群。而且这个集群如果要满足高性能要求的化,还要是一个高性能高可用的集群。高性能意味着这个集群能够分担客户端的请求量,高可用意味着集群中的某一个节点宕机以后,不影响整个集群的数据和继续提供服务的可能性。
结论:所以这个中间件需要考虑到集群,而且这个集群还需要分担客户端的请求流量。
2、接着上面那个结论再来思考,如果要满足这样的一个高性能集群,我们最直观的想法应该是,每个节点都能接收到请求,并且每个节点的数据都必须要保持一致。要实现各个节点的数据一致性,就势必要一个leader节点负责协调和数据同步操作。如果在这样一个集群中没有leader节点,每个节点都可以接收到所有请求,那么这个集群的数据同步的复杂度是非常高的。
结论:所以这个集群中设计到数据同步以及会存在leader节点
3、如何在这些节点中选举出leader节点,以及leader挂了以后,如何恢复呢?
结论:所以zookeeper用了基于paxos理论所衍生出来的ZAB协议
4、leader节点如何和其他节点保证数据一致性,并且要求是强一致的。在分布式系统中,每一个机器节点虽然都能够明确知道自己进行的事务操作过程是成功和失败,但是却无法直接获取其他分布式节点的操作结果。所以当一个事务操作涉及到跨节点的时候,就需要用到分布式事务,分布式事务的数据一致性协议有2PC协议和3PC协议。
基于这些猜想,我们基本上知道zookeeper为什么要用到zab理论来做选举、为什么要做集群、为什么要用到分布式事务来实现数据一致性了。接下来我们逐步去剖析zookeeper里面的这些内容
关于2PC提交
(Two Phase Commitment Protocol)当一个事务操作需要跨越多个分布式节点的时候,为了保证事务处理的ACID特性,就需要引入一个”协调者“(TM)来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为AP。TM负责调度AP的行为,并最终决定这些AP是否要把事务真正进行提交;因为整个事务是分为两个阶段提交,所以叫2PC
阶段一:提交事务请求(投票)
事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各个参与者的相应。
执行事务
各个参与者节点执行事务操作,并将Undo和Redo信息记录到事务日志中,尽量把提交过程中所有消耗时间的操作和准备都提前完成确保后面100%成功提交事务
各个参与者向协调者反馈事务询问的相应
如果各个参与者成功执行了事务操作,那么就反馈给参与者yes的响应,表示事务可以执行;如果参与者没有成功执行事务,就反馈给协调者no的响应,表示事务不可以执行,上面这个阶段有点类似协调者组织各个参与者对一次事务操作的投票表态过程,因此2PC协议的第一个阶段称为”投票阶段“,即各参与者投票表名是否需要继续执行接下去的事务提交此操作。
阶段二:执行事务提交
在这个阶段,协调者会根据各参与者的反馈情况来决定最终是否可以进行事务提交操作,正常情况下包含两种可能性:执行事务、中断事务
zookeeper的集群
在zookeeper中,客户端会随机连接到zookeeper集群中的一个节点,如果是读请求,就直接从当前节点中读取数据,如果是写请求,那么请求会被转发给leader提交事务,然后leader会广播事务,只要超过半数节点写入成功,那么写提交就会被提交(类2PC事务)
所有事务请求必须由一个全局唯一的服务器来协调处理,这个服务器就是Leader服务器,其他的服务器就是follower。leader服务器把客户端的失去请求转化为一个事务Proposal(提议),并把这个Proposal分发给集群中的所有Follower服务器。之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈,那么Leader就会再次向所有Follower服务器发送Commit消息,要求各个follower节点对前面的一个Proposal进行提交;
集群角色
Leader角色
Leader服务器是整个zookeeper集群的核心,主要的工作任务有两项
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
- 集群内部各服务器的调度者
Follower角色
Follower角色的主要职责是
- 处理客户端非事务请求、转发事务请求给leader服务器
- 参与事务请求Proposal的投票(需要半数以上服务器通过才能通过leader commit数据;Leader发起的提案,要求Follower投票)
- 参与Leader选举的投票
Observer
Observer是zookeeper3.3开始引入的一个全新的服务器角色,从字面来理解,该角色充当了观察者的角色。
观察zookeeper集群中的最新状态变化并将这些状态变化同步到observer服务器上。Observer的工作原理与follower角色基本一致,而它和follower角色唯一的不同在于observer不参与任何形式的投票,包括事务请求Proposal的投票和leader选举的投票。简单来说,observer服务器只提供非实物请求服务,通常在于不影响集群事务处理能力的前提下提升集群非实物处理的能力
集群组成
通常zookeeper是由2n+1台server组成,每个server都知道彼此的存在。对于2n+1台server,只要有n+1台(大多数)server可用,整个系统保持可用。我们已经了解到,一个zookeeper集群如果要对外提供可用的服务,那么集群中必须要有过半的机器正常工作并且彼此之间能够正常通信,基于这个特性,如果搭建一个能够允许F台机器down掉的集群,那么就要部署2F+1台服务器组成的zookeeper集群。因此3台机器组成的zookeeper集群,能够在挂掉一台机器后依然正常工作。一个5台机器集群的服务器,能够对2台机器挂掉的情况下进行容灾。如果一台由6台服务构成的集群,同样只能挂掉两台机器。因此5台和6台在容灾能力上没有明显优势,反而增加了网络通信负担。系统启动时,集群中的server会选举出一台server为leader,其他的就作为follower(此处暂时不考虑observer角色)。
之所以要满足这样一个等式,是因为一个节点要称为集群中的leader,需要有超过集群过半数的节点支持,这个涉及到leader选举算法。同时也涉及到事务请求的提交投票。
ZAB协议
ZAB(Zookeeper Atomic Broadcast)协议是为分布式协调服务zookeeper专门设计的一种支持崩溃恢复的原子广播协议。在Zookeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,Zookeeper实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性
zab协议介绍
ZAB协议包含两种基本模式,分别是
- 崩溃恢复
- 原子广播
当整个集群在启动时,或者当leader节点出现网络中断、崩溃等情况时,ZAB协议就会进入恢复模式并选举产生新的Leader,当Leader服务器选举出来后,并且集群中有过半的机器和该leader节点完成数据同步后(同步指定的是数据同步,用来保证集群中过半的机器能够和leader服务器的数据状态保持一致),ZAB协议就会退出恢复模式。
当集群中已经有过半的Follower节点完成了Leader状态同步以后,那么整个集群就进入了消息广播模式。这个时候,在Leader节点正常工作时,启动一台新的服务器加入到集群,那这个服务器会直接进入数据恢复模式,和leader节点进行数据同步。同步完成后即可正常对外提供非事务请求的处理。
消息广播的实现原理
如果大家了解分布式事务的2PC和3PC协议的话,消息广播的过程实际上是一个简化版本的二阶段提交过程
- leader接收到消息请求后,将消息赋予一个全局唯一大的64位自增id:zxid,通过zxid的大小比较既可以实现因果有序这个特征
- leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现了全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的follower
- 当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack
- 当leader接收到合法数量(超过半数节点)的ACK后,leader就会向这些follower发送commit命令,同时会在本地执行该消息
- 当follower收到消息的commit命令以后,会提交该消息
leader的投票过程,不需要Observer的ack,也就是Observer不需要参与投票过程,但是Observer必须要同步Leader的数据从而在处理请求的时候保证数据的一致性
崩溃恢复(数据恢复)
ZAB协议的这个基于原子广播协议的消息广播过程,在正常情况下是没有任何问题的,但是一旦Leader节点崩溃,或者由于网络问题导致Leader服务器失去了过半的Follower节点的联系(leader失去与过半follower节点联系,可能是leader节点和follower节点之间产生了网络分区,那么此时的leader不再是合法的leader了),那么就会进入到崩溃恢复模式。在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的leader
为了使leader挂了后系统能正常工作,需要解决以下两个问题:
- 已经被处理的消息不能丢失
当leader收到合法数量follower的ACKs后,就向各个follower广播commit命令,同时也会在本地执行commit并向连接的客户端返回【成功】。但是如果在各个follower在收到commit命令前leader就挂了,导致剩下的服务器并没有执行到这条消息。
leader对事务消息发起commit操作,但是该消息在follower1上执行了,但是follower2还没有收到commit,就已经挂了,而实际上客户端已经收到该事务消息处理成功的回执了。所以在zab协议下需要保证所有机器都要执行这个事务消息
- 被丢弃的消息不能再次出现
当leader接收到消息请求生成proposal后就挂了,其他follower并没有收到此proposal,因此经过恢复模式重新选了leader后,这条消息是被跳过的。此时,之前挂了的leader重新启动并注册成了follower,他保留了被跳过消息的proposal状态,与整个系统的状态是不一致的,需要将其删除。
ZAB协议需要满足上面两种情况,就必须要设计一个leader选举算法:能够确保已经被leader提交的事务proposal能够提交、同时丢弃已经被跳过的事务proposal。
针对这个要求
- 如果leader选举算法能够保证新选举出来的leader服务器拥有集群中所有机器最高编号(ZXID最大)的事务proposal,那么就可以保证这个新选举出来的Leader一定具有已经提交的提案。因为所有提案被commit之前必须有超过半数的follower ACK,即必须有超过半数节点的服务器的事务日志上有该提案的proposal,因此,只要有合法数量的节点正常工作,就必然有一个节点保存了所有被commit消息的proposal状态
- zxid是64位,高32位是epoch编号,每经过依次leader选举产生一个新的leader,新的leader会将epoch号+1,低32位是消息计数器,每接收到一条消息这个值+1,新的leader选举后这个值重置为0。这样设计的好处在于老的leader挂了以后重启,它不会被选举为leader,因此此时它的zxid肯定小于当前新的leader。当老的leader作为follower接入新的leader后,新的leader会让它将所有的拥有旧的epoch号的未被commit的proposal清除
关于ZXID
zxid,也就是事务id,为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上zxid。实现中zxid是一个64位的数字,它高32位epoch(ZAB协议通过epoch编号来区分Leader周期变化的策略)用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch=(原来的epoch+1),标识当前属于那个leader的统治时期。低32位用于递增技术。
epoch:可以理解为当前集群所处的年代或者周期,每个leader就像皇帝,都有自己的年号,所以每次改成朝换代,leader变更之后,都会在前一个年代的基础上加1 。这样就算旧的leader崩溃恢复之后,也没有人听他的了,因为follower只听从当前年代的leader的命令。
epoch的变化大家可以做一个简单的实验:
- 启动一个zookeeper集群。
- 在/tmp/zookeeper/VERSION-2路径下会看到一个currentEpoch文件。文件中显示的是当前的epoch
- 把leader节点停机,这个时候在看currentEpoch会有变化。随着每次选举新的leader,epoch都会发生变化
leader选举
leader选举会分两个过程
启动的时候的leader选举、leader崩溃的时候的选举
服务器启动时的leader选举
每个节点启动的时候状态都是LOOKING,处于观望状态,接下来就开始进行选leader流程
进行leader选举,至少需要两台机器,我们选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,它本身是无法进行和完成leader选举,当第二胎服务器Server2启动时,这个时候两台机器可以相互通信,每台机器都试图找到leader,于是进入leader选举过程。选举过程如下:
- 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID、epoch,使用(myid,ZXID,epoch)来表示,此时Server1的投票为(1,0),Server2的投票为(2,0),然后各自将这个投票发给集群中其他机器。
- 接收来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票(epoch)、是否来自LOOKING状态的服务器。
- 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
- 优先检查ZXID。ZXID比较大的服务器优先作为Leader
- 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
- 对于Server1而言,他的投票是(1,0),接收Server2的投票为(2,0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myID最大,于是更新自己的投票为(2,0),然后从新投票,对于Server2而言,它不需要更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
- 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,判断是否已经有过半机器接收到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接收了(2,0)的投票信息,此时便认为已经选出了Leader。
- 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。
运行过程中的leader选举
当集群中的leader服务器出现宕机或者不可用的情况时,那么整个集群将无法对外提供服务,而是进入新一轮的Leader选举,服务器运行期间的Leader选举和启动时期的Leader选举基本过程是一致的。
- 变更状态。Leader挂后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
- 每个Server会发出一个投票。在运行期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1,123),(3,122),然后各自将投票发送给集群中所有机器。接收来自各个服务器的投票。与启动时过程相同。
- 处理投票。与启动时过程相同,此时,Server1将会成为Leader。
- 统计投票。与启动时过程相同。
- 改变服务器的状态。与启动时过程相同
Leader选举源码分析
有了理论基础以后,我们先带大家读一下源码,看看他的实现逻辑。然后我们再通过图形的方式帮助大家理解
从入口函数QuorumPeerMain.java开始
public static void main(String[] args) {
QuorumPeerMain main = new QuorumPeerMain();
try {
//入口方法,初始化zookeeper并启动
main.initializeAndRun(args);
} catch... //为方便观看,省略异常捕获代码块
LOG.info("Exiting normally");
System.exit(0);
}
protected void initializeAndRun(String[] args)
throws ConfigException, IOException
{
QuorumPeerConfig config = new QuorumPeerConfig();
if (args.length == 1) {
config.parse(args[0]);
}
//开始并计划清除任务
DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config
.getDataDir(), config.getDataLogDir(), config
.getSnapRetainCount(), config.getPurgeInterval());
purgeMgr.start();
//判断是standalone模式还是集群模式
if (args.length == 1 && config.servers.size() > 0) {
runFromConfig(config);
} else {
LOG.warn("Either no config or no quorum defined in config, running "
+ " in standalone mode");
// there is only server in the quorum -- run as standalone
ZooKeeperServerMain.main(args);
}
}
public void runFromConfig(QuorumPeerConfig config) throws IOException {
try {
ManagedUtil.registerLog4jMBeans();
} catch (JMException e) {
LOG.warn("Unable to register log4j JMX control", e);
}
LOG.info("Starting quorum peer");
try {
//为客户端提供读写的server,也就是2181这个端口的访问功能
ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory();
cnxnFactory.configure(config.getClientPortAddress(),
config.getMaxClientCnxns());
//ZK的逻辑主线程,负责选举、投票逻辑
quorumPeer = getQuorumPeer();
//设置各种参数
quorumPeer.setQuorumPeers(config.getServers());
quorumPeer.setTxnFactory(new FileTxnSnapLog(
new File(config.getDataLogDir()),
new File(config.getDataDir())));
quorumPeer.setElectionType(config.getElectionAlg());
quorumPeer.setMyid(config.getServerId());
quorumPeer.setTickTime(config.getTickTime());
quorumPeer.setInitLimit(config.getInitLimit());
quorumPeer.setSyncLimit(config.getSyncLimit());
quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());
quorumPeer.setCnxnFactory(cnxnFactory);
quorumPeer.setQuorumVerifier(config.getQuorumVerifier());
quorumPeer.setClientPortAddress(config.getClientPortAddress());
quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());
quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());
quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));
quorumPeer.setLearnerType(config.getPeerType());
quorumPeer.setSyncEnabled(config.getSyncEnabled());
// sets quorum sasl authentication configurations
quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);
if(quorumPeer.isQuorumSaslAuthEnabled()){
quorumPeer.setQuorumServerSaslRequired(config.quorumServerRequireSasl);
quorumPeer.setQuorumLearnerSaslRequired(config.quorumLearnerRequireSasl);
quorumPeer.setQuorumServicePrincipal(config.quorumServicePrincipal);
quorumPeer.setQuorumServerLoginContext(config.quorumServerLoginContext);
quorumPeer.setQuorumLearnerLoginContext(config.quorumLearnerLoginContext);
}
quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);
quorumPeer.initialize();
//启动主线程,QuorumPeer重写了Thread().start方法
quorumPeer.start();
quorumPeer.join();
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Quorum Peer interrupted", e);
}
}
启动主线程,QuorumPeer.java重写了Thread.start方法
@Override
public synchronized void start() {
//恢复DB
loadDataBase();
cnxnFactory.start();
//Leader选举初始化
startLeaderElection();
super.start();
}
loadDataBase();主要是从本地文件中恢复数据,以及获取最新的zxid
private void loadDataBase() {
File updating = new File(getTxnFactory().getSnapDir(),
UPDATING_EPOCH_FILENAME);
try {
//从本地文件恢复DB
zkDb.loadDataBase();
// load the epochs
//从最新的zxid恢复epoch变量,zxid64位,前32位是epoch的值,低32位是zxid
long lastProcessedZxid = zkDb.getDataTree().lastProcessedZxid;
long epochOfZxid = ZxidUtils.getEpochFromZxid(lastProcessedZxid);
try {
//从文件中读取当前的epoch
currentEpoch = readLongFromFile(CURRENT_EPOCH_FILENAME);
if (epochOfZxid > currentEpoch && updating.exists()) {
LOG.info("{} found. The server was terminated after " +
"taking a snapshot but before updating current " +
"epoch. Setting current epoch to {}.",
UPDATING_EPOCH_FILENAME, epochOfZxid);
setCurrentEpoch(epochOfZxid);
if (!updating.delete()) {
throw new IOException("Failed to delete " +
updating.toString());
}
}
} catch(FileNotFoundException e) {
// pick a reasonable epoch number
// this should only happen once when moving to a
// new code version
currentEpoch = epochOfZxid;
LOG.info(CURRENT_EPOCH_FILENAME
+ " not found! Creating with a reasonable default of {}. This should only happen when you are upgrading your installation",
currentEpoch);
writeLongToFile(CURRENT_EPOCH_FILENAME, currentEpoch);
}
if (epochOfZxid > currentEpoch) {
throw new IOException("The current epoch, " + ZxidUtils.zxidToString(currentEpoch) + ", is older than the last zxid, " + lastProcessedZxid);
}
try {
acceptedEpoch = readLongFromFile(ACCEPTED_EPOCH_FILENAME);
} catch(FileNotFoundException e) {
// pick a reasonable epoch number
// this should only happen once when moving to a
// new code version
acceptedEpoch = epochOfZxid;
LOG.info(ACCEPTED_EPOCH_FILENAME
+ " not found! Creating with a reasonable default of {}. This should only happen when you are upgrading your installation",
acceptedEpoch);
writeLongToFile(ACCEPTED_EPOCH_FILENAME, acceptedEpoch);
}
if (acceptedEpoch < currentEpoch) {
throw new IOException("The accepted epoch, " + ZxidUtils.zxidToString(acceptedEpoch) + " is less than the current epoch, " + ZxidUtils.zxidToString(currentEpoch));
}
} catch(IOException ie) {
LOG.error("Unable to load database on disk", ie);
throw new RuntimeException("Unable to run quorum server ", ie);
}
}
初始化Leader选举,startLeaderElection();
synchronized public void startLeaderElection() {
try {
//如果当前节点的状态是LOOKING,则投票给自己
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
for (QuorumServer p : getView().values()) {
if (p.id == myid) {
myQuorumAddr = p.addr;
break;
}
}
if (myQuorumAddr == null) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
//根据配置获取选举方式
this.electionAlg = createElectionAlgorithm(electionType);
}
createElectionAlgorithm(electionType);,通过选举类型,创建选举算法,选举算法有3种,可以通过在zoo.cfg中进行配置,默认是fast选举
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
//TODO: use a factory rather than a switch
switch (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:
//Leader选举IO负责类
//显然前三种已经被zookeeper废弃了。
qcm = createCnxnManager();
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
//启动已绑定端口的选举线程,等待集群中其他机器连接
listener.start();
//基于TCP的选举算法
le = new FastLeaderElection(this, qcm);
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
继续看FastLeaderElection的初始化动作,主要初始化了业务层的发送队列和接收队列
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){
this.stop = false;
this.manager = manager;
starter(self, manager);
}
private void starter(QuorumPeer self, QuorumCnxManager manager) {
this.self = self;
proposedLeader = -1;
proposedZxid = -1;
//业务层发送队列,业务对象ToSend
sendqueue = new LinkedBlockingQueue<ToSend>();
//业务层接收队列,业务对象Notification
recvqueue = new LinkedBlockingQueue<Notification>();
this.messenger = new Messenger(manager);
}
Messenger(manager);方法对WorkerSender,WorkerReceiver这两个内部类进行初始化并启动,两个类分别负责发送投票信息、接收投票信息
Messenger(QuorumCnxManager manager) {
this.ws = new WorkerSender(manager);
Thread t = new Thread(this.ws,
"WorkerSender[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
this.wr = new WorkerReceiver(manager);
t = new Thread(this.wr,
"WorkerReceiver[myid=" + self.getId() + "]");
t.setDaemon(true);
t.start();
}
然后回到QuorumPeer.java。FastLeaderElection初始化完成后,调用super.start(),最终运行QuorumPeer的run方法
@Override
public synchronized void start() {
//恢复DB
loadDataBase();
cnxnFactory.start();
//Leader选举初始化
startLeaderElection();
super.start();
}
线程启动了,那么线程的主流程是什么样的呢?接下来我们看QuorumPeer.java类重写的run()方法,前部分主要时做JMX监控注册,后半部分时核心。
public void run() {
setName("QuorumPeer" + "[myid=" + getId() + "]" +
cnxnFactory.getLocalAddress());
LOG.debug("Starting quorum peer");
try {
//此处通过JMX来监控一些属性
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 {
p = new RemotePeerBean(s);
try {
MBeanRegistry.getInstance().register(p, jmxQuorumBean);
} 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循环,核心
*/
while (running) {
//判断当前节点的状态
switch (getPeerState()) {
//如果是LOOKING,则进入选举流程
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 immediately
final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(
logFactory, this,
new ZooKeeperServer.BasicDataTreeBuilder(),
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 secs
sleep(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();
setBCVote(null);
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 {
setBCVote(null);
//此处通过策略模式来决定当前用那个选举算法来进行leader选举
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
//如果收到的选票状态不是LOOKING,比如这台机器刚加入一个已经正在运行的zk集群时
//OBSERVING机器不参与选举
case OBSERVING:
try {
LOG.info("OBSERVING");
setObserver(makeObserver(logFactory));
observer.observeLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e );
} finally {
observer.shutdown();
setObserver(null);
setPeerState(ServerState.LOOKING);
}
break;
case FOLLOWING:
try {
LOG.info("FOLLOWING");
setFollower(makeFollower(logFactory));
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
follower.shutdown();
setFollower(null);
setPeerState(ServerState.LOOKING);
}
break;
case LEADING:
LOG.info("LEADING");
try {
setLeader(makeLeader(logFactory));
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
setPeerState(ServerState.LOOKING);
}
break;
}
}
} finally {
LOG.warn("QuorumPeer main thread exited");
try {
MBeanRegistry.getInstance().unregisterAll();
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
jmxQuorumBean = null;
jmxLocalPeerBean = null;
}
}
上面贴上了核心判断的第一个选择LOOKING状态;调用setCurrentVote(makeLEStrategy().lookForLeader());,最总根据策略应该运行FastLeaderElection中的选举算法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 = Time.currentElapsedTime();
}
try {
//收到的投票
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
//存储选举结果
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
synchronized(this){
//增加逻辑始终
logicalclock.incrementAndGet();
//刷新自己的zxid和epoch
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
* 主循环,直到选举出Leader
*/
while ((self.getPeerState() == ServerState.LOOKING) &&
(!stop)){
/*
* Remove next notification from queue, times out after 2 times
* the termination time
* 从IO线程里拿到投票消息,自己的投票也在这里处理
*/
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
/*
* Sends more notifications if haven't received enough.
* Otherwise processes new notification.
*/
if(n == null){
//如果空闲的情况下,消息发完了,继续发送,一直到选出leader为止
if(manager.haveDelivered()){
sendNotifications();
} else {
//消息还没投递出去,可能时其他server还没启动,尝试再连接
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
* voting view for a replica in the voting view.
*/
//判断收到消息的节点状态
switch (n.state) {
case LOOKING:
// If notification > current, replace and send messages out
//判断接收到的节点epoch大于logicalclock,则表示当前时新一轮的选举
if (n.electionEpoch > logicalclock.get()) {
//更新本地的logicalclock
logicalclock.set(n.electionEpoch);
//清空接收队列
recvset.clear();
//检查收到的这个消息是否可以胜出,依次比较epoch,zxid,myid
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
//胜出以后,把投票更改为对方的票据
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {//否则票据不变更
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
//继续广播消息,让其他节点知道我现在的票据
sendNotifications();
//如果收到的消息epoch小于当前节点的epoch,则忽略这条消息
} 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;
//如果epoch相同的化,就继续比较zxid、myid,如果胜出,则更新自己的票据,并且发出广播
} 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.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
//判断选举是否结束,默认算法时超过半数server同意
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch))) {
// Verify if there is any change in the proposed leader
//一直等新的notification到达,知道超时
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
*/
//确定leader
if (n == null) {
//修改状态,LEADING or FOLLOWING
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
//返回最终投票结果
Vote endVote = new Vote(proposedLeader,
proposedZxid,
logicalclock.get(),
proposedEpoch);
leaveInstance(endVote);
return endVote;
}
}
break;
case OBSERVING:
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(ooePredicate(recvset, outofelection, n)) {
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.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 unrecognized: {} (n.state), {} (n.sid)",
n.state, 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());
}
}
方法简直长。。。
sendNotifications();一起看看消息是如何广播的
private void sendNotifications() {
//循环发送
for (QuorumServer server : self.getVotingView().values()) {
long sid = server.id;
//消息实体
ToSend notmsg = new ToSend(ToSend.mType.notification,
proposedLeader,
proposedZxid,
logicalclock.get(),
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.get()) +
" (n.round), " + sid + " (recipient), " + self.getId() +
" (myid), 0x" + Long.toHexString(proposedEpoch) + " (n.peerEpoch)");
}
//添加到发送队列,这个队列会被workerSender消费
sendqueue.offer(notmsg);
}
}
一起来看FastLeaderElection类的WorkerSender内部类,是如何消费广播的
class WorkerSender extends ZooKeeperThread {
volatile boolean stop;
QuorumCnxManager manager;
WorkerSender(QuorumCnxManager manager){
super("WorkerSender");
this.stop = false;
this.manager = manager;
}
public void run() {
while (!stop) {
try {
//从发送队列中获取消息实体
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);
}
}
public void toSend(Long sid, ByteBuffer b) {
/*
* If sending message to myself, then simply enqueue it (loopback).
*/
//如果是自己,就不走网络发送,直接添加到本地接收队列
if (this.mySid == sid) {
b.position(0);
addToRecvQueue(new Message(b.duplicate(), sid));
/*
* Otherwise send to the corresponding thread to send.
*/
} else {
/*
* Start a new connection if doesn't have one already.
*/
//发送给别的节点,半段之前是不是发送过
ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
//这个SEND_CAPACITY的大小是1,所以如果之前已经有一个还在等待发送,则会把之前的一个删除掉,发送新的
ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
if (bqExisting != null) {
addToSendQueue(bqExisting, b);
} else {
addToSendQueue(bq, b);
}
//这里是真正的发送逻辑
connectOne(sid);
}
}
可以看到真正的发送逻辑是通过socket进行通信的
synchronized public void connectOne(long sid){
if (!connectedToPeer(sid)){
InetSocketAddress electionAddr;
if (view.containsKey(sid)) {
electionAddr = view.get(sid).electionAddr;
} else {
LOG.warn("Invalid server id: " + sid);
return;
}
try {
LOG.debug("Opening channel to server " + sid);
Socket sock = new Socket();
setSockOpts(sock);
sock.connect(view.get(sid).electionAddr, cnxTO);
LOG.debug("Connected to server " + sid);
// Sends connection request asynchronously if the quorum
// sasl authentication is enabled. This is required because
// sasl server authentication process may take few seconds to
// finish, this may delay next peer connection requests.
if (quorumSaslAuthEnabled) {
initiateConnectionAsync(sock, sid);
} else {
initiateConnection(sock, sid);
}
} catch (UnresolvedAddressException e) {
// Sun doesn't include the address that causes this
// exception to be thrown, also UAE cannot be wrapped cleanly
// so we log the exception in order to capture this critical
// detail.
LOG.warn("Cannot open channel to " + sid
+ " at election address " + electionAddr, e);
// Resolve hostname for this server in case the
// underlying ip address has changed.
if (view.containsKey(sid)) {
view.get(sid).recreateSocketAddresses();
}
throw e;
} catch (IOException e) {
LOG.warn("Cannot open channel to " + sid
+ " at election address " + electionAddr,
e);
// We can't really tell if the server is actually down or it failed
// to connect to the server because the underlying IP address
// changed. Resolve the hostname again just in case.
if (view.containsKey(sid)) {
view.get(sid).recreateSocketAddresses();
}
}
} else {
LOG.debug("There is a connection already for server " + sid);
}
}
FastLeaderElection选举过程
其实在这个投票过程中就涉及到几个类:
FastLeaderElection
FastLeaderElection实现了Election接口,实现各服务器之间基于TCP协议进行选举
Notification
内部类,Notification表示收到的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的id、zxid、选举周期等信息
ToSend
Tosend表示发送给其他服务器的选举投票信息,也包含了被选举者的id、zxid、选举周期等信息
Messenger
Messenger包含了WorkerReceiver和WorkerSender两个内部类
- WorkerReceiver实现了Runnable接口,是选票接收器。会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue中
- WorkerSender也实现了Runnable接口,为选票发送器。会不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中。