6、MsgApp消息
在MsgVote消息的处理过程中,集群中其他节点己经切换成了Follower状态, 并且它们自身记录的Term值与该任期中Leader节点维护的Term值相同。当它们收到当前Leader节点发来的MsgApp消息时,也是交由ra仕Step()方法处理的。
func (r *raft) Step(m pb.Message) error {
switch m.Type {
case pb.MsgHup:
case pb.MsgVote, pb.MsgPreVote:
default:
r.step(r, m)//当前节点是Follower状态,raft.step字段指向stepFollower( )函数
}
return nil
}
func stepFollower(r *raft, m pb.Message) {
switch m.Type {
case pb.MsgProp:
case pb.MsgApp:
r.electionElapsed = 0//重置选举计算器,防止当前Follower发起新一轮选举
r.lead = m.From //设立raft.Lead记录,保存当前集群的Leader节点ID
r.handleAppendEntries(m)//将MsgApp消息中携带的Entry记录追加到raftLog中, 并且向Leader节点发送MsgAppResp消息,响应此次MsgApp消息
case pb.MsgHeartbeat:
r.electionElapsed = 0
r.lead = m.From
r.handleHeartbeat(m)
case pb.MsgSnap:
r.electionElapsed = 0
r.lead = m.From
r.handleSnapshot(m)
case pb.MsgTransferLeader:
if r.lead == None {
r.logger.Infof("%x no leader at term %d; dropping leader transfer msg", r.id, r.Term)
return
}
m.To = r.lead
r.send(m)
case pb.MsgTimeoutNow:
case pb.MsgReadIndex:
case pb.MsgReadIndexResp:
}
}
raft.handleAppendEntries()方法首先会检测MsgApp消息中携带的Entry记录是否合法,然后将这些Entry记录追加到raftLog中,最后创建相应的MsgAppResp消息。handleAppendEntries()方法
func (r *raft) handleAppendEntries(m pb.Message) {
//m.Index表示leader发送给follower的上一条日志的索引位置,如采Follower节点在Index位置的Entry记录已经提交过了,
//则不能进行追加操作,在前面在Raft协议时捉到过,己提交的记录不能被覆盖,
//所以Follower节点会将其committed位置通过MsgAppResp消息(Index字段)通知Leader节点
if m.Index < r.raftLog.committed {
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
return
}
//尝试将消息携带的Entry记录追加到raftLog中
if mlastIndex, ok := r.raftLog.maybeAppend(m.Index, m.LogTerm, m.Commit, m.Entries...); ok {
//如采追加成功,则将最后一条记录的索引位通过MsgAppResp消息返回给Leader节点,这样Leader节点就可以根据此值更新其对应的Next和Match值
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: mlastIndex})
} else {
//如采追加记录失败,则将失败信息返回给Leader节点(即MsgAppResp 消息的Reject字段为true),同时返回的还有一些提示信息(RejectHint字段保存了当前节点raftLog中最后一条记录的索引)
r.logger.Debugf("%x [logterm: %d, index: %d] rejected msgApp [logterm: %d, index: %d] from %x",
r.id, r.raftLog.zeroTermOnErrCompacted(r.raftLog.term(m.Index)), m.Index, m.LogTerm, m.Index, m.From)
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: m.Index, Reject: true, RejectHint: r.raftLog.lastIndex()})
}
}
7、MsgAppResp消息
再回到Leader节点,当其收到集群中其他Follower节点发迭的MsgAppResp响应消息之后,也是交由raft.Step()方法进行处理的,其中会调用raft.step 字段指向的StepLeader() 函数进行处理的。下面开始分析raft.StepLeader()方法中处理MsgAppResp消息的代码
func stepLeader(r *raft, m pb.Message) {
// These message types do not require any progress for m.From.
switch m.Type {
case pb.MsgBeat:
case pb.MsgCheckQuorum:
case pb.MsgProp:
case pb.MsgReadIndex:
//根据消息的From字段获取对应的Progress实例,为后面的消息处理做准备
// All other message types require a progress for m.From (pr).
pr := r.getProgress(m.From)
if pr == nil {
r.logger.Debugf("%x no progress available for %x", r.id, m.From)
return
}
switch m.Type {
case pb.MsgAppResp:
//更新对应Progress实例的RecentActive字段,从Leader节点的角度来看,MsgAppResp消息的发送节点还是存活的
pr.RecentActive = true
if m.Reject {//MsgApp 消息被拒绝
r.logger.Debugf("%x received msgApp rejection(lastindex: %d) from %x for index %d",
r.id, m.RejectHint, m.From, m.Index)
//通过MsgAppResp消息携带的信息及对应的Progress状态,重新设立其Next
if pr.maybeDecrTo(m.Index, m.RejectHint) {
//如果对应的Progress处于ProgressStateReplicate状态,则切换成
I //ProgressStateProbe状态,试探Follower的匹配位置(这里的试探是指发送一条消息并等待其相应之后,再发送后续的消息)
r.logger.Debugf("%x decreased progress of %x to [%s]", r.id, m.From, pr)
if pr.State == ProgressStateReplicate {
pr.becomeProbe()
}
//再次向对应Follower节点发送MsgApp消息,在sendAppend()方法中会将对应的Progress.pause字段设立为true,从而暂停后续消息的发送,从而实现前面说的“试探”的效果
r.sendAppend(m.From)
}
} else {//之前发送的MsgApp消息已经被对反的Follower节点接收(Entry记录被成功追加)
oldPaused := pr.IsPaused()
//MsgAppResp消息的Index字段是对应Follower节点raftLog中最后一条Entry记录的索引,这里会根据该值更新其对应Progress实例的Match和Next,Progress. maybeUpdate ()
//方法在前面已经介绍过了
if pr.maybeUpdate(m.Index) {
switch {
case pr.State == ProgressStateProbe:
//一旦MsgApp被Follower节点接收,则表示已经找到其正确的Next和Match,不必再进行“试探”,这里将对应的Progress.state切换成ProgressStateReplicate
pr.becomeReplicate()
case pr.State == ProgressStateSnapshot && pr.needSnapshotAbort():
r.logger.Debugf("%x snapshot aborted, resumed sending replication messages to %x [%s]", r.id, m.From, pr)
//之前由于某些原因,Leader节点通过发送快照的方式恢复Follower节点,但在发送MsgSnap消息的过程中,Follower节点恢复,并正常接收了Leader节点的MsgApp消
//息,此时会丢弃MsgSnap消息,并开始“试探”该Follower节点正确的Match和Next值
pr.becomeProbe()
case pr.State == ProgressStateReplicate:
//之前向某个Follower节点发送MsgApp消息时,会将其相关信息保存到对应的
//Progress.ins中,在这里收到相应的MsgAppResp响应之后,会将其从ins中删除,
//这样可以实现了限流的效采,避免网络出现延迟时,继续发送消息,从而导致网络更加拥堵
pr.ins.freeTo(m.Index)
}
//收到一个Follower节点的MsgAppResp消息之后,除了修改相应的Match和Next,还会尝试更新raftLog.committed,因为有些Entry记录可能在此次复制中被保存到了
//半数以上的节点中,raft.maybeCommit()方法在前面已经分析过了
if r.maybeCommit() {
//向所有节点发送MsgApp消息,注意,此次MsgApp消息的Commit字段与上次MsgApp消息已经不同,raft.bcastAppend()方法前面已经讲过
r.bcastAppend()
} else if oldPaused {//之前是pause状态,现在可以任性地发消息了
// update() reset the wait state on this node. If we had delayed sending
// an update before, send it now.
//之前Leader节点暂停向该Follower节点发送消息,收到MsgAppResp消息后,在上述代码中已经重立了相应状态,所以可以继续发送MsgApp消息
r.sendAppend(m.From)
}
// Transfer leadership is in progress.
if m.From == r.leadTransferee && pr.Match == r.raftLog.lastIndex() {
r.logger.Infof("%x sent MsgTimeoutNow to %x after received MsgAppResp", r.id, m.From)
r.sendTimeoutNow(m.From)
}
}
}
case pb.MsgHeartbeatResp:
case pb.MsgSnapStatus:
case pb.MsgUnreachable:
// During optimistic replication, if the remote becomes unreachable,
// there is huge probability that a MsgApp is lost.
if pr.State == ProgressStateReplicate {
pr.becomeProbe()
}
r.logger.Debugf("%x failed to send message to %x because it is unreachable [%s]", r.id, m.From, pr)
case pb.MsgTransferLeader:
}
}
stepLeader()方法关于MsgAppResp消息处理的代码片段中,介绍一下两个第一次遇到的方法。首先是Progress.maybeDecrTo()方法,它会根据对应Progress的状态和MsgAppResp消息携带的提示信息,完成Progress.Next的更新
//maybeDecrTo()方法的两个参数都是MsgAppResp消息携带的信息:
//reject是被拒绝MsgApp消息的Index字段佳,
//last是被拒绝MsgAppResp消息的RejectHint字段佳(即对应Follower节点raftLog中最后一条Entry记录的索引)
func (pr *Progress) maybeDecrTo(rejected, last uint64) bool {
if pr.State == ProgressStateReplicate {
// the rejection must be stale if the progress has matched and "rejected"
// is smaller than "match".
if rejected <= pr.Match {
return false
}
// directly decrease next to match + 1
//根据前面对MsgApp消息发送过程的分析,处于ProgressStateReplicate状态时,发送MsgApp
//消息的同时会直接调用Progress.optimisticUpdate()方法增加Next,这就使得Next可能会
//比Match大很多,这里回退Next至Match位置,并在后面重新发送MsgApp消息进行尝试
pr.Next = pr.Match + 1
return true
}
// the rejection must be stale if "rejected" does not match next - 1
if pr.Next-1 != rejected {//出现过时的MsgAppResp消息直接忽略
return false
}
//根据MsgAppResp携带的信息重直Next
if pr.Next = min(rejected, last+1); pr.Next < 1 {
pr.Next = 1//将Next重直为l
}
//Next重置完成,恢复消息发送,并在后面重新发送MsgApp消息
pr.resume()
return true
}
另一个需要介绍的是inflights结构体,Progress.ins字段就指向了一个inflights实例,inflights的主要功能是记录当前节点己发出但未收到响应的MsgApp消息
在前面介绍MsgApp消息的发送过程时,提到过inflights.add()方法是用来记录发送出去的MsgApp消息
通过前面分析的内容可知,当Leader节点收到MsgAppResp消息时,会通过inflights.freeTo()方法将指定消息及其之前的消息全部清空,释放inflights 空间,让后面的消息继续发送。
7、MsgBeat消息和MsgCheckQuorum消息
Leader节点除了向集群中其他Follower节点发送MsgApp消息,还会向这些Follower节点发送MsgBeat消息。MsgBeat消息的主要作用是探活, 当Follower节点收到MsgBeat消息时会重置其选举计时器,从而防止Follower节点发起新一轮选举。
8、MsgHeartbeat消息和MsgHeartbeatResp消息
当集群中的Follower 节点收到Leader 节点发来的MsgHeartbeat 消息之后,也是通过raft.Step()方法调用raft.stepFollower()方法进行处理的,处理完成后会向Leader节点返回相应的MsgHearbeatResp消息作为响应
9、MsgProp消息
raft模块中,客户端发往到集群的写请求是通过MsgProp消息表示的。 在前面介绍Raft 协议时提到,Raft 集群中只有Leader节点能够响应客户端的写入请求。Leader节点对于MsgProp消息的主要处理是在raft.stepLeader()方法中实现的,相关的代码片段如下:
func stepLeader(r *raft, m pb.Message) {
// These message types do not require any progress for m.From.
switch m.Type {
case pb.MsgBeat:
case pb.MsgCheckQuorum:
case pb.MsgProp:
//这里重点介绍MsgProp消息相关代码片段,其他类型消息的处理片段暂时省略检测MsgProp消息是否携带了Entry记录,如果未携带,则输出异常日志并终止程序(略)
//检测当前节点是否被移出集群,如果当前节点以Leader状态被移出集群,则不再处理MsgProp消息(咯)检测当前是否正在进行Leader节点的转移,不再处理MsgProp消息(略)
if len(m.Entries) == 0 {
r.logger.Panicf("%x stepped empty MsgProp", r.id)
}
if _, ok := r.prs[r.id]; !ok {
// If we are not currently a member of the range (i.e. this node
// was removed from the configuration while serving as leader),
// drop any new proposals.
return
}
if r.leadTransferee != None {
r.logger.Debugf("%x [term %d] transfer leadership to %x is in progress; dropping proposal", r.id, r.Term, r.leadTransferee)
return
}
for i, e := range m.Entries {//边历MsgProp消息携带的全部Entry记录
if e.Type == pb.EntryConfChange {//如存在EntryConfChange类型的Entry记录,则将raft.pendingConf设立为true
if r.pendingConf {//如存在多条EntryConfChange类型的记录,则只保留第一条
r.logger.Infof("propose conf %s ignored since pending unapplied configuration", e.String())
m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
}
r.pendingConf = true
}
}
//将上述Entry记录追加到当前节点的raftLog中通过MsgApp消息向集群中其他节点复制Entry记录,bcastAppend( )方法在前面介绍过了
r.appendEntry(m.Entries...)
r.bcastAppend()
return
case pb.MsgReadIndex:
return
}
// All other message types require a progress for m.From (pr).
pr := r.getProgress(m.From)
if pr == nil {
r.logger.Debugf("%x no progress available for %x", r.id, m.From)
return
}
switch m.Type {
case pb.MsgAppResp:
case pb.MsgHeartbeatResp:
case pb.MsgSnapStatus:
case pb.MsgUnreachable:
case pb.MsgTransferLeader:
}
}
当集群中的Candidate 节点收到客户端发来的MsgProp 消息时, 会直接忽略该消息。当Follower 节点收到MsgProp 消息时,会将该MsgProp 消息转发给当前集群的Leader节点,stepFollower()方法中相关代码片段如下:
func stepFollower(r *raft, m pb.Message) {
switch m.Type {
case pb.MsgProp:
if r.lead == None {//当前集群中没有Leader节点, 则忽略该MsgProp消息
r.logger.Infof("%x no leader at term %d; dropping proposal", r.id, r.Term)
return
} else if r.disableProposalForwarding {
r.logger.Infof("%x not forwarding to leader %x at term %d; dropping proposal", r.id, r.lead, r.Term)
return
}
m.To = r.lead//将消息的To字段设立为当前Leader节点的id
r.send(m)//将MsgProp消息发送到当前的Leader节点,send()方法前面介绍过
}
}
10、MsgReadlndex消息和MsgReadlndexResp消息
介绍Raft协议时提到, 客户端的读请求需要读到集群中最新的、己提交的数据(即linearizability语义), 而不能读到老数据。在现实场景中也经常会遇到读多写少的情况,如果每次读请求都涉及多个节点的磁盘操作,则性能必然较差。Leader 节点保存了整个集群中最新的数据,如果只读请求只访问Leader节点,则Leader节点可以直接将结果返回给客户端,但是在网络分区的场景下,一个旧的Leader节点就可能返回旧数据。为此,raft模块使用MsgReadlndex消息来解决上述问题:当Leader节点收到客户端的只读请求时,会将当前请求的编号记录下来,在返回数据给客户端之前,Leader 节点需要先确定自己是否依然是当前集群的Leader 节点(通过心跳的方式〉,在确定其依然是Leader节点之后,就可以说明该节点可以响应该请求,只需要等待当前Leader 节点的提交位置(即raftLog.committed)到达或是超过只读请求的编号即可向客户端返回响应。在raft模块中,客户端发往集群的只读请求使用MsgReadlndex消息表示,其中只读请求有两种模式,分别是ReadOnlySafe和ReadOnly LeaseBased。 Leader节点对于MsgReadlndex消息的主要处理也是在raft.stepLeader()方法中实现的。集群中,Follower节点不能直接响应客户端的只读请求,而是转发给Leader节点进行处理,等待Leader节点响应之后,Follower节点才能响应Client.
11、MsgSnap消息
前面介绍的ra位sendAppend()方法可知,在Leader节点尝试向集群中的Follower节点发送MsgApp消息时,如果查找不到待发送的Entry记录(即该Follow节点对应的Progress.Next指定的Entry记录),则会尝试通过MsgSnap消息将快照数据发送到Follower节点, Follower节点之后会通过快照数据恢复其自身状态,从而可以与Leader节点进行正常的Entry记录复制。例如, 当Follower节点右机时间比较长, 就可能出现上述发送MsgSnap消息的场景。
func (r *raft) sendAppend(to uint64) {
pr := r.getProgress(to)
if pr.IsPaused() {
return
}
m := pb.Message{}
m.To = to
term, errt := r.raftLog.term(pr.Next - 1)
ents, erre := r.raftLog.entries(pr.Next, r.maxMsgSize)
if errt != nil || erre != nil { // send snapshot if we failed to get term or entries
if !pr.RecentActive {
r.logger.Debugf("ignore sending snapshot to %x since it is not recently active", to)
return
}
m.Type = pb.MsgSnap//将消息类型设直成MsgSnap,为后续发送快照数据做准备
snapshot, err := r.raftLog.snapshot()//获取快照数据
if err != nil {
if err == ErrSnapshotTemporarilyUnavailable {
r.logger.Debugf("%x failed to send snapshot to %x because snapshot is temporarily unavailable", r.id, to)
return
}
panic(err) // TODO(bdarnell)
}
if IsEmptySnap(snapshot) {
panic("need non-empty snapshot")
}
m.Snapshot = snapshot//设置MsgSnap消息的Snapshot字段
sindex, sterm := snapshot.Metadata.Index, snapshot.Metadata.Term//获取快照的相关信息
r.logger.Debugf("%x [firstindex: %d, commit: %d] sent snapshot[index: %d, term: %d] to %x [%s]",
r.id, r.raftLog.firstIndex(), r.raftLog.committed, sindex, sterm, to, pr)
//将目标Follower节点对应的Progress切换成ProgressStateSnapshot状态,其中会用Progress.PendingSnapshot字段记录快照数据的信息
pr.becomeSnapshot(sindex)
r.logger.Debugf("%x paused sending replication messages to %x [%s]", r.id, to, pr)
} else {
}
r.send(m)
}
下面转到Follower 节点,介绍其对MsgSnap 消息的处理,其相关的代码片段位于raftstepFollower()方法
func stepFollower(r *raft, m pb.Message) {
switch m.Type {
case pb.MsgProp:
case pb.MsgApp:
r.electionElapsed = 0
r.lead = m.From
r.handleAppendEntries(m)
case pb.MsgHeartbeat:
r.electionElapsed = 0
r.lead = m.From
r.handleHeartbeat(m)
case pb.MsgSnap:
r.electionElapsed = 0//重置raft.electionElapsed,防止发生选举
r.lead = m.From //设置Leader的id
r.handleSnapshot(m)//通过MsgSnap消息中的快照数据,重建当前节点的raftLog
}
在raft.handleSnapshot()方法中,会读取MsgSnap 消息中的快照数据,并重建当前节点的raftLog
func (r *raft) handleSnapshot(m pb.Message) {
//获取快照数据的元数据
sindex, sterm := m.Snapshot.Metadata.Index, m.Snapshot.Metadata.Term
if r.restore(m.Snapshot) {//返回值表示是否通过快照数据进行了重建
//向Leader节点返回MsgAppResp消息(Reject始终为false) 。 该MsgAppResp消息作为MsgSnap消息的响应,与前面介绍的MsgApp消息并无差别
r.logger.Infof("%x [commit: %d] restored snapshot [index: %d, term: %d]",
r.id, r.raftLog.committed, sindex, sterm)
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.lastIndex()})
} else {
r.logger.Infof("%x [commit: %d] ignored snapshot [index: %d, term: %d]",
r.id, r.raftLog.committed, sindex, sterm)
r.send(pb.Message{To: m.From, Type: pb.MsgAppResp, Index: r.raftLog.committed})
}
}
在raft.restore()方法中,会调用raftLog中相应的方法进行检测和重建,其具体实现如下
func (r *raft) restore(s pb.Snapshot) bool {
if s.Metadata.Index <= r.raftLog.committed {
return false
}
//根据快照数据的元数据查找匹自己的Entry记录,如采存在,则表示当前节点已经拥有了该快照中的全部数据,所以无须进行后续重建操作
if r.raftLog.matchTerm(s.Metadata.Index, s.Metadata.Term) {
r.logger.Infof("%x [commit: %d, lastindex: %d, lastterm: %d] fast-forwarded commit to snapshot [index: %d, term: %d]",
r.id, r.raftLog.committed, r.raftLog.lastIndex(), r.raftLog.lastTerm(), s.Metadata.Index, s.Metadata.Term)
//快照中全部的Entry记录都已经提交了,所以尝试修改当前节点的raftLog.committed,通过前面的介绍我们知道,raftLog.committed只增不减
r.raftLog.commitTo(s.Metadata.Index)
return false
}
// The normal peer can't become learner.
if !r.isLearner {
for _, id := range s.Metadata.ConfState.Learners {
if id == r.id {
r.logger.Errorf("%x can't become learner when restores snapshot [index: %d, term: %d]", r.id, s.Metadata.Index, s.Metadata.Term)
return false
}
}
}
r.logger.Infof("%x [commit: %d, lastindex: %d, lastterm: %d] starts to restore snapshot [index: %d, term: %d]",
r.id, r.raftLog.committed, r.raftLog.lastIndex(), r.raftLog.lastTerm(), s.Metadata.Index, s.Metadata.Term)
r.raftLog.restore(s)
//通过raftLog.unstable记录该快照数据,同时重豆相关字段
r.prs = make(map[uint64]*Progress)
r.learnerPrs = make(map[uint64]*Progress)
r.restoreNode(s.Metadata.ConfState.Nodes, false)
r.restoreNode(s.Metadata.ConfState.Learners, true)
return true
}