[{"data":1,"prerenderedAt":367},["ShallowReactive",2],{"content:\u002Fnotes\u002F65840\u002Fraftlab2a":3,"surround:\u002Fnotes\u002F65840\u002Fraftlab2a":356},{"id":4,"title":5,"body":6,"categories":331,"date":333,"description":334,"draft":335,"extension":336,"image":337,"meta":338,"navigation":340,"path":341,"permalink":337,"published":337,"readingTime":342,"recommend":337,"references":337,"seo":347,"sitemap":348,"stem":349,"tags":350,"type":354,"__hash__":355},"content\u002Fposts\u002Fnotes\u002F65840\u002Fraftlab2a.md","6.5840 实验 2a —— Leader 选举",{"type":7,"value":8,"toc":319},"minimark",[9,13,17,20,23,26,41,51,54,57,68,71,78,89,92,96,99,109,112,123,126,132,135,141,147,153,156,159,165,168,181,184,190,193,196,202,205,208,215,221,224,230,233,239,246,252,255,261,264,267,270,273,276,283,289,292,298,301,307,313],[10,11,12],"h3",{"id":12},"前言",[14,15,16],"p",{},"6.824 的实验二，是实现 Raft 算法，在后续实验中的实现的分布式 KV 存储会将本实验实现的 Raft 算法作为分布式共识模块使用，所以实验二对后续实验至关重要。",[14,18,19],{},"实验二将整个 Raft 算法分为四个步骤，作为四个子实验去实现。实验 2a 只实现基本的 Leader 选举和心跳，来保证在各种极端（断线）场景下都可以正常地换届和选举。",[14,21,22],{},"Of course，2a 作为奠定整个四个子实验基础的起始实验，不仅仅需要实现 Leader 选举功能，更需要搭好整体的流程处理的框架。同样，我实现的是无锁版本，Raft 结构体里的 mu 变量可以删掉啦（癫狂",[10,24,25],{"id":25},"实验讲解",[14,27,28,29,35,36,40],{},"实验指导书在 ",[30,31,32],"a",{"href":32,"rel":33},"https:\u002F\u002Fpdos.csail.mit.edu\u002F6.824\u002Flabs\u002Flab-raft.html",[34],"nofollow","。和实验一不一样，这次几乎没有任何参考。我们需要实现的代码在 ",[37,38,39],"code",{"code":39},"src\u002Fraft\u002Fraft.go"," 中，这个 Raft 结构体只有一个很基础的结构体：",[42,43,49],"pre",{"className":44,"code":46,"language":47,"meta":48},[45],"language-go","type Raft struct {\n    peers []*labrpc.ClientEnd \u002F\u002F RPC end points of all peers\n    persister *Persister \u002F\u002F Object to hold this peer's persisted state\n    me int \u002F\u002F this peer's index into peers[]\n    dead int32 \u002F\u002F set by Kill()\n}\n","go","",[37,50,46],{"__ignoreMap":48},[14,52,53],{},"每一个 Raft 结构体都是集群中的一个 Server，Raft 结构体需要存储该 Server 所有需要的内容。",[14,55,56],{},"其中 peers 是当前配置集群的所有 server，ClientEnd 结构体可以通过调用 Call 直接发送 RPC 请求，me 则是当前机器在集群中的唯一标识，再其他机器上也是认这个 index 的。",[14,58,59,60,63,64,67],{},"lab2a 中 Raft 的入口是 ",[37,61,62],{"code":62},"Make()"," 方法，在 Make 方法初始化完成结构体后，会启动一个协程 ",[37,65,66],{"code":66},"rf.ticker()","，该协程会执行一个无限循环（其实是根据结束标识持续执行的循环，鉴于我们不关心机器被关闭后的事情，所以可以看作无限循环），这个方法可以看作主协程。",[14,69,70],{},"实验二最难的地方就在于，框架实现的内容太少了，我们基本需要从零实现整个 Raft 算法。好在，论文中的 Figure 2 基本已经给出了整体的实现思路。",[14,72,73,74,77],{},"另外，测试用例的实现在同文件夹下的 ",[37,75,76],{"code":76},"test_test.go"," 中，如果测试用例不通过，可以看一下测试用例的实现，根据测试场景来 debug。",[14,79,80,81,84,85,88],{},"lab 2d 的测试命令为 ",[37,82,83],{"code":83},"go test -run 2A","，建议使用 ",[37,86,87],{"code":87},"go test -race -run 2A"," 来同时检测数据竞争。",[10,90,91],{"id":91},"实验思路",[93,94,95],"h4",{"id":95},"整体流程",[14,97,98],{},"由于实现无锁版本，首先就需要仔细规划整体的处理流程和协程间通信，打好一个良好的基础，对后续实验也有很大的帮助，毕竟是一系列实验中的第一个。",[14,100,101,102,104,105,108],{},"首先约定主协程就是 ",[37,103,66],{"code":66}," 方法，只有这个方法可以修改 Raft 结构体中的字段，其他协程都不允许，这样就直接避免了数据竞争，所以 ",[37,106,107],{"code":107},"ticker()"," 方法中应当是无限循环监听一堆 channel 的消息。",[14,110,111],{},"那么具体有哪些协程需要通信，需要哪些管道呢？首先，选举一共涉及两种 RPC 请求：追加请求和拉票请求，当服务器作为这两种请求的接收端时，一定不是首先在主协程中接收 RPC 请求的，那么这两种 RPC 请求需要发给主协程处理，就需要两个管道；其次，如果机器作为发送端，发送请求一般是由非主协程操作的（不能让主协程等待 RPC 返回），那么在这两种 RPC 获取到响应时，需要提交给主协程处理，就又需要两个管道。",[14,113,114,115,118,119,122],{},"除此以外，还需要两个定时器，分别用于选举超时和心跳超时。实验指导中推荐是使用 ",[37,116,117],{"code":117},"time.Sleep()","，通过睡一段时间来实现定时。但是这种方式没有办法实现倒计时打断，所以，虽然指导书中不推荐 ",[37,120,121],{"code":121},"time.Timer","，但是叛逆为了实现倒计时打断重置，我还是使用了 Timer。不过，timer 用对真的挺不容易。",[14,124,125],{},"首先需要定义 Server 的状态，来标识 Server 的身份：",[42,127,130],{"className":128,"code":129,"language":47,"meta":48},[45],"type ServerStatus uint8\n \nconst (\n    Follower  ServerStatus = 0\n    Candidate ServerStatus = 1\n    Leader    ServerStatus = 2\n)\n",[37,131,129],{"__ignoreMap":48},[14,133,134],{},"对照 Raft 论文 Figure 2，定义一下 Server 的基础字段，并定义上面提到的几个管道和定时器：",[42,136,139],{"className":137,"code":138,"language":47,"meta":48},[45],"type Raft struct {\n    ...\n\n    \u002F\u002F Status\n    Status ServerStatus\n    \u002F\u002F 已提交日志，外部获取管道\n    ApplyCh chan ApplyMsg\n\n    \u002F***** 所有 Server 都包含的持久状态 *****\u002F\n    \u002F\u002F CurrentTerm 机器遇到的最大的任期，启动时初始化为 0，单调递增\n    CurrentTerm int\n    \u002F\u002F VotedFor 当前任期内投票的 Candidate ID，未投票则为 -1\n    VotedFor int\n    \u002F\u002F Logs 日志条目，每个条目都包含了一条状态机指令和 Leader 接收该条目时的任期，index 从 1 开始\n    Logs []*LogEntry\n\n    \u002F***** 所有 Server 都包含的可变状态 *****\u002F\n    \u002F\u002F CommitIndex 已知的最大的即将提交的日志索引，启动时初始化为 0，单调递增\n    CommitIndex uint64\n    \u002F\u002F LastApplied 最大的已提交的日志索引，启动时初始化为 0，单调递增\n    LastApplied uint64\n\n    \u002F******* Leader 包含的可变状态，选举后初始化 *******\u002F\n    \u002F\u002F NextIndex 每台机器下一个要发送的日志条目的索引，初始化为 Leader 最后一个日志索引 +1\n    NextIndex []uint64\n    \u002F\u002F MatchIndex 每台机器已知复制的最高的日志条目，初始化为 0，单调递增\n    MatchIndex []uint64\n\n    \u002F\u002F 定时器\n    electionTimer  *time.Timer\n    heartbeatTimer *time.Timer\n\n    \u002F\u002F 处理 rpc 请求的管道\n    requestVoteChan   chan RequestVoteMsg\n    appendEntriesChan chan AppendEntriesMsg\n}\n",[37,140,138],{"__ignoreMap":48},[14,142,143,144,146],{},"注意用到的管道和定时器等，都需要在 ",[37,145,62],{"code":62}," 函数中初始化，否则 nil 管道会阻塞所有的读写操作，并在函数返回前起一协程作为主协程，监听管道消息：",[42,148,151],{"className":149,"code":150,"language":47,"meta":48},[45],"func (rf *Raft) ticker() {\n    for !rf.killed() {\n        select {\n        case \u003C-rf.electionTimer.C:\n            rf.startElection()\n            resetTimer(rf.electionTimer, RandomizedElectionTimeout())\n        case \u003C-rf.heartbeatTimer.C:\n            rf.broadcastHeartbeat()\n            resetTimer(rf.heartbeatTimer, FixedHeartbeatTimeout())\n        case msg := \u003C-rf.requestVoteChan:\n            rf.handleRequestVote(msg)\n        case msg := \u003C-rf.appendEntriesChan:\n            rf.handleAppendEntries(msg)\n        case msg := \u003C-rf.requestVoteResChan:\n            rf.handleRequestVoteRes(msg)\n        case msg := \u003C-rf.appendEntriesResChan:\n            rf.handleAppendEntriesRes(msg)\n        }\n    }\n}\n",[37,152,150],{"__ignoreMap":48},[93,154,155],{"id":155},"两个定时器",[14,157,158],{},"electionTimer 是选举超时的定时器，每次需要初始化为一个随机时间，来防止启动时集群中的机器集体选举超时。这里随机时间范围为 300 ~ 450 ms。heartbeatTimer 是心跳超时的定时器，初始化为一个固定时间 100 ms。",[42,160,163],{"className":161,"code":162,"language":47,"meta":48},[45],"func Make(peers []*labrpc.ClientEnd, me int,\n    persister *Persister, applyCh chan ApplyMsg) *Raft {\n    ...\n    rf.electionTimer = time.NewTimer(RandomizedElectionTimeout())\n    rf.heartbeatTimer = time.NewTimer(FixedHeartbeatTimeout())\n    ...\n}\n\nfunc RandomizedElectionTimeout() time.Duration {\n    rand.Seed(time.Now().UnixNano())\n    return time.Duration(rand.Intn(150)+300) * time.Millisecond\n}\n\nfunc FixedHeartbeatTimeout() time.Duration {\n    return time.Millisecond * 100\n}\n",[37,164,162],{"__ignoreMap":48},[14,166,167],{},"选举超时定时器主要用于非 Leader，每次收到 Leader 的心跳后，Server 会重置选举定时器，然而在一段时间没有收到 Server 的消息，Server 就会发起选举。发起选举流程如下：",[169,170,171,175,178],"ol",{},[172,173,174],"li",{},"当前任期 +1",[172,176,177],{},"身份变为 Candidate，同时投票给自己",[172,179,180],{},"向所有机器发送拉票请求",[14,182,183],{},"实现如下：",[42,185,188],{"className":186,"code":187,"language":47,"meta":48},[45],"func (rf *Raft) startElection() {\n    if rf.Status == Leader {\n        \u002F\u002F leader 无需发起新选举\n        return\n    }\n    rf.CurrentTerm += 1\n    \u002F\u002F fmt.Printf(\"server %d start election for term %d\\n\", rf.me, rf.CurrentTerm)\n    rf.Status = Candidate\n    rf.VotedFor = rf.me\n    args := RequestVoteArgs{\n        Term:         rf.CurrentTerm,\n        CandidateId:  rf.me,\n        LastLogIndex: len(rf.Logs) - 1,\n    }\n    if len(rf.Logs) != 0 {\n        args.LastLogTerm = rf.Logs[len(rf.Logs)-1].Term\n    }\n    meta := ElectionMeta{\n        term: rf.CurrentTerm,\n        yeas: 1,\n        nays: 0,\n    }\n    for peer := range rf.peers {\n        if peer == rf.me {\n            continue\n        }\n        go rf.sendRequestVoteRoutine(peer, args, &meta)\n    }\n}\n",[37,189,187],{"__ignoreMap":48},[14,191,192],{},"构造了一个 ElectionMeta 来存储一次选举的元信息，包含这次选举的任期、投赞同和反对票的 Server 个数。由于不可能在主协程中等待各个机器投票完毕，便对集群中的每一台机器都开启了一个协程来管理拉票 RPC，这些 RPC 会在获知选举结果后通过管道通知主协程。另外在发起选举后，需要重置选举超时定时器。",[14,194,195],{},"心跳超时定时器主要用于 Leader，用来在集群中维系自己的 Leader 身份，每当心跳超时定时器超时，Leader 就会在集群中广播心跳，来保证不会有新的选举发起。同样，广播过后也需要重置心跳超时定时器。",[42,197,200],{"className":198,"code":199,"language":47,"meta":48},[45],"func (rf *Raft) broadcastHeartbeat() {\n    if rf.Status != Leader {\n        return\n    }\n    \u002F\u002F fmt.Printf(\"server %d broadcast heartbeat\\n\", rf.me)\n    args := AppendEntriesArgs{\n        Term:     rf.CurrentTerm,\n        LeaderID: rf.me,\n    }\n    for peer := range rf.peers {\n        if peer == rf.me {\n            continue\n        }\n        go rf.sendAppendEntriesRoutine(peer, args)\n    }\n}\n",[37,201,199],{"__ignoreMap":48},[14,203,204],{},"心跳 RPC 复用追加 RPC，同样，和集群中其他机器的 RPC 连接在单独的协程中处理。",[93,206,207],{"id":207},"拉票相关",[14,209,210,211,214],{},"在选举定时器超时后，非 Leader 的机器就会发起新的选举，来尝试选出新的 Leader。在上面 ",[37,212,213],{"code":213},"StartElection()"," 中，已经为每个机器开启了一个协程，用来管理对每个机器的拉票 RPC。发送拉票请求的协程函数如下：",[42,216,219],{"className":217,"code":218,"language":47,"meta":48},[45],"\u002F\u002F 发送拉票请求的协程\nfunc (rf *Raft) sendRequestVoteRoutine(peer int, args RequestVoteArgs, electionMeta *ElectionMeta) {\n    reply := RequestVoteReply{}\n    ok := rf.sendRequestVote(peer, &args, &reply)\n    if !ok {\n        return\n    }\n    msg := RequestVoteResMsg{\n        resp: &reply,\n        meta: electionMeta,\n    }\n    rf.requestVoteResChan \u003C- msg\n}\n",[37,220,218],{"__ignoreMap":48},[14,222,223],{},"没有特殊的处理，仅仅是发送 RPC 请求，并将请求结果包装后通过管道发送给主协程处理。这里定义 RPC 的请求和响应参数结构：",[42,225,228],{"className":226,"code":227,"language":47,"meta":48},[45],"\u002F\u002F 拉票 RPC 请求\ntype RequestVoteArgs struct {\n    \u002F\u002F Term Candidate 的任期\n    Term int\n    \u002F\u002F CandidateId 拉票的 Candidate 的 ID\n    CandidateId int\n    \u002F\u002F LastLogIndex Candidate 最后一条日志序列的索引\n    LastLogIndex int\n    \u002F\u002F LastLogTerm Candidate 最后一条日志序列的任期\n    LastLogTerm int64\n}\n\n\u002F\u002F 拉票 RPC 响应\ntype RequestVoteReply struct {\n    \u002F\u002F Term 当前任期\n    Term int\n    \u002F\u002F VoteGranted true 则拉票成功\n    VoteGranted bool\n}\n\n\u002F\u002F 拉票请求 RPC 发送入口\nfunc (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {\n    ok := rf.peers[server].Call(\"Raft.RequestVote\", args, reply)\n    return ok\n}\n",[37,229,227],{"__ignoreMap":48},[14,231,232],{},"接收端的 RPC 入口是 RequestVote 方法，由于接收 RPC 请求的协程不是主协程，这里仍然需要使用管道将拉票请求传递给主协程处理",[42,234,237],{"className":235,"code":236,"language":47,"meta":48},[45],"\u002F********* 拉票请求接收端相关方法 *********\u002F\n\u002F\u002F 拉票请求 RPC 接收入口\nfunc (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {\n    msg := RequestVoteMsg{\n        req: args,\n        ok:  make(chan RequestVoteReply),\n    }\n    rf.requestVoteChan \u003C- msg\n    resp := \u003C-msg.ok\n    *reply = resp\n}\n\n\u002F\u002F 主协程处理拉票请求\nfunc (rf *Raft) handleRequestVote(msg RequestVoteMsg) {\n    req := msg.req\n    if req.Term \u003C rf.CurrentTerm ||\n        (req.Term == rf.CurrentTerm && rf.VotedFor != -1 && rf.VotedFor != req.CandidateId) {\n        msg.ok \u003C- RequestVoteReply{\n            Term:        rf.CurrentTerm,\n            VoteGranted: false,\n        }\n        return\n    }\n    rf.rpcTermCheck(req.Term)\n    rf.VotedFor = req.CandidateId\n    resetTimer(rf.electionTimer, RandomizedElectionTimeout())\n    \u002F\u002F fmt.Printf(\"server %d vote for server %d for term %d\\n\", rf.me, msg.req.CandidateId, req.Term)\n    msg.ok \u003C- RequestVoteReply{\n        Term:        rf.CurrentTerm,\n        VoteGranted: true,\n    }\n}\n",[37,238,236],{"__ignoreMap":48},[14,240,241,242,245],{},"如果 Server 投了赞成票，还需要重置选举超时定时器。",[37,243,244],{"code":244},"rpcTermCheck()"," 是一个通用的，用于检查 rpc 请求或响应中的任期是否大于自身的任期，如果大于自身任期则需要更新任期并成为 Follower：",[42,247,250],{"className":248,"code":249,"language":47,"meta":48},[45],"\u002F\u002F 检查 rpc 请求响应中的 term，如果大于自己的则需要更新任期并成为 Follower\nfunc (rf *Raft) rpcTermCheck(msgTerm int) {\n    if rf.CurrentTerm \u003C msgTerm {\n        rf.CurrentTerm = msgTerm\n        rf.Status = Follower\n        rf.VotedFor = -1\n    }\n}\n",[37,251,249],{"__ignoreMap":48},[14,253,254],{},"在发起投票的协程获得了返回的投票结果后，将投票结果提交给主协程处理，在主协程中进行一些计票等判断。主协程处理投票结果如下",[42,256,259],{"className":257,"code":258,"language":47,"meta":48},[45],"\u002F\u002F 主协程处理拉票请求返回结果\nfunc (rf *Raft) handleRequestVoteRes(msg RequestVoteResMsg) {\n    meta := msg.meta\n    if rf.Status != Candidate {\n        return\n    }\n    if rf.CurrentTerm != meta.term {\n        return\n    }\n    if msg.resp.VoteGranted {\n        meta.yeas++\n        if meta.yeas > len(rf.peers)\u002F2 {\n            \u002F\u002F fmt.Printf(\"server %d become leader for term %d\\n\", rf.me, rf.CurrentTerm)\n            rf.Status = Leader\n            resetTimer(rf.heartbeatTimer, FixedHeartbeatTimeout())\n            rf.broadcastHeartbeat()\n        }\n    } else {\n        meta.nays++\n        rf.rpcTermCheck(msg.resp.Term)\n        if meta.nays > len(rf.peers)\u002F2 {\n            \u002F\u002F 反对票超过一半，则该任期选举失败；可以给其他机器投票\n            rf.VotedFor = -1\n        }\n    }\n}\n",[37,260,258],{"__ignoreMap":48},[14,262,263],{},"前置两个校验，如果当前 Server 的身份不是 Candidate，或者 Server 的任期和投票的任期不一致，就说明是一场过期的投票，无需处理，直接返回即可。",[14,265,266],{},"如果投的是赞成票，那么就计算一下当前赞成票数是否已大于一半，如果已经大于一半，说明选举成功，发起选举的 Server 转变为 Leader，并重置心跳定时器，向所有机器广播心跳来声明自己的 Leader 身份。",[14,268,269],{},"如果投的是反对票，这里算是个小优化。可以直接校验反对票是否已经超过一半，如果超过一半，那么可以认为该次发起的选举已经失败。可以将本任期内的投票置为 -1，以给其他潜在的 Candidate 投票来加速 Leader 选举。",[93,271,272],{"id":272},"追加相关",[14,274,275],{},"追加相关的内容，本次实验只需要实现心跳的处理即可。更具体的追加在实验 2b 中。",[14,277,278,279,282],{},"在 ",[37,280,281],{"code":281},"broadcastHeartbeat()"," 函数中，对集群中的全部机器广播心跳，使用的就是追加请求。对于每个机器都有一协程来管理与之通信的追加请求。追加协程的函数如下",[42,284,287],{"className":285,"code":286,"language":47,"meta":48},[45],"\u002F\u002F 发送追加请求的协程\nfunc (rf *Raft) sendAppendEntriesRoutine(peer int, args AppendEntriesArgs) {\n    reply := AppendEntriesReply{}\n    ok := rf.sendAppendEntries(peer, &args, &reply)\n    if !ok {\n        return\n    }\n    rf.appendEntriesResChan \u003C- AppendEntriesResMsg{\n        resp: &reply,\n    }\n}\n",[37,288,286],{"__ignoreMap":48},[14,290,291],{},"与上面的发起选举类似，也是直接发送一个 RPC 请求，并等待响应，将响应通过管道交由主协程处理。追加 RPC 的请求与响应定义如下",[42,293,296],{"className":294,"code":295,"language":47,"meta":48},[45],"\u002F\u002F 追加 RPC 请求\ntype AppendEntriesArgs struct {\n    \u002F\u002F Term Leader 的任期\n    Term int\n    \u002F\u002F LeaderID Follower 可以将客户端请求重定向到 Leader\n    LeaderID int\n    \u002F\u002F PrevLogIndex 新日志条目前一个日志条目的日志索引\n    PrevLogIndex int\n    \u002F\u002F PrevLogTerm 前一个日志条目的任期\n    PrevLogTerm int\n    \u002F\u002F Entries 需要保存的日志条目，心跳包为空\n    Entries []*LogEntry\n    \u002F\u002F LeaderCommit Leader 的 CommitIndex\n    LeaderCommit int\n}\n\n\u002F\u002F 追加 RPC 响应\ntype AppendEntriesReply struct {\n    \u002F\u002F Term Follower 当前任期\n    Term int\n    \u002F\u002F Success Follower 包含 PrevLogIndex 和 PrevLogTerm 的日志条目为 true\n    Success bool\n}\n\n\u002F\u002F 追加请求 RPC 发送入口\nfunc (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {\n    ok := rf.peers[server].Call(\"Raft.AppendEntries\", args, reply)\n    return ok\n}\n",[37,297,295],{"__ignoreMap":48},[14,299,300],{},"接收方在收到追加 RPC 请求后，将请求交由主协程处理，当前主协程也只需要处理心跳场景，即转变为 Follower，重置选举超时定时器，并判断是否需要更新任期：",[42,302,305],{"className":303,"code":304,"language":47,"meta":48},[45],"\u002F********* 追加请求接收端相关方法 *********\u002F\n\u002F\u002F 追加请求 RPC 接收入口\nfunc (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {\n    msg := AppendEntriesMsg{\n        req: args,\n        ok:  make(chan AppendEntriesReply),\n    }\n    rf.appendEntriesChan \u003C- msg\n    resp := \u003C-msg.ok\n    *reply = resp\n}\n\n\u002F\u002F 主协程处理追加请求\nfunc (rf *Raft) handleAppendEntries(msg AppendEntriesMsg) {\n    rf.Status = Follower\n    resetTimer(rf.electionTimer, RandomizedElectionTimeout())\n    rf.rpcTermCheck(msg.req.Term)\n    msg.ok \u003C- AppendEntriesReply{\n        Term: rf.CurrentTerm,\n    }\n}\n",[37,306,304],{"__ignoreMap":48},[14,308,309,310,312],{},"最后发送端处理响应，只需要检查任期即可，仍可复用 ",[37,311,244],{"code":244},"：",[42,314,317],{"className":315,"code":316,"language":47,"meta":48},[45],"\u002F\u002F 主协程处理追加请求返回结果\nfunc (rf *Raft) handleAppendEntriesRes(msg AppendEntriesResMsg) {\n    resp := msg.resp\n    rf.rpcTermCheck(resp.Term)\n}\n",[37,318,316],{"__ignoreMap":48},{"title":48,"searchDepth":320,"depth":320,"links":321},4,[322,324,325],{"id":12,"depth":323,"text":12},3,{"id":25,"depth":323,"text":25},{"id":91,"depth":323,"text":91,"children":326},[327,328,329,330],{"id":95,"depth":320,"text":95},{"id":155,"depth":320,"text":155},{"id":207,"depth":320,"text":207},{"id":272,"depth":320,"text":272},[332],"笔记","2022-12-16 02:06:10","实验 2a 专注于实现 Raft 算法中的 Leader 选举和心跳机制，旨在确保在各种极端情况下的正常换届和选举。这一实验是后续分布式KV存储实现的基础，包含了四个步骤，通过无锁版本的设计简化了 Raft 结构体的复杂性。实验指导书提供了必要的背景资料，但与前一实验相比，本次几乎不依赖参考资料，强调了独立实现的重要性。",false,"md",null,{"slots":339},{},true,"\u002Fnotes\u002F65840\u002Fraftlab2a",{"text":343,"minutes":344,"time":345,"words":346},"18 min read",17.75,1065000,3550,{"title":5,"description":334},{"loc":341},"posts\u002Fnotes\u002F65840\u002Fraftlab2a",[351,352,353],"raft","6.5840","6.824","tech","zGb_p3tybo_t5OYawF94aVAGXhne-SMleX2fMURWHZc",[357,362],{"title":358,"path":359,"stem":360,"date":361,"type":354,"children":-1},"Raft 论文阅读","\u002Fnotes\u002F65840\u002Freftextendedpaper","posts\u002Fnotes\u002F65840\u002Freftextendedpaper","2022-12-03 21:40:09",{"title":363,"path":364,"stem":365,"date":366,"type":354,"children":-1},"《三体》中的“大多数人”","\u002Fdaily\u002Fpeople-in-three-body","posts\u002Fdaily\u002Fpeople-in-three-body","2022-04-11 00:13:13",1787554445094]