zookeeper集群
配置多个实例共同构成一个集群对外提供服务以达到水平扩展的目的,每个服务器上的数据是相同的,每一个服务器均可以对外提供读和写的服务,这点和redis是相同的,即对客户端来讲每个服务器都是平等的。
这篇主要分析leader的选择机制,zookeeper提供了三种方式:
- LeaderElection
- AuthFastLeaderElection
- FastLeaderElection
默认的算法是FastLeaderElection,所以这篇主要分析它的选举机制。
选择机制中的概念
服务器ID
比如有三台服务器,编号分别是1,2,3。
编号越大在选择算法中的权重越大。
数据ID
服务器中存放的最大数据ID.
值越大说明数据越新,在选举算法中数据越新权重越大。
逻辑时钟
或者叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比,根据不同的值做出不同的判断。
选举状态
- LOOKING,竞选状态。
- FOLLOWING,随从状态,同步leader状态,参与投票。
- OBSERVING,观察状态,同步leader状态,不参与投票。
- LEADING,领导者状态。
选举消息内容
在投票完成后,需要将投票信息发送给集群中的所有服务器,它包含如下内容。
- 服务器ID
- 数据ID
- 逻辑时钟
- 选举状态
选举流程图
因为每个服务器都是独立的,在启动时均从初始状态开始参与选举,下面是简易流程图。
选举状态图
描述Leader选择过程中的状态变化,这是假设全部实例中均没有数据,假设服务器启动顺序分别为:A,B,C。
源码分析
QuorumPeer
主要看这个类,只有LOOKING状态才会去执行选举算法。每个服务器在启动时都会选择自己做为领导,然后将投票信息发送出去,循环一直到选举出领导为止。
public void run() { //....... try { while (running) { switch (getPeerState()) { case LOOKING: if (Boolean.getBoolean("readonlymode.enabled")) { //... try { //投票给自己... setCurrentVote(makeLEStrategy().lookForLeader()); } catch (Exception e) { //... } finally { //... } } else { try { //... setCurrentVote(makeLEStrategy().lookForLeader()); } catch (Exception e) { //... } } break; case OBSERVING: //... break; case FOLLOWING: //... break; case LEADING: //... break; } } } finally { //... } }
FastLeaderElection
它是zookeeper默认提供的选举算法,核心方法如下:具体的可以与本文上面的流程图对照。
public Vote lookForLeader() throws InterruptedException { //... try { HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>(); int notTimeout = finalizeWait; synchronized(this){ //给自己投票 logicalclock.incrementAndGet(); updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } //将投票信息发送给集群中的每个服务器 sendNotifications(); //循环,如果是竞选状态一直到选举出结果 while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){ Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS); //没有收到投票信息 if(n == null){ if(manager.haveDelivered()){ sendNotifications(); } else { manager.connectAll(); } //... } //收到投票信息 else if (self.getCurrentAndNextConfigVoters().contains(n.sid)) { switch (n.state) { case LOOKING: // 判断投票是否过时,如果过时就清除之前已经接收到的信息 if (n.electionEpoch > logicalclock.get()) { logicalclock.set(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.get()) { //忽略 break; } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { //更新投票信息 updateProposal(n.leader, n.zxid, n.peerEpoch); sendNotifications(); } recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); //判断是否投票结束 if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch))) { // 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; } } if (n == null) { self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState()); Vote endVote = new Vote(proposedLeader, proposedZxid, proposedEpoch); leaveInstance(endVote); return endVote; } } break; case OBSERVING: //忽略 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; } } //记录投票已经完成 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; } } else { LOG.warn("Ignoring notification from non-cluster member " + n.sid); } } return null; } finally { //... } }
判断是否已经胜出
默认是采用投票数大于半数则胜出的逻辑。
选举流程简述
一、zk的leader选举分两种情况,第一种是启动的时候,第二种是leader奔溃的时候。
二、说zk 的leader选举之前需要了解两个东西,一个是myid和zxid
1.myid:zk集群中服务器的唯一标识,称为myid,例如有三个zk服务器,那么编号分别为1,2,3
2.zxid:zxid为Long类型,zxid有64为,其中高32位表示epoch,低32位表示xid。即zxid由两部分组成:epoch和xid。每个Leader都会具有一个不同的epoch值,表示一个时期、时代。新的Leader产生,则会更新所有zkServer的zxid中的epoch,而xid则为zk的事务id,每一次写操作都是一个事务,都会有一个xid。每一个写操作都需要由Leader发起一个提议,由所有Follwer表决是否同意本次写操作。
三、
选举机制:集群中,半数zkServer同意,则产生新的Leader(搭建集群时,一般都是奇数个),三台服务器,最多允许一台宕机,四台服务器也最多允许一台宕机。
启动的情况如下:
选举过程:假设有三台服务器,server1(myid=1),server2(myid=2),server3(myid=3),
server1启动之后给自己投票,发布投票结果(myid,zxid)。
server2启动之后同样给自己投票,发布投票结果(myid,zxid),跟server1交换投票结果,zxid一样,server2的myid大于server1的myid,所有server2当选leader。
server3启动之后虽然myid比前两台服务器的myid都大,但是因为这个时候server2已经胜出了,所有server2也只能是作为一个follwer。
Leader宕机的情况如下:
选举过程:假设有三台服务器,server1(myid=1),server2(myid=2),server3(myid=3)
此时server2为leader,当server2宕机时,server1和server3的状态会进入looking的一个状态,此时分两种情况,zxid相同和不相同:
1)zxid相同:因为server3的myid大于server1的myid所有server3当选Leader。
2)zxid不同:如果server1的zxid大于server3的zxid则server1当选leader,这里的server1的zxid大于server3的zxid的意思就是 在server2宕机的时候server1同步了server2的事务过来,而server3没有同步server2的事务过来,那么此时的server1的zxid就回大于server3的zxid,其实也就是server1的数据更新,所以肯定server1当选leader。