MIT 6.824 lab3B/C

2 阅读13分钟

前言

花两天把lab3B/C写了一下,有了A的基础,简单了不少。gitee地址放在末尾。

一、3B/3C 前的整体认知

1.1 3B 的目标

  • Leader 接收 Start(command) → 追加到 rf.logs → 复制到多数派 → 推进 commitIndex → 通过 applyChan 交给状态机。

1.2 3C 的目标

  • term / votedFor / logs 做到“随时可重启且不违背承诺”(同一任期不可能投两次票、日志不可能倒退)。

二、Part B:日志复制(Log Replication,3B)

2.1 整体思路:把 Raft 看成一条闭环流水线

       主要流程:Start 写入 → AppendEntries 复制 → reply 反馈 → Leader 推进 commit → 心跳携带 LeaderCommit → 双端 applier 应用

  • 客户端 → Leader:Start
  • Leader → Followers:AppendEntries(entries, LeaderCommit)
  • Followers → Leader:AppendEntriesReply(Success/Term)
  • Leader:更新 nextIndex/matchIndex → updateCommitIndex → 唤醒 applier
  • Followers:更新 commitIndex → 唤醒 applier

2.2 关键数据结构

  • 持久化状态(3C 才真正持久化)rf.termrf.votedForrf.logs
  • 易失状态(所有节点)rf.commitIndexrf.lastApplied
  • 易失状态(Leader 专用)rf.nextIndex[]rf.matchIndex[]

日志索引约定:  

  • rf.logs[0] 为占位条目(term=0, command=nil)  不仅能够对日志进行更方便的访问,在3D中这里放快照的最后一个日志信息
  • 第一条真实日志在 index=1  
  • 最后一条 index = len(rf.logs)-1

2.3 Start(command):

写这一段时建议按(接口语义 + 并发点 + 返回值)组织:

  • 语义:只有 rf.state==Leader 才能接收命令;否则立即返回 (-1, rf.term, false)
  • 写入:追加 LogEntry{Term: rf.term, Command: command}rf.logs
  • 返回(index, term, true)
  • 触发复制:两种常见方式

          - 触发一次广播(推模式),或

          - 等下一次心跳(拉模式,简单但可能慢)

func (rf *Raft) Start(command interface{}) (int, int, bool) {
	// Your code here (3B).
	rf.mu.Lock()
	if rf.Killed() {
		rf.mu.Unlock()
		return -1, -1, false
	}
	if rf.state != Leader {
		rf.mu.Unlock()
		return -1, -1, false
	}
	term := rf.currentTerm
	index := rf.getLastIndex() + 1
	rf.logs = append(rf.logs, LogEntry{Term: term, Command: command})
	rf.persist()
	rf.mu.Unlock()
	//Leader 刚追加了一条日志,立刻再推一轮 RPC,不用干等下面 ticker 的 50ms 心跳周期,复制会快一点
	go rf.broadcastHeartbeat()//广播心跳
	return index, term, true
}

2.4 AppendEntries(Follower 侧):日志一致性检查与截断追加

这一节建议用“检查顺序”写,跟 Figure 2/论文一致,最好对着一步步走:

  1. term 检查args.Term < rf.term 直接拒绝,回 reply.Term = rf.term
  2. 退位与重置计时器:收到合法 Leader 的 RPC → lastHeartBeatTime = now,必要时退回 Follower
  3. 一致性检查:用 PrevLogIndex/PrevLogTerm 验证日志前缀一致

           - 本地太短(PrevLogIndex 超界)→ Success=false

           - term 不匹配 → Success=false

  1. 截断并追加:从 PrevLogIndex+1 开始,删除本地冲突后缀,再追加 Entries
  2. 推进 commitIndexcommitIndex = min(args.LeaderCommit, lastNewIndex)
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	DPrintf("[%d] AppendEntries from L%d term=%d, my term=%d state=%d", rf.me, args.LeaderId, args.Term, rf.currentTerm, rf.state)
	reply.Success = false
	reply.Term = rf.currentTerm

	// 发现任期比自己小
	if args.Term < rf.currentTerm {
		DPrintf("[%d] AppendEntries: 拒绝 term 更小 leader=%d argsTerm=%d myTerm=%d", rf.me, args.LeaderId, args.Term, rf.currentTerm)
		return
	}

	// 发现更高任期或来自合法 Leader 的心跳
	rf.lastHeartBeatTime = time.Now()
	if args.Term > rf.currentTerm {
		rf.currentTerm = args.Term
		rf.votedFor = -1
		rf.persist()
		DPrintf("[%d] step down to Follower due to AE term=%d", rf.me, rf.currentTerm)
	}
	rf.state = Follower

	//一致性检查
	//日志长度不够 Follower 若还没有 PrevLogIndex 这一条,才拒绝
	if rf.getLastIndex() < args.PrevLogIndex { // 3C
		reply.ConflictTerm = -1 // 3C
		reply.ConflictIndex = rf.getLastIndex() + 1 // 3C
		return // 3C
	}

	//一致性检查:PrevLogIndex 处的 Term 是否匹配
	// 特别注意:如果 PrevLogIndex 恰好在快照边界,要用 LastIncludedIndex
	if args.PrevLogIndex < rf.LastIncludedIndex { // 3C
		reply.ConflictTerm = -1 // 3C
		reply.ConflictIndex = rf.LastIncludedIndex + 1 // 3C
		return // 3C
	}

	//PrevLogIndex处任期不匹配
	if rf.getTermByIndex(args.PrevLogIndex) != args.PrevLogTerm { // 3C
		reply.ConflictTerm = rf.getTermByIndex(args.PrevLogIndex) // 3C
		idx := args.PrevLogIndex // 3C
		//从最新的日志位置开始向前找,找到冲突任期的下标
		//告诉leader,这个下标是冲突任期的下标,下一步继续找冲突位置,若没有则进行同步
		for idx > rf.LastIncludedIndex && rf.getTermByIndex(idx) == reply.ConflictTerm { // 3C
			idx-- // 3C
		}
		reply.ConflictIndex = idx + 1 // 3C
		return // 3C
	}

	//追加日志
	isChange := false // 3D
	for i, entry := range args.Entries { // 3D
		logicIdx := i + args.PrevLogIndex + 1 // 3D
		// 如果 logicIdx 已经落入快照范围,跳过(或者报错,理论上不该发生)
		if logicIdx <= rf.LastIncludedIndex { // 3D
			continue // 3D
		}
		phyIdx := rf.getPhysicIdx(logicIdx) // 3D
		if phyIdx < len(rf.logs) { // 3D
			//如果索引范围内已经有日志了,检查任期
			if rf.logs[phyIdx].Term != entry.Term { // 3D
				//如果追加日志的位置的任期和leader日志的位置的任期不相等
				//将idx下标前面的日志进行切片保留
				rf.logs = rf.logs[:phyIdx] // 3D
				rf.logs = append(rf.logs, entry) // 3D
				isChange = true // 3D
			}
			//如果任期一样,说明这一段已经同步过了,下一条
		} else { // 3D
			//超出本地的日志长度,直接追加
			rf.logs = append(rf.logs, entry) // 3D
			isChange = true // 3D
		}
	}
	if isChange { // 3D
		rf.persist() // 3D
	}

	// 更新 CommitIndex:须用「当前日志最后一条」与 LeaderCommit 取 min。
	// 心跳时 len(Entries)==0,若仍用 PrevLogIndex+0 会小于 getLastIndex(),导致 commit 永远追不上 Leader。
	if args.LeaderCommit > rf.commitIndex {
		rf.commitIndex = min(args.LeaderCommit, rf.getLastIndex())
		rf.applyCond.Broadcast()
	}
	reply.Success = true
}

2.5 Leader 侧复制:nextIndex / matchIndex 维护

这一部分主要分为两个小点:

  • 发送时构造 args(按 peer 单独算)

          - prevIdx := nextIndex[i]-1

          - entries := logs[nextIndex[i]:] 将日志切片

          - 心跳就是 entries 为空,但仍携带 LeaderCommit

  • 收到回复时更新
  • 新增关键成员ConflictTerm (冲突任期)/ ConflictIndex(冲突任期索引)

          - Success=true:更新 matchIndex[i]nextIndex[i]

          - Success=false:回退 nextIndex[i] 并重试

          - 优化(性能关键):ConflictTerm / ConflictIndex,让回退“整块跳过”

func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
	ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
	if ok {
		rf.mu.Lock()
		defer rf.mu.Unlock()
		// 处理回复(任期更新、调整 nextIndex 等)
		if reply.Term > rf.currentTerm {
			rf.currentTerm = reply.Term
			rf.state = Follower
			rf.votedFor = -1
			rf.persist()
			return ok
		}
		// 2. 状态检查
		if rf.state != Leader || rf.currentTerm != args.Term {
			return ok
		}

		// 3B 以后你会在这里处理日志同步的 reply.Success 为 false 的情况
		// 如果追加日志成功 —— 对应论文 Leader 规则:If successful, update matchIndex and nextIndex
		if reply.Success {
			newMathIdx := args.PrevLogIndex + len(args.Entries)
			if newMathIdx > rf.matchIndex[server] {
				rf.matchIndex[server] = newMathIdx
			}
			rf.nextIndex[server] = rf.matchIndex[server] + 1

			// 更新提交的日志
			rf.updateCommitIndex()
		} else {
			// 如果失败,根据 reply.ConflictIndex 实现快速跳转 —— 对应论文:If AppendEntries fails because of log inconsistency, decrement nextIndex and retry
			if reply.ConflictTerm == -1 {
				// 日志过短
				rf.nextIndex[server] = reply.ConflictIndex
			} else {
				flag := false
				for i := args.PrevLogIndex; i >= rf.LastIncludedIndex; i-- {
					if rf.getTermByIndex(i) == reply.ConflictTerm {
						flag = true
						rf.nextIndex[server] = i + 1
						break
					}
				}
				if !flag {
					rf.nextIndex[server] = reply.ConflictIndex
				}
			}
		}
	}
	return ok
}

2.6 updateCommitIndex:Leader 如何合法推进提交

这节写清楚 Figure 8 约束(非常重要):

  • Leader 推进到某个 index N 的条件:

          - 多数派 matchIndex[i] >= N

          - 且 logs[N].Term == rf.term(只用当前任期的日志推进提交,间接提交旧任期日志)

推荐这样写:

  • 从后往前找最大的 N(更快)

  • 注意边界:len(logs)-1len(logs) 

  • // 日志提交应用-leader专属(matchIndex/commitIndex 均为逻辑索引)
    func (rf *Raft) updateCommitIndex() {
    	if rf.state != Leader {
    		return
    	}
    	for i := rf.getLastIndex(); i > rf.commitIndex; i-- {
    		if rf.getTermByIndex(i) == rf.currentTerm {
    			cnt := 1
    			for j := range rf.peers {
    				if j != rf.me && rf.matchIndex[j] >= i {
    					cnt++
    				}
    			}
    			if cnt >= len(rf.peers)/2+1 {
    				rf.commitIndex = i
    				rf.applyCond.Broadcast()
    				break
    			}
    		} else if rf.getTermByIndex(i) < rf.currentTerm {
    			break
    		}
    	}
    }
    

2.7 applier:把已提交日志送到 applyChan(不要持锁发 chan)

在这部分中需要特别注意的是:

  • 不要用 sleep 轮询:延迟大、测试反馈慢
  • 用 sync.Cond:commitIndex 增加时 Broadcast 唤醒 applier
  • 锁内拷贝、锁外发送:避免 applyChan 阻塞时卡死整个 Raft

来看这个简短伪代码流程:

  • lock → 等待 commitIndex > lastApplied(cond.Wait)
  • 计算要 apply 的区间,拷贝 entries
  • unlock → 逐条向 applyChan 发送
  • lock → 更新 lastApplied → unlock

2.8 3B 典型 Bug 记录

我按表格整理了一下常见的坑,我基本都是踩过的:

问题分类具体描述
Agreement Failed(commitIndex 没推进 / applier 没被唤醒)就是集群明明已经达成一致了,但 commitIndex 死活不涨,或者涨了却没人去 apply。要么是 Leader 忘了给 Follower 发心跳,要么是 Follower 收到了心跳却没更新自己的 commitIndex,再要么是 applyLoop 没被唤醒,卡在 channel 上没人通知。
too many RPCs(心跳过频,缺少节流)每个日志条目都单独发一次 RPC,或者心跳发得太快,把网络打满。正确做法是用 lastBroadcastTime 做个节流,比如 10ms 内只发一次心跳,没必要每条日志都立刻广播。
回退太慢(未实现 conflict 优化)日志冲突之后,如果只靠 nextIndex-- 一格格往回退,遇到恶意日志会退几百次,导致同步极慢。用论文里的 conflict term 和 conflict index 做快速回退,一次性跳到冲突点,性能好很多。
死锁(持锁发 chan / 持锁做重活)最常见的是 持有锁然后向 applyCh 发消息,如果对端处理慢或者也来拿同一把锁,就死锁了。还有就是 持有锁做耗时的磁盘写入,虽然不死锁,但会卡住整个 Raft 状态机。解决方法是 先拷贝需要的数据,解锁,再发 chan 或写盘

这些 Bug 在 3B 阶段几乎每个人都会遇到,提前有个印象能省不少调试时间。

三、Part C:持久化(Persistence,3C)

3.1 3C 的核心:持久化“承诺”,不是持久化“结果”

一句话:只要重启可能导致你做出与重启前矛盾的行为,就必须在返回 RPC 之前把状态写盘,其实很简单,只要将需要持久化的内容每次改动的时候进行持久化就可以了。

3.2 persist/readPersist:持久化内容与编码格式

持久化内容(Figure 2):  

  • rf.term  
  • rf.votedFor  
  • rf.logs  

编码顺序必须和解码顺序一致,否则 readPersist (会失忆/乱序),这里也是一个坑,但是我这里为了方便怎么存的怎么取,所以把坑跳过去了。  

func (rf *Raft) persist() {
	// Your code here (3C).
	// Example:
	// w := new(bytes.Buffer)
	// e := labgob.NewEncoder(w)
	// e.Encode(rf.xxx)
	// e.Encode(rf.yyy)
	// raftstate := w.Bytes()
	// rf.persister.Save(raftstate, nil)
	w := new(bytes.Buffer)
	e := labgob.NewEncoder(w)
	e.Encode(rf.currentTerm)
	e.Encode(rf.votedFor)
	e.Encode(rf.logs)
	e.Encode(rf.LastIncludedIndex)
	e.Encode(rf.LastIncludedTerm)
	raftstate := w.Bytes()
	snap := rf.persister.ReadSnapshot()//先读取快照
	rf.persister.Save(raftstate, snap)//再持久化状态和快照

}
// restore previously persisted state.
func (rf *Raft) readPersist(data []byte) {
	if data == nil || len(data) < 1 { // bootstrap without any state?
		return
	}
	// Your code here (3C).
	// Example:
	r := bytes.NewBuffer(data)
	d := labgob.NewDecoder(r)
	var term int
	var vorfor int
	var logs []LogEntry
	var lastincludedindex int
	var lastincludedterm int
	if d.Decode(&term) != nil ||
		d.Decode(&vorfor) != nil ||
		d.Decode(&logs) != nil ||
		d.Decode(&lastincludedindex) != nil ||
		d.Decode(&lastincludedterm) != nil {
		DPrintf("readPersist err")
	} else {
		rf.currentTerm = term
		rf.votedFor = vorfor
		rf.logs = logs
		rf.LastIncludedIndex = lastincludedindex
		rf.LastIncludedTerm = lastincludedterm
	}

}

3.3 何时调用 persist:时机与原子性

可以按谁改了谁负责 persist写成清单:

  • startElectionterm++votedFor=me 之后
  • RequestVote:更新 term / votedFor 之后(尤其是投票成功路径)
  • AppendEntries:看到更大 term 退位时;日志截断/追加时
  • Start:追加日志后 

关键点:一定要在 handler 返回 reply 之前 persist(否则崩溃重启后可能同任期重复投票,造成脑裂等一系列问题)。  

3.4 为什么 3B 正常、3C 就崩

  • 3B 都过了,3C 断电测试出现 Log Inconsistency
  • 根因通常是:readPersist 没写 / 解码顺序错 / logs 没恢复占位条目
  • commitIndex/lastApplied/nextIndex/matchIndex 不需要持久化,它们会在重启后通过 Leader 的 LeaderCommit、以及日志复制流程动态恢复。

3.5 性能与正确性的取舍

  • persist 频率高会拖慢系统(尤其心跳很频繁时)
  • 心跳频率要“优雅”:足够快维持领导权,但不能淹没网络(too many RPCs)
  • 正确性优先:该 persist 的地方不能省;优化要在正确性上做节流/批量/减少无效 RPC

四、测试和遇到的问题

cd 6.5840/src
make RUN="-run 3B" raft1
make RUN="-run 3C" raft1

  •  3B 阶段主要折腾的是 Raft 里两个搬运者:谁在什么时候推进提交、谁把已提交日志交给状态机。我对这两块做了比较彻底的重构。

    updateCommitIndex(Leader 侧)

  • 问题:一开始有数组越界(len 和 len-1 混用)、以及用「日志下标」去当 matchIndex 的下标这类逻辑错误。

  • 做法:改成从后往前扫合法的提交点 NN,并严格按 Figure 8:Leader 只能用「当前任期里复制到多数派的那条日志」去推进 commitIndex,旧任期的条目只能被顺带提交。

  • Figure 2 流水线(自测时心里要有这条链)
    Start 写日志 → Leader 发 AppendEntries → Follower 确认并追加 → Leader 用 matchIndex 推进 commitIndex 并唤醒 applier → 后续心跳带上 LeaderCommit,Follower 跟进 commitIndex 并同样唤醒 applier → 双方经 applyChan 按序应用。

    • 实现中需要注意的点:

    • 尽量不要在持锁时写 channel。

    • 等 commit 用 sync.Cond,少用忙等 / 固定 Sleep

    • 心跳:够快以维持领导权,但不要打到 RPC 风暴。

    • readPersist

    • 这是 3C 里很隐蔽的一类问题:没实现、或 Decode 顺序和 Encode 对不上,节点等于「失忆」,往往在断电测试里表现为大量 Log Inconsistency,而纯 3B 有时还能蒙混过关。

    • 性能和正确性

    • HeartBeatTimeout、RPC 频率和 persist 次数会互相拉扯;变了就存最安全,但心跳极密时无脑 persist 会把测试拖慢。可以在保证该存必存的前提下,适当拉长心跳间隔、减少无意义刷盘(具体以你当时通过测试的配置为准)。

    • 哪些不用存

    • commitIndex 等易失状态不必持久化;重启后靠 Leader 的 LeaderCommit 和日志一致性检查再对齐即可。

    • 其它 3B 测试里碰到的

      问题现象原因与处理
      Agreement 失败one(100) 等闭环缺一环:Leader 收到成功回复没更新提交;或 Follower 更新了 commitIndex 却没唤醒 applier。
      RPC 过多too many RPCs心跳发得太勤(例如 ticker 里固定短间隔全员 RPC)。需要按间隔节流(如配合选举/心跳超时)。
      冲突回退慢性能测试超时nextIndex 一格一格退。应实现 ConflictTerm / ConflictIndex,让 Leader 整块对齐。
      死锁测试 hang持锁路径里 RPC 回调或 apply 过重。缩小锁粒度,禁止持锁写 channel。

      3C:持久化(测试拉长后才会暴露)

    • 何时 persist、保的是什么
      我一开始在 Start 里就落了盘,其它 RPC 路径也零零散散加了一些 persist,跑 3C 才发现还有路径没覆盖全。
      要点:持久化保的不是业务结果,而是 承诺——凡是崩溃重启后可能做出和崩溃前矛盾的事(例如同一任期投两次票),就要在 对外承诺已经形成之后、RPC 返回之前 把该存的状态写盘。

    • applier(应用侧)

    • 问题:早期用 Sleep 轮询 commitIndex,测试反馈慢;还有在持锁时往 applyChan 里送消息,容易把整个节点卡死。

    • 做法:用 sync.Cond,在 commitIndex 前进时 Broadcast;发送 apply 消息时坚持 锁里只拷贝、锁外再写 channel,避免 channel 阻塞拖死持锁路径。 

这是最终测试通过的输出

3B

lcz@iv-yef3xahqtc5i3z5jzmr5:~/mit6.5840/6.5840/src$ make RUN="-run 3B" raft1

go build -race -o main/raft1d main/raft1d.go cd raft1 && go test -v -race -run 3B

=== RUN TestBasicAgree3B Test (3B): basic agreement (reliable network)

... ... Passed -- time 0.4s #peers 3 #RPCs 14 #Ops 3

--- PASS: TestBasicAgree3B (0.71s)

=== RUN TestRPCBytes3B Test (3B): RPC byte count (reliable network)

... ... Passed -- time 1.8s #peers 3 #RPCs 58 #Ops 11

--- PASS: TestRPCBytes3B (2.14s)

=== RUN TestFollowerFailure3B Test (3B): test progressive failure of followers (reliable network)...

... Passed -- time 4.3s #peers 3 #RPCs 188 #Ops 3

--- PASS: TestFollowerFailure3B (4.67s)

=== RUN TestLeaderFailure3B Test (3B): test failure of leaders (reliable network)...

... Passed -- time 4.7s #peers 3 #RPCs 294 #Ops 3

--- PASS: TestLeaderFailure3B (5.03s)

=== RUN TestFailAgree3B Test (3B): agreement after follower reconnects (reliable network)...

... Passed -- time 3.9s #peers 3 #RPCs 134 #Ops 7

--- PASS: TestFailAgree3B (4.37s)

=== RUN TestFailNoAgree3B Test (3B): no agreement if too many followers disconnect (reliable network)...

... Passed -- time 3.3s #peers 5 #RPCs 316 #Ops 2

--- PASS: TestFailNoAgree3B (3.81s)

=== RUN TestConcurrentStarts3B Test (3B): concurrent Start()s (reliable network)... ... Passed -- time 0.6s #peers 3 #RPCs 24 #Ops 0

--- PASS: TestConcurrentStarts3B (1.07s)

=== RUN TestRejoin3B Test (3B): rejoin of partitioned leader (reliable network)...

... Passed -- time 5.7s #peers 3 #RPCs 282 #Ops 4

--- PASS: TestRejoin3B (6.05s)

=== RUN TestBackup3B Test (3B): leader backs up quickly over incorrect follower logs (reliable network)...

... Passed -- time 19.1s #peers 5 #RPCs 2568 #Ops 102

--- PASS: TestBackup3B (19.68s)

=== RUN TestCount3B Test (3B): RPC counts aren't too high (reliable network)...

... Passed -- time 2.2s #peers 3 #RPCs 72 #Ops 0

--- PASS: TestCount3B (2.71s) PASS ok 6.5840/raft1 51.273s


3C

lcz@iv-yef3xahqtc5i3z5jzmr5:~/mit6.5840/6.5840/src$ make RUN="-run 3C" raft1
go build -race -o main/raft1d main/raft1d.go
cd raft1 && go test -v -race -run 3C 
=== RUN   TestPersist13C
Test (3C): basic persistence (reliable network)...
... Passed --  time  3.3s #peers 3 #RPCs    98 #Ops    6
--- PASS: TestPersist13C (3.75s)
=== RUN   TestPersist23C
Test (3C): more persistence (reliable network)...
... Passed --  time 13.7s #peers 5 #RPCs   568 #Ops   16
--- PASS: TestPersist23C (14.36s)
=== RUN   TestPersist33C
Test (3C): partitioned leader and one follower crash, leader restarts (reliable network)...
... Passed --  time  1.5s #peers 3 #RPCs    48 #Ops    4
--- PASS: TestPersist33C (1.84s)
=== RUN   TestFigure83C
Test (3C): Figure 8 (reliable network)...
2026/03/18 22:15:05 6PCdkEFTs2eiMT_RlFB1: dmxsrv.reader: clnt ACu3swj2_8gbs1Wd6nbn ReadCall err read unix /tmp/6.5840-6PCdkEFTs2eiMT_RlFB1->@: read: connection reset by peer
2026/03/18 22:15:47 6PCdkEFTs2eiMT_RlFB1: dmxsrv.reader: clnt 5VPxjteaE_i4P1o9h71T ReadCall err read unix /tmp/6.5840-6PCdkEFTs2eiMT_RlFB1->@: read: connection reset by peer
... Passed --  time 51.8s #peers 5 #RPCs  2369 #Ops    2
--- PASS: TestFigure83C (52.30s)
=== RUN   TestUnreliableAgree3C
Test (3C): unreliable agreement (unreliable network)...
... Passed --  time  3.4s #peers 5 #RPCs   220 #Ops  246
--- PASS: TestUnreliableAgree3C (4.02s)
=== RUN   TestFigure8Unreliable3C
Test (3C): Figure 8 (unreliable) (unreliable network)...
... Passed --  time 48.0s #peers 5 #RPCs  7496 #Ops    2
2026/03/18 22:16:44 T5AgHTtizWPRzjsYwAQ6: dmxsrv.reader: clnt UkD_e2Q-b5OG3f1PBPSa ReadCall err read unix /tmp/6.5840-T5AgHTtizWPRzjsYwAQ6->@: read: connection reset by peer
--- PASS: TestFigure8Unreliable3C (48.77s)
=== RUN   TestReliableChurn3C
Test (3C): churn (reliable network)...
... Passed --  time 16.6s #peers 5 #RPCs  1084 #Ops    1
--- PASS: TestReliableChurn3C (17.17s)
=== RUN   TestUnreliableChurn3C
Test (3C): unreliable churn (unreliable network)...
... Passed --  time 16.8s #peers 5 #RPCs  1028 #Ops    1
--- PASS: TestUnreliableChurn3C (17.46s)
PASS
ok      6.5840/raft1    160.685s

五、收获

      在看完论文理解之后lab3还是较简单的,但是对于test中其实并不能test出你代码的漏洞,就像我之前ticker设置的检索间隔一样。照样是通过了所有test。但是在B中却因为RPC调用太频繁超时了。我们在代码实现中要使用尽量多的调试信息打印,以便后续调试,希望我的思路可以带给你们思路,然后按照自己的思路实现出属于自己的lab1。最后希望有错误的地方多指正指正~