手把手带你撸zookeeper源码-zookeeper故障重启时如何恢复数据(二)

上篇文章 手把手带你撸zookeeper源码-zookeeper故障重启时如何恢复数据(一) 分析了在zookeeper启动的时候从本地日志文件中如何恢复数据,先获取快照文件中的数据然后反序列化到内存中,接着再对日志文件进行增量逐条回放,本篇文章详细分析一下在启动的时候如何和leader建立连接,然后从leader中如何同步数据到本地内存的

 

如果当前存在一个zookeeper集群,现在不管是对原有的zookeeper进行重启,还是新加入一台zookeeper节点,此时zookeeper要么是observer角色、要么是follower角色。当然这是不考虑极端情况,比如刚好碰到zookeeper的leader挂掉,碰到leader选举等情况。因为启动一个zookeeper节点加入到集群中,那么集群中肯定有leader的,我们主要分析一个follower加入到集群中,现在直接定位到Follower.followLeader()方法,至于怎么走到这的可以去看之前zookeer源码剖析系列文章

// 查找leader所在服务器
            QuorumServer leaderServer = findLeader();            
            try {
                //向leader发起连接
                connectToLeader(leaderServer.addr, leaderServer.hostname);
                //向leader进行注册, 经过三次握手
                long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);

                // 校验一下leader的zxid是否小于我们的, 这种情肯定不会发生,只是做个安全检查
                long newEpoch = ZxidUtils.getEpochFromZxid(newEpochZxid);
                if (newEpoch < self.getAcceptedEpoch()) {
                    throw new IOException("Error: Epoch of leader is lower");
                }
                // 直接跟leader进行数据同步
                // 集群启动的磁盘数据恢复、leader -> follower数据同步、leader重新选举之后的数据同步
                syncWithLeader(newEpochZxid);

只复制其中比较关键的代码,也是这次要分析的代码

第一行代码findLeader()就是查找当前leader是哪一台服务器,QuorumServer对象里面封装了选举的地址、leader地址

follower如何和leader建立连接的

第二行代码

connectToLeader(leaderServer.addr, leaderServer.hostname);

向leader发起连接,即要建立一个socket连接

 

然后我们转向Leader.lead()方法看看leader如何创建socket.accept()来等待客户端来连接的,把之前的文章前后关联起来

cnxAcceptor = new LearnerCnxAcceptor();
cnxAcceptor.start();

找到如上代码,创建了LearnerCnxAcceptor对象,它是一个线程,并且启动了这个线程

LearnerCnxAcceptor中的run方法

Socket s = ss.accept();
// start with the initLimit, once the ack is processed
// in LearnerHandler switch to the syncLimit
s.setSoTimeout(self.tickTime * self.initLimit);
s.setTcpNoDelay(nodelay);

也就是说,leader是单独起了一个线程,然后等待客户端连接,没有客户端时此线程会阻塞在accept()方法这,当有客户端进行连接时会接着向下执行代码

Leader如何接收follower发送过来的请求的

//socket 输入流,用来读取客户端发送过来的数据
BufferedInputStream is = new BufferedInputStream(s.getInputStream());
LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
fh.start();

在leader接收到客户端发送过来的连接时,会执行上面的代码,创建一个LearnerHandler对象,封装了socket对象和socket输入流

LearnerHandler也是一个线程,这里提一点,其实在看很多开源源码的时候都会碰到这种xx.start()方法,其实十有八九都是启动一个线程来执行后续的代码。也有一些是自己封装的start方法,然后去启动多个组件的。此时应该执行去看这个对象的run方法中。即leader在接收到一个客户端发送过来的请求之后,会单独启动一个线程来处理此客户端发送过来的请求,接下来如果有客户端发送过来数据的时候我们直接去LearnerHandler中的run方法中看怎么处理数据的即可

follower如何发起注册请求

 //向leader进行注册, 经过三次握手
long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);

在follower和leader建立连接之后,客户端接下里就是向客户端发送注册请求,来获取leader的epoch,我们详细看一下代码

protected long registerWithLeader(int pktType) throws IOException{
    	long lastLoggedZxid = self.getLastLoggedZxid();
        QuorumPacket qp = new QuorumPacket();                
        qp.setType(pktType);
        qp.setZxid(ZxidUtils.makeZxid(self.getAcceptedEpoch(), 0));
        
       
        LearnerInfo li = new LearnerInfo(self.getId(), 0x10000);
        ByteArrayOutputStream bsid = new ByteArrayOutputStream();
        BinaryOutputArchive boa = BinaryOutputArchive.getArchive(bsid);
        boa.writeRecord(li, "LearnerInfo");
        qp.setData(bsid.toByteArray());
        //发送sid和协议版本号给leader,
        writePacket(qp, true);

        // 读取到leader发送回来的leader中的zxid和version、type
        readPacket(qp);        
        final long newEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
        // 第一次接收到leader发送过来的数据,leader发送过来的协议版本号
		if (qp.getType() == Leader.LEADERINFO) {
        	// we are connected to a 1.0 server so accept the new epoch and read the next packet
        	leaderProtocolVersion = ByteBuffer.wrap(qp.getData()).getInt();
        	byte epochBytes[] = new byte[4];
        	final ByteBuffer wrappedEpochBytes = ByteBuffer.wrap(epochBytes);
        	if (newEpoch > self.getAcceptedEpoch()) {
        	    // 如果leader中的epoch比我们本地的大, 则和leader保持同步
        		wrappedEpochBytes.putInt((int)self.getCurrentEpoch());
        		self.setAcceptedEpoch(newEpoch);
        	} else if (newEpoch == self.getAcceptedEpoch()) {
                // 如果一样就不用修改了
                wrappedEpochBytes.putInt(-1);
        	} else {
        		throw new IOException("Leaders epoch, " + newEpoch + " is less than accepted epoch, " + self.getAcceptedEpoch());
        	}
        	// type = ackepoch  zxid = follower的lastZxid, 和 currentEpoch
        	QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
        	// 发送epoch ack回去
        	writePacket(ackNewEpoch, true);
            return ZxidUtils.makeZxid(newEpoch, 0);
        } else {
        	if (newEpoch > self.getAcceptedEpoch()) {
        		self.setAcceptedEpoch(newEpoch);
        	}
            if (qp.getType() != Leader.NEWLEADER) {
                throw new IOException("First packet should have been NEWLEADER");
            }
            return qp.getZxid();
        }
    } 

这是一大段代码,我们一段一段的分析,以一个请求发送数据为一段

long lastLoggedZxid = self.getLastLoggedZxid();
        QuorumPacket qp = new QuorumPacket();                
        qp.setType(pktType);
        qp.setZxid(ZxidUtils.makeZxid(self.getAcceptedEpoch(), 0));
        
        /*
         * Add sid to payload
         * 包装当前的sid为learnerInfo对象
         */
        LearnerInfo li = new LearnerInfo(self.getId(), 0x10000);
        ByteArrayOutputStream bsid = new ByteArrayOutputStream();
        BinaryOutputArchive boa = BinaryOutputArchive.getArchive(bsid);
        boa.writeRecord(li, "LearnerInfo");
        qp.setData(bsid.toByteArray());
        //发送sid和协议版本号给leader,
        writePacket(qp, true);

封装一个QuorumPacket包,里面包含了type = Leader.FOLLOWERINFO = 11,它其实就是代表了当前的请求类型,leader通过解析这个type来判断当前请求如何进行处理的,zxid是从数据日志文件中读取leader的epoch版本,如果没有读取到,则设置为0,另外还封装了一个LearnerInfo对象,里面有当前zookeeper的myid,和一个协议版本号,然后使用jute对数据进行序列化,通过writePacket发送到leader去

我们看看leader如何进行这部分数据如何处理的

// 保存处理当前连接进来的follower的LearnerHandler
            leader.addLearnerHandler(this);
            tickOfNextAckDeadline = leader.self.tick.get()
                    + leader.self.initLimit + leader.self.syncLimit;

            ia = BinaryInputArchive.getArchive(bufferedInput);
            bufferedOutput = new BufferedOutputStream(sock.getOutputStream());
            oa = BinaryOutputArchive.getArchive(bufferedOutput);

            QuorumPacket qp = new QuorumPacket();
            ia.readRecord(qp, "packet");//读取follower发送过来的注册数据包
            if(qp.getType() != Leader.FOLLOWERINFO && qp.getType() != Leader.OBSERVERINFO){
            	LOG.error("First packet " + qp.toString()
                        + " is not FOLLOWERINFO or OBSERVERINFO!");
                return;
            }
            // follower发送注册请求的时候data肯定不为空,包含了follower的zxid(8) + 协议号(4) = 14个字节
            byte learnerInfoData[] = qp.getData();
            if (learnerInfoData != null) {
            	if (learnerInfoData.length == 8) {
            		ByteBuffer bbsid = ByteBuffer.wrap(learnerInfoData);
            		this.sid = bbsid.getLong();
            	} else {//肯定走到这个分支
            		LearnerInfo li = new LearnerInfo();
            		ByteBufferInputStream.byteBuffer2Record(ByteBuffer.wrap(learnerInfoData), li);
            		this.sid = li.getServerid();
            		this.version = li.getProtocolVersion();
            	}
            } else {
            	this.sid = leader.followerCounter.getAndDecrement();
            }
leader.addLearnerHandler(this);

在leader内部 learners属性  set集合中保存了和每个follower相对应的learnerHandler

ia = BinaryInputArchive.getArchive(bufferedInput);
            bufferedOutput = new BufferedOutputStream(sock.getOutputStream());
            oa = BinaryOutputArchive.getArchive(bufferedOutput);

            QuorumPacket qp = new QuorumPacket();
            ia.readRecord(qp, "packet");//读取follower发送过来的注册数据包,jute反序列化

读取follower发送过来的数据包,并反序列化为QuorumPacket对象

 // follower发送注册请求的时候data肯定不为空,包含了follower的zxid(8) + 协议号(4) = 14个字节
            byte learnerInfoData[] = qp.getData();
            if (learnerInfoData != null) {
            	if (learnerInfoData.length == 8) {
            		ByteBuffer bbsid = ByteBuffer.wrap(learnerInfoData);
            		this.sid = bbsid.getLong();
            	} else {//肯定走到这个分支
            		LearnerInfo li = new LearnerInfo();
            		ByteBufferInputStream.byteBuffer2Record(ByteBuffer.wrap(learnerInfoData), li);
            		this.sid = li.getServerid();
            		this.version = li.getProtocolVersion();
            	}
            } else {
            	this.sid = leader.followerCounter.getAndDecrement();
            }

看代码中的注释,不再详细解释,此处主要是解析发送过来的数据中data数据(其实就是上面分析的LearnerInfo对象),读取出来连接进来的follower的sid和version = 0x10000

long lastAcceptedEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
            
            long peerLastZxid;
            StateSummary ss = null;
            long zxid = qp.getZxid();
            //此处会等待超过一半的follower连接进来,newEpoch会返回当前连接进来的所有的follower中epoch最大的一个 + 1
            long newEpoch = leader.getEpochToPropose(this.getSid(), lastAcceptedEpoch);

这里lastAcceptedEpoch是follower传递过来follower可以接受的epoch,这里比较重要的是

long newEpoch = leader.getEpochToPropose(this.getSid(), lastAcceptedEpoch);

这行代码,返回一个最新的epoch,我们可以看看它的代码

public long getEpochToPropose(long sid, long lastAcceptedEpoch) throws InterruptedException, IOException {
        synchronized(connectingFollowers) {
            if (!waitingForNewEpoch) {
                return epoch;
            }
            //每个follower都会进入到这里,此时会获取到当前连接进来follower发送过来的最大的epoch,然后 + 1返回作为最新的epoch
            /**
             * 打个比方有5台机器
             * 第一台: epoch: 5
             * 第二台: epoch: 5
             * 第三台: epoch: 6
             * 第四台: epoch: 5
             * 第五台: epoch: 6
             * 1、2、3台连接进来
             * 第一台进来: 5 >= -1  epoch = 5 + 1 = 6
             * 第二台进来: 5 >= 6  epoch 不变
             * 第三台进来: 6 >= 6  epoch = 6 + 1 = 7
             * 返回 7
             * 不同的机器可能就不一样的,如果是1、2、4连接进来,超过一半,此时会返回 6
             */

            if (lastAcceptedEpoch >= epoch) {
                epoch = lastAcceptedEpoch+1;
            }
            if (isParticipant(sid)) {
                // 如果连接进来的是follower参与者,则保存到connectingFollowers集合中
                connectingFollowers.add(sid);
            }
            QuorumVerifier verifier = self.getQuorumVerifier();
            // 判断现在是否有超过一半的follower连接到leader了
            if (connectingFollowers.contains(self.getId()) && 
                                            verifier.containsQuorum(connectingFollowers)) {
                waitingForNewEpoch = false;
                self.setAcceptedEpoch(epoch);
                // 如果有超一半的连接进来则唤醒
                connectingFollowers.notifyAll();
            } else {
                long start = Time.currentElapsedTime();
                long cur = start;
                long end = start + self.getInitLimit()*self.getTickTime();
                while(waitingForNewEpoch && cur < end) {
                    // 如果没有一半follower连接到leader,并且还有足够的时间等待follower连接进来,则wait等待
                    connectingFollowers.wait(end - cur);
                    cur = Time.currentElapsedTime();
                }
                //等待超时报异常
                if (waitingForNewEpoch) {
                    throw new InterruptedException("Timeout while waiting for epoch from quorum");        
                }
            }
            return epoch;
        }
    }

代码中已经详细剖析了执行流程,此处不再单独拉出来说了

            if (this.getVersion() < 0x10000) {
                // we are going to have to extrapolate the epoch information
                long epoch = ZxidUtils.getEpochFromZxid(zxid);
                ss = new StateSummary(epoch, zxid);
                // fake the message
                leader.waitForEpochAck(this.getSid(), ss);
            } else { // version = 0x10000
                byte ver[] = new byte[4];
                ByteBuffer.wrap(ver).putInt(0x10000);
                QuorumPacket newEpochPacket = new QuorumPacket(Leader.LEADERINFO,
                        ZxidUtils.makeZxid(newEpoch, 0), ver, null);
                oa.writeRecord(newEpochPacket, "packet");
                bufferedOutput.flush();

                // 读取follower发送回来的ack epoch
                QuorumPacket ackEpochPacket = new QuorumPacket();
                ia.readRecord(ackEpochPacket, "packet");
                if (ackEpochPacket.getType() != Leader.ACKEPOCH) {
                    LOG.error(ackEpochPacket.toString()
                            + " is not ACKEPOCH");
                    return;
				}
                // data: follower currentEpoch
                ByteBuffer bbepoch = ByteBuffer.wrap(ackEpochPacket.getData());
                ss = new StateSummary(bbepoch.getInt(), ackEpochPacket.getZxid());
                // 等待超过一半的follower 确认epoch
                leader.waitForEpochAck(this.getSid(), ss);
            }

继续,在上面的代码中已经知道了version = 0x10000, 所以此处肯定是走else分支的代码

                 byte ver[] = new byte[4];
                ByteBuffer.wrap(ver).putInt(0x10000);
                QuorumPacket newEpochPacket = new QuorumPacket(Leader.LEADERINFO,
                        ZxidUtils.makeZxid(newEpoch, 0), ver, null);
                oa.writeRecord(newEpochPacket, "packet");
                bufferedOutput.flush();

此处, leader封装一个QuorumPacket,type = Leader.LEADERINFO,zxid = 上面分析的newEpoch, ver = 0x10000, 发送给follower

 

follower读取leader发送过来的LEADERINFO请求

// 读取到leader发送回来的leader中的zxid和version、type
readPacket(qp);

follower发送出去之后,紧接着调用了readPacket方法等待着leader发送过来的数据,并解析数据到qp对象中,即QuorumPacket对象中

final long newEpoch = ZxidUtils.getEpochFromZxid(qp.getZxid());
        // 第一次接收到leader发送过来的数据,leader发送过来的协议版本号
		if (qp.getType() == Leader.LEADERINFO) {
        	// we are connected to a 1.0 server so accept the new epoch and read the next packet
        	leaderProtocolVersion = ByteBuffer.wrap(qp.getData()).getInt();
        	byte epochBytes[] = new byte[4];
        	final ByteBuffer wrappedEpochBytes = ByteBuffer.wrap(epochBytes);
        	if (newEpoch > self.getAcceptedEpoch()) {
        	    // 如果leader中的epoch比我们本地的大, 则和leader保持同步
        		wrappedEpochBytes.putInt((int)self.getCurrentEpoch());
        		self.setAcceptedEpoch(newEpoch);
        	} else if (newEpoch == self.getAcceptedEpoch()) {
        		// since we have already acked an epoch equal to the leaders, we cannot ack
        		// again, but we still need to send our lastZxid to the leader so that we can
        		// sync with it if it does assume leadership of the epoch.
        		// the -1 indicates that this reply should not count as an ack for the new epoch
                // 如果一样就不用修改了
                wrappedEpochBytes.putInt(-1);
        	} else {
        		throw new IOException("Leaders epoch, " + newEpoch + " is less than accepted epoch, " + self.getAcceptedEpoch());
        	}
        	// type = ackepoch  zxid = follower的lastZxid, 和 currentEpoch
        	QuorumPacket ackNewEpoch = new QuorumPacket(Leader.ACKEPOCH, lastLoggedZxid, epochBytes, null);
        	// 发送epoch ack回去
        	writePacket(ackNewEpoch, true);
            return ZxidUtils.makeZxid(newEpoch, 0);
        } 

接着会走这这段分支代码,发送过来的type = Leader.LEADERINFO

最后会把自己的最新日志zxid和当前follower的epoch(和leader保持同步的epoch)发送给leader进行ack, type = Leader.ACKEPOCH

 

紧接着看看leader如何处理的

                // 读取follower发送回来的ack epoch
                QuorumPacket ackEpochPacket = new QuorumPacket();
                ia.readRecord(ackEpochPacket, "packet");
                if (ackEpochPacket.getType() != Leader.ACKEPOCH) {
                    LOG.error(ackEpochPacket.toString()
                            + " is not ACKEPOCH");
                    return;
				}
                // data: follower currentEpoch
                ByteBuffer bbepoch = ByteBuffer.wrap(ackEpochPacket.getData());
                ss = new StateSummary(bbepoch.getInt(), ackEpochPacket.getZxid());
                // 等待超过一半的follower 确认epoch
                leader.waitForEpochAck(this.getSid(), ss);

leader会读取follower发送过来的数据,如果不是ACKEPOCH则打印error日志并返回,我们知道肯定不会走这个分支的,紧接着往下执行,bbepoch.getInt就是follower和leader进行同步完的epoch,还有follower的最新日志zxid,封装到StateSummary对象中紧接着执行了leader.waitForEpochAck(this.getSid(), ss)

public void waitForEpochAck(long id, StateSummary ss) throws IOException, InterruptedException {
        // electingFollowers 所有需要选举的follower
        synchronized(electingFollowers) {
            if (electionFinished) {
                return;
            }
            if (ss.getCurrentEpoch() != -1) {
                // follower比leader超前,报异常
                if (ss.isMoreRecentThan(leaderStateSummary)) {
                    throw new IOException("Follower is ahead of the leader, leader summary: " 
                                                    + leaderStateSummary.getCurrentEpoch()
                                                    + " (current epoch), "
                                                    + leaderStateSummary.getLastZxid()
                                                    + " (last zxid)");
                }
                // 当前必须是参与者 follower
                if (isParticipant(id)) {
                    electingFollowers.add(id);
                }
            }
            QuorumVerifier verifier = self.getQuorumVerifier();
            if (electingFollowers.contains(self.getId()) && verifier.containsQuorum(electingFollowers)) {
                electionFinished = true;
                electingFollowers.notifyAll();
            } else {                
                long start = Time.currentElapsedTime();
                long cur = start;
                long end = start + self.getInitLimit()*self.getTickTime();
                while(!electionFinished && cur < end) {
                    electingFollowers.wait(end - cur);
                    cur = Time.currentElapsedTime();
                }
                if (!electionFinished) {
                    throw new InterruptedException("Timeout while waiting for epoch to be acked by quorum");
                }
            }
        }
    }

同样的,需要在此处等待超过一半的follower连接进来,然后才能往下执行,此时会保存连接进来的follower保存到electingFollowers对象集合中,即来选举的followers

// 默认发送内存快照,全量
            int packetToSend = Leader.SNAP;
            long zxidToSend = 0;
            long leaderLastZxid = 0;
            /** the packets that the follower needs to get updates from 
             * follower从那开始同步数据
             * **/
            long updates = peerLastZxid;
            
            // 先加读写锁
            ReentrantReadWriteLock lock = leader.zk.getZKDatabase().getLogLock();
            ReadLock rl = lock.readLock();

接着往下看,此时获取内存数据库中的一个读写锁,然后对内存数据结构加一个读锁,此时是不能写数据的,会短暂的暂停对外的服务

下面代码有点长,一个分支一个分支来分析

rl.lock();        //加读锁
                final long maxCommittedLog = leader.zk.getZKDatabase().getmaxCommittedLog();
                final long minCommittedLog = leader.zk.getZKDatabase().getminCommittedLog();

                LinkedList<Proposal> proposals = leader.zk.getZKDatabase().getCommittedLog();

                if (peerLastZxid == leader.zk.getZKDatabase().getDataTreeLastProcessedZxid()) {
                    // Follower is already sync with us, send empty diff
                    // follower 和 leader保持同步
                    LOG.info("leader and follower are in sync, zxid=0x{}",
                            Long.toHexString(peerLastZxid));
                    packetToSend = Leader.DIFF;
                    zxidToSend = peerLastZxid;
                }

前面是获取当前leader内存中最大的已经提交的日志id,和最小的日志id。  proposals集合是获取在proposal阶段但是还没有commit的日志数据

接下来判断follower的zxid是否和leader一样,如果相同的话则就不要同步了

                if (proposals.size() != 0) {
                    // 如果有需要同步的proposal
                    if ((maxCommittedLog >= peerLastZxid)
                            && (minCommittedLog <= peerLastZxid)) {

                        保存上一次同步的proposalId
                        long prevProposalZxid = minCommittedLog;

                        // 发送第一个包之前,告诉 learner是否期望trunc或者diff 对比
                        boolean firstPacket=true;

                        // 使用comittedLog 同步数据到follower
                        packetToSend = Leader.DIFF;
                        zxidToSend = maxCommittedLog;

                        for (Proposal propose: proposals) {
                            //  跳过已经同步的proposals
                            if (propose.packet.getZxid() <= peerLastZxid) {
                                prevProposalZxid = propose.packet.getZxid();
                                continue;
                            } else {
                                // 发送第一个包时,算出是否要trunc, 就是在follower有的proposals但是在leader没有
                                if (firstPacket) {
                                    firstPacket = false;
                                    if (prevProposalZxid < peerLastZxid) {
                                        // 先发送一个trunc,然后再发送diff数据
                                        packetToSend = Leader.TRUNC;                                        
                                        zxidToSend = prevProposalZxid;
                                        updates = zxidToSend;
                                    }
                                }
                                // proposal包
                                queuePacket(propose.packet);
                                // 发送commit包
                                QuorumPacket qcommit = new QuorumPacket(Leader.COMMIT, propose.packet.getZxid(),
                                        null, null);
                                queuePacket(qcommit);
                            }
                        }
                    }

如果有Proposal需要同步,并且follower的zxid在leader最大已提交的commitLog和最小的commitLog之间,则要遍历所有的proposal,并进行判断,是否需要同步,如果需要同步,回先把proposal数据放入到queuePackets队列中,接着封装一个commit,然后放入队列中,此处相当于一条数据会发送两条数据给follower, 一个是Proposal,一个commit发送的。

if (peerLastZxid > maxCommittedLog) {
                        // 如果follwer的zxid比leader要大
                        LOG.debug("Sending TRUNC to follower zxidToSend=0x{} updates=0x{}",
                                Long.toHexString(maxCommittedLog),
                                Long.toHexString(updates));

                        packetToSend = Leader.TRUNC;
                        zxidToSend = maxCommittedLog;
                        updates = zxidToSend;
                    } 

这段的分支就是如果follower的zxid比leader的maxCommitLog还要打,则发送TRUNC截断通知给follower,此处只有一种情况会发生,就是当客户端频繁写入数据的时候,集群中的leader先把数据写入到本地日志文件中,还没来得及给其他的follower发送proposal的时候,此时leader宕机了,此时leader的zxid是比其他的follower超前的,此时剩余的follower会重新选举leader,然后宕机后的leader重新启动,以follower角色加入集群,此时新leader就会发送截断的通知给旧leader

 

接着往下执行

            QuorumPacket newLeaderQP = new QuorumPacket(Leader.NEWLEADER,
                    ZxidUtils.makeZxid(newEpoch, 0), null, null);
            if (getVersion() < 0x10000) {
                oa.writeRecord(newLeaderQP, "packet");
            } else {
                queuedPackets.add(newLeaderQP);
            }
            bufferedOutput.flush();
            //Need to set the zxidToSend to the latest zxid
            if (packetToSend == Leader.SNAP) {
                zxidToSend = leader.zk.getZKDatabase().getDataTreeLastProcessedZxid();
            }
            oa.writeRecord(new QuorumPacket(packetToSend, zxidToSend, null, null), "packet");
            bufferedOutput.flush();

封装了一个type = Leader.NEWLEADER的数据,并加入到queuedPackets队列中,现在只看到使劲往queuedPackets队列里面仍数据,还没看到在哪进行发送的

如如果需要发送SNAP数据,则获取leader内存中最新的zxid = zxidToSend

/*
             *if we are not truncating or sending a diff just send a snapshot
             * 如果不截断、或者发送diff, 则发送snap,全量内存快照
             */
            if (packetToSend == Leader.SNAP) {
                // Dump data to peer, dump内存数据,发送给follower
                leader.zk.getZKDatabase().serializeSnapshot(oa);
                oa.writeString("BenWasHere", "signature");
            }
            bufferedOutput.flush();

把leader内存中的数据库进行序列化为快照,然后发送出去

            // 启动一个线程发送数据
            new Thread() {
                public void run() {
                    Thread.currentThread().setName(
                            "Sender-" + sock.getRemoteSocketAddress());
                    try {
                        // 发送要同步的数据, proposal、 commit数据
                        sendPackets();
                    } catch (InterruptedException e) {
                        LOG.warn("Unexpected interruption",e);
                    }
                }
            }.start();

紧接着启动了一个线程,然后调用了sendPackets()方法

private void sendPackets() throws InterruptedException {
        long traceMask = ZooTrace.SERVER_PACKET_TRACE_MASK;
        while (true) {
            try {
                QuorumPacket p;
                p = queuedPackets.poll();// 从队列中读取数据
                if (p == null) {
                    bufferedOutput.flush();
                    p = queuedPackets.take();
                }

                if (p == proposalOfDeath) {
                    // Packet of death!
                    break;
                }
                if (p.getType() == Leader.PING) {
                    traceMask = ZooTrace.SERVER_PING_TRACE_MASK;
                }
                if (p.getType() == Leader.PROPOSAL) {
                    syncLimitCheck.updateProposal(p.getZxid(), System.nanoTime());
                }
                if (LOG.isTraceEnabled()) {
                    ZooTrace.logQuorumPacket(LOG, traceMask, 'o', p);
                }
                // 发送过去
                oa.writeRecord(p, "packet");
            } catch (IOException e) {
            }
        }
    }

此处就是会从我们前面往queuedPackets队列里面的数据,然后发送出去

接着follower向leader注册完毕之后,接着执行以下代码

 // 校验一下leader的zxid是否小于我们的, 这种情肯定不会发生,只是做个安全检查
                long newEpoch = ZxidUtils.getEpochFromZxid(newEpochZxid);
                if (newEpoch < self.getAcceptedEpoch()) {
                    throw new IOException("Error: Epoch of leader is lower");
                }
                // 直接跟leader进行数据同步
                // 集群启动的磁盘数据恢复、leader -> follower数据同步、leader重新选举之后的数据同步
                syncWithLeader(newEpochZxid);

执行syncWithLeader()开始和leader同步数据

// 如果和leader之间有不同的数据, 则不需要快照
            if (qp.getType() == Leader.DIFF) {
                LOG.info("Getting a diff from the leader 0x{}", Long.toHexString(qp.getZxid()));
                // 如果当前follower宕机,然后恢复重启,此时会落后leader一部分数据,然后去同步宕机之后的数据即可
                snapshotNeeded = false;
            }
            // 从leader获取快照信息
            else if (qp.getType() == Leader.SNAP) {
                LOG.info("Getting a snapshot from leader 0x" + Long.toHexString(qp.getZxid()));
                // The leader is going to dump the database
                // clear our own database and read
                // 清空本地的数据文件和内存数据库,从leader中读取快照数据,进行反序列化
                // 可能当前的zk服务器新加入集群的,此时会从leader进行全量同步数据
                zk.getZKDatabase().clear();
                zk.getZKDatabase().deserializeSnapshot(leaderIs);
                // 读取签名
                String signature = leaderIs.readString("signature");
                // 全量同步时必须有signature = BenWasHere
                if (!signature.equals("BenWasHere")) {
                    LOG.error("Missing signature. Got " + signature);
                    throw new IOException("Missing signature");                   
                }
                zk.getZKDatabase().setlastProcessedZxid(qp.getZxid());
            } else if (qp.getType() == Leader.TRUNC) {
                //we need to truncate the log to the lastzxid of the leader
                // 根据leader的lastzxid对本地日志进行截断
                // 有这样的一种场景,当前zk服务器原来是leader,然后有客户端发送过来数据,写入本地日志文件,还没来得及发送给follower
                // 此时leader宕机了,然后原有的集群中某个follower会被选为leader,此时相当于就丢掉了一条数据
                // 当挂掉的leader宕机恢复之后,会作为一个follower加入集群中,此时回和leader进行同步,因为此时的leader中是没有这条数据的
                // 所以会把宕机之前最后的一条数据给删除掉
                LOG.warn("Truncating log to get in sync with the leader 0x"
                        + Long.toHexString(qp.getZxid()));
                // 阶段日志
                boolean truncated=zk.getZKDatabase().truncateLog(qp.getZxid());
                if (!truncated) {
                    // not able to truncate the log
                    LOG.error("Not able to truncate the log "
                            + Long.toHexString(qp.getZxid()));
                    System.exit(13);
                }
                zk.getZKDatabase().setlastProcessedZxid(qp.getZxid());
            }
            else {
                LOG.error("Got unexpected packet from leader "
                        + qp.getType() + " exiting ... " );
                System.exit(13);

            }

这个分支里面的代码之前有分析过,并且代码中的注释也写的比较想起,直接看代码中的注释吧

// 创建会话跟踪器, 初始化nextSessionId
            zk.createSessionTracker();

这个之前讲过,主要是为以后客户端连接进来创建session的,之前有分析,可以去前面文章看看,此处不分析

接下来是一个while循环,按照分支来逐步分析

// 开始读取leader发送过来的proposal
                readPacket(qp);
                switch(qp.getType()) {
                case Leader.PROPOSAL: // 如果是proposal提议的数据
                    PacketInFlight pif = new PacketInFlight();
                    pif.hdr = new TxnHeader();
                    pif.rec = SerializeUtils.deserializeTxn(qp.getData(), pif.hdr);
                    if (pif.hdr.getZxid() != lastQueued + 1) {
                    LOG.warn("Got zxid 0x"
                            + Long.toHexString(pif.hdr.getZxid())
                            + " expected 0x"
                            + Long.toHexString(lastQueued + 1));
                    }
                    lastQueued = pif.hdr.getZxid();
                    packetsNotCommitted.add(pif);//把处于proposal阶段的数据加入到packetsNotCommitted集合中
                    break;

此处如果是同步的leader发送过来的proposals阶段的数据,就放到packetsNotCommitted集合中

                    case Leader.COMMIT:// commit数据
                    if (!writeToTxnLog) {
                        pif = packetsNotCommitted.peekFirst();
                        if (pif.hdr.getZxid() != qp.getZxid()) {
                            LOG.warn("Committing " + qp.getZxid() + ", but next proposal is " + pif.hdr.getZxid());
                        } else {
                            zk.processTxn(pif.hdr, pif.rec);
                            packetsNotCommitted.remove();
                        }
                    } else {
                        packetsCommitted.add(qp.getZxid());
                    }
                    break;

此处主要是已经commit提交的数据,一种是已经commit过的数据,另一种可能是proposal阶段的数据,然后在同步过程中进行了commit操作

                case Leader.INFORM:// 只是通知  observer
                    PacketInFlight packet = new PacketInFlight();
                    packet.hdr = new TxnHeader();
                    packet.rec = SerializeUtils.deserializeTxn(qp.getData(), packet.hdr);
                    // Log warning message if txn comes out-of-order
                    if (packet.hdr.getZxid() != lastQueued + 1) {
                        LOG.warn("Got zxid 0x"
                                + Long.toHexString(packet.hdr.getZxid())
                                + " expected 0x"
                                + Long.toHexString(lastQueued + 1));
                    }
                    lastQueued = packet.hdr.getZxid();
                    if (!writeToTxnLog) {
                        // Apply to db directly if we haven't taken the snapshot
                        zk.processTxn(packet.hdr, packet.rec);
                    } else {
                        packetsNotCommitted.add(packet);
                        packetsCommitted.add(qp.getZxid());
                    }
                    break;

这个分支代码只适用于当前zookeeper的角色是observer角色,只是通知给observer

                   case Leader.UPTODATE:   // 标识同步leader数据已经同步完了,可以接收客户端发起的请求了
                    if (isPreZAB1_0) {
                        zk.takeSnapshot();
                        self.setCurrentEpoch(newEpoch);
                    }
                    self.cnxnFactory.setZooKeeperServer(zk);                
                    break outerLoop;    // 跳出循环

此处的代码就是当leader同步完数据之后会发送一个UPTODATE来通知follower同步完数据了,然后跳出循环

                case Leader.NEWLEADER: 
                    File updating = new File(self.getTxnFactory().getSnapDir(),
                                        QuorumPeer.UPDATING_EPOCH_FILENAME);
                    if (!updating.exists() && !updating.createNewFile()) {
                        throw new IOException("Failed to create " +
                                              updating.toString());
                    }
                    if (snapshotNeeded) {
                        zk.takeSnapshot();// 保存快照
                    }
                    self.setCurrentEpoch(newEpoch);
                    if (!updating.delete()) {
                        throw new IOException("Failed to delete " +
                                              updating.toString());
                    }
                    writeToTxnLog = true; //Anything after this needs to go to the transaction log, not applied directly in memory
                    isPreZAB1_0 = false;
                    writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true);
                    break;
                }

就是发送一个NEWLEADER通知,本地回先把内存中的数据生成一个快照文件保存起来,然后这是epoch和leader保持同步

ack.setZxid(ZxidUtils.makeZxid(newEpoch, 0));
// 给leader发送ack
writePacket(ack, true);

接着follower会给leader发送一个ack

 

leader处理ACK

            qp = new QuorumPacket();
            ia.readRecord(qp, "packet");
            if(qp.getType() != Leader.ACK){
                LOG.error("Next packet was supposed to be an ACK");
                return;
            }
            LOG.info("Received NEWLEADER-ACK message from " + getSid());
            // 进入等待,follower发送ACK
            leader.waitForNewLeaderAck(getSid(), qp.getZxid());

而leader也在启动一个线程发送数据之后,会等待follower发送ACK

    public void waitForNewLeaderAck(long sid, long zxid)
            throws InterruptedException {

        synchronized (newLeaderProposal.ackSet) {

            if (quorumFormed) {
                return;
            }

            long currentZxid = newLeaderProposal.packet.getZxid();
            if (zxid != currentZxid) {
            }

            if (isParticipant(sid)) {
                newLeaderProposal.ackSet.add(sid);
            }

            if (self.getQuorumVerifier().containsQuorum(
                    newLeaderProposal.ackSet)) {
                quorumFormed = true;
                newLeaderProposal.ackSet.notifyAll();
            } else {
                long start = Time.currentElapsedTime();
                long cur = start;
                long end = start + self.getInitLimit() * self.getTickTime();
                while (!quorumFormed && cur < end) {
                    newLeaderProposal.ackSet.wait(end - cur);
                    cur = Time.currentElapsedTime();
                }
                if (!quorumFormed) {
                    throw new InterruptedException(
                            "Timeout while waiting for NEWLEADER to be acked by quorum");
                }
            }
        }
    }

此时把发送ack的follower加入到newLeaderProposal.ackSet集合中,当有超过一半的follower发送了ack之后,接着往下执行代码

 queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));

最后会发送UPTODATE通知follower数据已经同步完毕了

接下来的代码就是leader开始进入while死循环,然后进行处理其他follower发送过来的写请求,然后开始2PC阶段的数据处理,此处不分析

 

follower接收到leader发送过来同步的数据,然后保存到了packetsNotCommitted集合和packetsCommitted中

// 如果从leader中读取到的数据是proposal或者commit,应该交给processor处理链条进行2pc去处理
        if (zk instanceof FollowerZooKeeperServer) { // 如果当前是follower
            FollowerZooKeeperServer fzk = (FollowerZooKeeperServer)zk;
            for(PacketInFlight p: packetsNotCommitted) {
                //如果是proposal数据,则记录日志,作为一个日志请求处理
                fzk.logRequest(p.hdr, p.rec);//syncProcessor调用链处理
            }
            for(Long zxid: packetsCommitted) {
                // 如果是commit,则提交当前的proposal
                fzk.commit(zxid);   // commitProcessor调用链处理
            }
        } else if (zk instanceof ObserverZooKeeperServer) {//当前是observer
            // Similar to follower, we need to log requests between the snapshot
            // and UPTODATE
            ObserverZooKeeperServer ozk = (ObserverZooKeeperServer) zk;
            for (PacketInFlight p : packetsNotCommitted) {
                Long zxid = packetsCommitted.peekFirst();
                if (p.hdr.getZxid() != zxid) {
                    // log warning message if there is no matching commit
                    // old leader send outstanding proposal to observer
                    LOG.warn("Committing " + Long.toHexString(zxid)
                            + ", but next proposal is "
                            + Long.toHexString(p.hdr.getZxid()));
                    continue;
                }
                packetsCommitted.remove();
                Request request = new Request(null, p.hdr.getClientId(),
                        p.hdr.getCxid(), p.hdr.getType(), null, null);
                request.txn = p.rec;
                request.hdr = p.hdr;
                ozk.commitRequest(request);
            }
        } else {
            // New server type need to handle in-flight packets
            throw new UnsupportedOperationException("Unknown server type");
        }

上面的代码就是会根据当前zookeeper是follower还是observer来进行处理,处理方式大同小异,分为两部分,先处理proposal阶段的数据,然后处理commit阶段的数据,有可能在proposal处理的数据已经提交了,此时leader也会再次发送commit给follower的,接下来的 zk.logRequest()和zk.commit()就会走整个数据处理的调用链了,这块之后的文章分析

 

然后follower和leader进行数据同步完了之后呢

                QuorumPacket qp = new QuorumPacket();
                while (this.isRunning()) {
                    readPacket(qp); //从leader读数据
                    processPacket(qp);
                }

这块的代码就是读取leader发送过来的proposal数据,把数据写入本地日志文件,然后给leader发送ack,紧接着就是leader等待超过半数的follower发送了ack之后,会发送commit给follower,follower再把数据写入到内存zkDataBase数据库中,这个就是典型的2PC两阶段提交,

 

接下来会分析客户端和zk集群建立连接、以及zk集群之间如何对数据进行的处理、session处理、2pc处理、watcher通知等

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值