本文将来深度解析ETCD的领导者在处理消息类型为MsgProp的具体逻辑,日志提议的处理过程中主要涉及到如下几个部分的逻辑:
1、验证提议是否有效:确保日志条目不为空、节点是集群的一部分,并且领导权转移没有进行中。
2、处理配置变更:如果提议包含配置变更,必须确保当前集群状态可以接受该变更。
3、追加日志条目:将日志条目追加到 Raft 日志中,若失败则丢弃提议。
4、广播日志条目:向集群中的其他节点广播日志条目,以实现日志复制。
代码解析:
1. 处理空日志条目
if len(m.Entries) == 0 {
r.logger.Panicf("%x stepped empty MsgProp", r.id)
}
- 目的:此检查确保接收到的日志条目不为空。如果
m.Entries
为空,则会触发Panicf
,并打印出日志。 - 背景:
MsgProp
消息应包含至少一个日志条目(通常是用户提交的操作),如果为空,意味着客户端没有提交有效的日志,或有潜在错误。
2. 检查节点是否在 Raft 集群中
if r.prs.Progress[r.id] == nil {
// 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 ErrProposalDropped
}
- 目的:检查领导者是否仍然是集群中的成员。Raft 协议中,节点可能会被从集群配置中删除。
- 解析:
r.prs.Progress[r.id]
表示当前节点在ProgressTracker
中的状态,如果该节点不再是集群成员(可能由于配置变更),则会拒绝任何新的提议。返回ErrProposalDropped
表示提议被丢弃。
3. 检查领导转移
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 ErrProposalDropped
}
- 目的:检查领导者是否正在进行领导权转移。如果是,则拒绝新的日志提议。
- 背景:Raft 协议允许在领导者之间转移控制,如果领导权转移正在进行中,新的日志提议会被丢弃以避免在转移期间的操作冲突。
4. 处理日志条目中的配置变更
for i := range m.Entries {
e := &m.Entries[i]
var cc pb.ConfChangeI
if e.Type == pb.EntryConfChange {
var ccc pb.ConfChange
if err := ccc.Unmarshal(e.Data); err != nil {
panic(err)
}
cc = ccc
} else if e.Type == pb.EntryConfChangeV2 {
var ccc pb.ConfChangeV2
if err := ccc.Unmarshal(e.Data); err != nil {
panic(err)
}
cc = ccc
}
- 目的:处理日志条目类型,如果条目是配置变更(
EntryConfChange
或EntryConfChangeV2
),则解析该条目的配置变更数据。 - 解析:
e.Type == pb.EntryConfChange
和e.Type == pb.EntryConfChangeV2
分别对应两种类型的配置变更条目。pb.ConfChange
和pb.ConfChangeV2
用于表示配置变更的具体数据。Unmarshal
方法将字节数据转换为结构体类型,以便后续处理。
5. 检查配置变更的条件
if cc != nil {
alreadyPending := r.pendingConfIndex > r.raftLog.applied
alreadyJoint := len(r.prs.Config.Voters[1]) > 0
wantsLeaveJoint := len(cc.AsV2().Changes) == 0
var refused string
if alreadyPending {
refused = fmt.Sprintf("possible unapplied conf change at index %d (applied to %d)", r.pendingConfIndex, r.raftLog.applied)
} else if alreadyJoint && !wantsLeaveJoint {
refused = "must transition out of joint config first"
} else if !alreadyJoint && wantsLeaveJoint {
refused = "not in joint state; refusing empty conf change"
}
if refused != "" {
r.logger.Infof("%x ignoring conf change %v at config %s: %s", r.id, cc, r.prs.Config, refused)
m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
} else {
r.pendingConfIndex = r.raftLog.lastIndex() + uint64(i) + 1
}
}
- 目的:对配置变更进行一些检查,确保当前集群状态可以接受该变更。
- 具体解析:
alreadyPending
:检查是否存在尚未应用的配置变更。alreadyJoint
:检查当前是否处于联合配置状态(即配置变更过程中)。wantsLeaveJoint
:检查配置变更是否要求离开联合配置状态。- 如果上述条件不符合,日志条目会被忽略,类型会被重设为
EntryNormal
。 r.pendingConfIndex
更新为日志的最后索引,以标记配置变更的提交位置。
6. 提交日志条目
if !r.appendEntry(m.Entries...) {
return ErrProposalDropped
}
- 目的:将接收到的日志条目追加到 Raft 日志中。如果追加失败,则返回
ErrProposalDropped
。 - 背景:这是 Raft 协议中的标准操作,领导者会将客户端请求作为日志条目追加到本地日志中。若日志追加失败,则表示提议无法处理。
7. 广播日志条目
r.bcastAppend()
- 目的:广播追加的日志条目到集群中的其他节点,确保所有节点都能看到并处理这些日志。
- 背景:在 Raft 协议中,领导者需要向所有跟随者发送日志条目,以保证一致性和日志复制的可靠性。
8. 返回
return nil
- 目的:成功处理提议后返回
nil
,表示没有错误。
总结:
这段代码实现了 ETCD 中 Raft 协议领导者节点处理客户端日志提议的核心逻辑。具体步骤包括:
- 验证提议是否有效:确保日志条目不为空、节点是集群的一部分,并且领导权转移没有进行中。
- 处理配置变更:如果提议包含配置变更,必须确保当前集群状态可以接受该变更。
- 追加日志条目:将日志条目追加到 Raft 日志中,若失败则丢弃提议。
- 广播日志条目:向集群中的其他节点广播日志条目,以实现日志复制。
这些操作确保了 Raft 协议的日志一致性和集群状态的稳定性。