Log replication
Commited日志
首先引入Commited日志的概念,那么什么样的日志是Commited日志呢?
Committed:
Leader会决定什么样的日志entry应用到状态机是安全的,这样的日志entry被称为committed。如果一个日志entry被复制到大部分的server,那么这个日志就是一个committed。
Commited的日志会有如下的特性:
如果这个日志被这个Leader标记为commited,那么这个日志之前的日志也都是commited(注意:这里之前的日志,可能是本Leader提交,也可能是其他的历史Leader提交的日志)
Leader决定了一个日志的Committed状态后,就会维护这个日志entry的索引,在后续的RPC中(包括heartbeat),就会把这个索引值附加进去,那么其他的Followers就可以知道这个entry是可以被应用到状态机中。
日志复制过程
日志复制提交过程:
- Leader接受到来自客户端的命令;
- Leader将entry Append到日志中,并且给其他的Followers发送AppendEntry RPC;
- 收到来大部分Follower的回复后,Leader Commit该日志(更新commitIndex),将日志应用到状态机(更新lastApplied),给客户端回复;
- 在Leader的下一个AppendEntry RPC中,会额外包含Commited日志的信息(即自己的commitIndex);
- Follower接收到Commited日志的信息后,会应用到状态机。
因此对于上图所有,Commited Entries是1~7,index8只被复制到2个server。
一致性检查
raft日志具有如下两个特性(Log Matching):
- 在不同的日志中,如果两个entries具有相同的index和term,那么这两个entry存储了相同的内容;
- 在不同的日志中,如果两个entries具有相同的index和term,那么这两个entry的所有前继日志也是完全相同的;
第一个特性的保证:Leader在一个term内,只会在一个index位置出创建一条日志,且这个日志一旦被创建后,就不会再被更改;
第二个特性的保证:**一致性检查:**每次Leader发送AppendEntryRPC时,PRC都会包含最新日志的直接前继日志的index和term,如果Follower发现自己没有这个index和term的日志,那么就会拒绝。
初始化时,满足一致性检查;当日志被Append成功,一致性检查是通过的,Leader就知道Follower的日志和自己是一样的。
一致性检查失败
Leader日志和Follower不一致的情况,可以用下面这个图来列举:
term = 8阶段,新Leader产生,新Leader的日志为1~10,会存在以下几种情况:
a-b: Follower日志缺失,比如老Leader还没有来得及将日志复制过去,就发生了崩溃;
c-d:日志比新Leader多,比如老Leader还没有来得及将日志复制给新Leader(那个时候它的角色还是Follower),但是已经复制给了c(d),发生了奔溃;
e: term4是Leader,但没能将index6,index7复制到其他的Follower就发生了奔溃,后续就一直没有上线;
f:term2是Leader,但是没能将term2期间的日志复制到其他的Follower发生了奔溃重启,term3又当选了Leader,但是还没能将term2和term3的日志复制到其他Follower又发生了奔溃,后来就一直没有上线;
不一致日志的处理:
强制Follower复制自己的日志来解决,这就意味着,Follower中的冲突日志,可能不会覆盖。
额外的限制:
具体的做法:
- 成为新的Leader后,对每一个Follower维护一个
nextIndex
,这表示下一个需要发送给Follower的日志index。对于Figure7,新Leader获选后,nextIndex = 11; - 在下一次AppendEntry RPC中,会包含用于一致性检查的信息(
index = 10, term = 6
); - 以e为例,一致性检查失败,Leader会
nextIndex--
并进行重试,直到index = 5, term = 4
时,一致性检查才通过; - 此时会删除e冲突的日志,append Leader包含的日志。此后,在这个任期内,e的日志就会和Leader的保持一致。
基于以上的机制,与Leader的不一致就会在多次AppendEntries RPC的一致性检查得到收敛,这个过程中修改的都是Followers的日志。Leader从来不会覆盖或者删除自己的日志(Leader Append-only)。
问题讨论
问题一
Leader决定了一个entry的状态为Commited的详细流程,以及之后所做的动作?
Leader:
初始化时
Logs:
commitIndex = 0;
lastApplied = 0;
nextIndex[i] = 1;
matchIndex[i] = 0; // i 指的是对于所有的raft节点
成为Candidate后,构建RequestVote RPC:
RequestVoteArg {
Term = 1,
CandidateId = me, // me指自身的节点id
LastLogIndex = 0,
LastLogTerm = 0,
}
成为Leader后,接受到Command,首先将entry追加到自身的logs中:
更新index:
matchIndex[me] = 1; // 因为自身直接append给自己的log了
nextIndex[me] = 2;
给其他的raft节点发送AppendEntry,构建AppendEntry RPC:
AppendEntrryArg {
term = 1,
LeaderId = me,
Entries,
LeaderCommit = 0,
}
LeaderCommit
被赋值为commitIndex
,当Leader的commitIndex
更新时,会通过AppendEntry带给Follower。
RPC成功,更新对应的index:
matchIndex[i] = 1;
nextIndex[i] = 2;
假设五个节点的RAFT集群,3个更新成功,2个没有更新成功,即多数更新成功, commitIndex
会根据多数节点的matchIndex
的值被更新: commitIndex = 1
。
commitIndex更新为1后,触发apply日志:
for lastApplied < commitIndex {
lastApplied++;
applyLog;
}
lastApplied = 1;
Follower:
Follower收到AppendEntry,会将RPC中携带的logs append到自己的log中。
且如果RPC中携带的LeaderCommit
比自己的commitIndex
大,就更新自身的commitIndex
,然后apply自己的日志,更新lastApplied
。
问题二
日志已经被应用到大多数节点的状态机,此时一个没有复制该日志的节点当选leader(这种情况是否会发生)?此时一致性检测,是否会删除其他节点的日志?这种情况是如何处理的?
回答:
没有复制这个日志条目的节点,由于最新的日志条目没有其他的节点新,因此在elect leader request vote的时候,会被其他的节点拒绝。下面的章节也会说到这个问题。
。