上篇文章 手把手带你撸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通知等