您提出的问题比与 etcd 相关的问题更多(etcd 实现了 raft,因此它们仍然是相关的)。要获得对 raft 算法的高级理解,我强烈建议您查看 raft webpage 和 raft paper(写得真好!)。我相信第 5.3 节“日志复制”会有所帮助。
首先让我们打个基础:Leader 跟踪与每个 Follower 匹配的条目。它将信息保存在论文中的nextIndex[] 和matchIndex[](见图2)和etcd 中的ProgressMap。
// ProgressMap is a map of *Progress.
type ProgressMap map[uint64]*Progress
type Progress struct {
Match, Next uint64
...
}
现在让我们跳到你的问题。
- 例如,leader 发出 {term:2,index:3} 然后发出 {term:2,index:4},大多数也按顺序响应。但是由于网络延迟,leader 接收到的响应乱序,首先接收到 {term:2,index:4} 的响应。 etcd 如何处理这种情况?好像只是忽略日志 {term:2,index:3},直接提交 {term:2,index:4}。
这一切都取决于追随者的状态(从领导者的角度来看)。让我们深入了解StateProbe 和StateReplicate。
在StateProbe 中,领导者试图找出应该发送给追随者的条目。它一次发送一条消息并等待响应(这可能是拒绝响应,在这种情况下,领导者必须减少与该关注者相关的Next 并重试)。在这种状态下,无法向同一个关注者发送 2 个不同的 MsgApp。
在StateReplicate 中,领导者假设网络稳定并(可能)发送许多MsgApp 消息。让我们举个例子。
Match := 2, Next := 2
关注日志:[E1, E2](E代表“入口”)
领队日志:[E1, E2]
在这个状态下,leader 收到对条目 E3、E4 和 E5 的 put 请求。让我们假设最大批量大小为 2,因此所有新条目都不能在单个消息中发送。领导者将发送 2 条消息:(Index: 3, Entries: [E3, E4]) 和 (Index: 5, Entries: [E5])。第二条消息将在获得第一个消息的 ack 之前发送。如果在图片中,follower 收到第一条消息,检查它是否可以使用来自请求的Index 附加它(检查在(raft).handleAppendEntries > (raftLog).maybeAppend > (raftLog).matchTerm > (raftLog).term 中执行),将条目附加到它的日志并发送 ack。稍后,follower 收到第二个请求并对其执行相同的操作(检查它是否可以附加它并发送 ack)。
follower 在发送 ack 之前检查它是否可以附加条目的事实在这里很重要。一旦领导者对任何消息进行确认,就可以确保直到Index + len(Entries) 的所有条目都填充在跟随者的日志中(否则该消息将被拒绝而不是被确认)。多亏了这一点,第一次确认是否延迟(甚至丢失)并不重要。
- 当响应数据包丢失时,etcd 如何重试(例如 {term:2,index:3} 的响应)?我在 etcd 项目中找不到任何代码 sn-p 来处理这个问题。
我现在将专注于 etcd,因为在 raft 论文中它被描述为“领导者无限期地重试 AppendEntries RPC”,这是相当不具建设性的。每隔一小段时间,leader 发送MsgHeartbeat 给follower,follower 回复MsgHeartbeatResp。作为MsgHeartbeatResp 处理的一部分,领导者会关注
if pr.Match < r.raftLog.lastIndex() {
r.sendAppend(m.From)
}
这应该读作:“如果有任何条目不存在于追随者身上,请将第一个缺失的条目发送给他”。这可以看作是重试机制,因为pr.Match 不会在没有跟随者 ack 的情况下增加。