1.概述
今天来看一下jraft如何将日志写入到状态机,其实就是业务真正的存储工作。如果我们需要使用jraft,我们对这里的实现就需要足够的了解。然后还会介绍jraft的读取逻辑。
2.思路整理
对于状态机,我们关注问题如下:
- 何时会将日志同步到状态机?
- 对于节点变化,状态机会做什么?
- 状态机为了业务解藕做了怎么样封装?
对于读取操作:主要就是如何做读取优化操作?
那我们带着这几个问题一起深入源码,寻找答案吧!
3.状态机源码分析
主要就是leader和follower将日志应用到状态机的过程。当然leader和follower应用的时机不一样,但是过程都是一样的。
我们先来看leader。leader再增加日志的时候,会有一个回调,如果成功会执行这个回调方法(具体时机为将日志添加到本地磁盘后,也就是AppendBatcher 的flush方法)。
这个回调回执行ballotBox的commitAt方法。
@Override
public void run(final Status status) {
if (status.isOk()) {
NodeImpl.this.ballotBox.commitAt(this.firstLogIndex, this.firstLogIndex + this.nEntries - 1,
NodeImpl.this.serverId);
} else {
LOG.error("Node {} append [{}, {}] failed, status={}.", getNodeId(), this.firstLogIndex,
this.firstLogIndex + this.nEntries - 1, status);
}
}
commitAt方法
这里说一下,ballotBox主要记录了日志提交的状态。每个节点提交日志成功后都会调用这个方法。
上面只说了leader成功,如果follower提交成功,则会以响应的形式告诉leader。在onAppendEntriesReturned 中也会调用该方法。如下图。
这就很清晰了。其实上篇博客都介绍了这个方法的作用。因为和状态机实现衔接,所以我们在来回顾一下这个方法。
final long startAt = Math.max(this.pendingIndex, firstLogIndex);
Ballot.PosHint hint = new Ballot.PosHint();
for (long logIndex = startAt; logIndex <= lastLogIndex; logIndex++) {
final Ballot bl = this.pendingMetaQueue.get((int) (logIndex - this.pendingIndex));
hint = bl.grant(peer, hint);
if (bl.isGranted()) {
lastCommittedIndex = logIndex;
}
}
if (lastCommittedIndex == 0) {
return true;
}
this.pendingMetaQueue.removeFromFirst((int) (lastCommittedIndex - this.pendingIndex) + 1);
LOG.debug("Committed log fromIndex={}, toIndex={}.", this.pendingIndex, lastCommittedIndex);
this.pendingIndex = lastCommittedIndex + 1;
this.lastCommittedIndex = lastCommittedIndex;
...
this.waiter.onCommitted(lastCommittedIndex);
- pendingIndex:当前已经ok的日志索引+1(何为ok,就是大多数节点都持久化的)
- firstLogIndex:本次提交成功日志的起始值。
- lastLogIndex:本次提交成功日志的终止值。
为什么这里startAt为max,因为这个有很大的可能pendingIndex比firstLogIndex大,原因是这个节点响应比较慢。在他响应之前Ballot的isGranted已经返回true了。
这样的话,我们能理解这个方法其实就是用来维护this.lastCommittedIndex这个成员变量。最后他会调用this.waiter.onCommitted方法。
onCommitted方法
其实这个方法就是commit到状态机的入口。
当然这个方法也会在两处被调用。一处是被leader,一处是被follower调用。
@Override
public boolean onCommitted(final long committedIndex) {
return enqueueTask((task, sequence) -> {
task.type = TaskType.COMMITTED;
task.committedIndex = committedIndex;
});
}
这个方法逻辑比较简单,就是创建一个commit时间丢到FSMCallerImpl 的队列。
我们顺藤摸瓜看看follower何时调用这个onCommitted方法。
FollowerStableClosure#run
1.在follower增加日志成功之后,有执行FollowerStableClosure 回调,上篇文章说过,他就是用来响应leader。当然在响应之前回执行node.ballotBox.setLastCommittedIndex方法。其实这个方法最后会调用onCommitted方法。
2.在follower处理心跳或者探针消息的时候。也会调用setLastCommittedIndex方法。ok,到这里我们已经了解了。follower和leader何时会给FSMCallerImpl 的队列提交commit事件。接下来我们只需要关注如何处理事件了。
if (entriesCount == 0) {
// heartbeat
final AppendEntriesResponse.Builder respBuilder = AppendEntriesResponse.newBuilder() //
.setSuccess(true) //
.setTerm(this.currTerm) //
.setLastLogIndex(this.logManager.getLastLogIndex());
doUnlock = false;
this.writeLock.unlock();
// see the comments at FollowerStableClosure#run()
this.ballotBox.setLastCommittedIndex(Math.min(request.getCommittedIndex(), prevLogIndex));
return respBuilder.build(