实现版本:Spring 2025
实现时间:2025/8/2
MIT 6.5840 Distributed System Lab 4 个人踩坑记录。
前言:请保证Lab3能够稳定通过1000次,不然Lab4的错误很有可能是Lab3引起的。
4A
- 需要检测raft是否宕机,通过
close(rf.applyCh)
可以检测,这个在Lab3测试中并没有体现,如果未关闭则上层rsm
会有协程残留,例如读取applyCh
的协程,会影响关于shutdown
的一些测试。
// apply for all server
func (rf *Raft) applier() {
for !rf.killed() {
rf.apply()
time.Sleep(ApplyInterval)
}
// close chan
close(rf.applyCh)
}
- 在测试
TestRestartSubmit4A
时,测试10次会有1~2次Fali,错误是"Submit didn't stop after shutdown"。原因是测试并发调用了100次ts.oneNull()
,只等待了20ms让其全部submit
,然后shutdown
全部,在运行的慢的时候可能有几个submit在关闭后才调用,导致wg.Wait()一直等待,done信号无法发出。
修改方法:在rsm上增加killed状态,知道close(rf.applyCh)
后设置kill为true,同时清空等待的submit。在这之后如果调用submit就直接返回不处理。
rsm.mu.Lock()
if rsm.killed {
rsm.mu.Unlock()
return rpc.ErrWrongLeader, nil
}
- 一开始我在submit内是等待2s后自动退出(为什么是2s,面向测试),但我认为这种做法是不太对的,只是刚好能过测试。仔细读Hit发现在leader被分区后是可以无限等待的,只需要检测任期或者leader发生变化来判断某些submit是否应该返回。
解决方案:
submit 内开一个协程定期去检测该index和leadership(包括任期)是否发生变化,如果变化则调用ctx的cancel,这样就可以让无效的submit返回,而不是等待固定时间,并且可以很快的检测出来。
go rsm.monitorLeadership(ctx, index, term)
select {
case resp := <-result.respCh:
return rpc.OK, resp
case <-ctx.Done():
return rpc.ErrWrongLeader, nil
}
4A测试1000次无问题。

4B
- 在TestConcurrent4B测试卡住了三天,测试100次平均1~4次Fail。
错误原因日志:
Test: many clients (4B many clients) (reliable network)...
2025/08/07 15:24:03 server: 0 args: {ID:5460730212102918053 Key:k Value:{"Id":3,"V":0} Version:0} not exist and match
2025/08/07 15:24:03 server: 0 args: {ID:1384698804140868238 Key:k Value:{"Id":1,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 0 args: {ID:3783242132577992117 Key:k Value:{"Id":0,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 0 args: {ID:7025132105560995297 Key:k Value:{"Id":4,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 clerk Key: k Value: {"Id":0,"V":0} Version: 0 recv reply: {Err:ErrVersion}
2025/08/07 15:24:03 clerk Key: k Value: {"Id":1,"V":0} Version: 0 recv reply: {Err:ErrVersion}
2025/08/07 15:24:03 clerk Key: k Value: {"Id":4,"V":0} Version: 0 recv reply: {Err:ErrVersion}
2025/08/07 15:24:03 server: 2 args: {ID:5460730212102918053 Key:k Value:{"Id":3,"V":0} Version:0} not exist and match
2025/08/07 15:24:03 server: 2 args: {ID:1384698804140868238 Key:k Value:{"Id":1,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 2 args: {ID:3783242132577992117 Key:k Value:{"Id":0,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 2 args: {ID:7025132105560995297 Key:k Value:{"Id":4,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 1 args: {ID:5460730212102918053 Key:k Value:{"Id":3,"V":0} Version:0} not exist and match
2025/08/07 15:24:03 server: 1 args: {ID:1384698804140868238 Key:k Value:{"Id":1,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 1 args: {ID:3783242132577992117 Key:k Value:{"Id":0,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 1 args: {ID:7025132105560995297 Key:k Value:{"Id":4,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 4 args: {ID:5460730212102918053 Key:k Value:{"Id":3,"V":0} Version:0} not exist and match
2025/08/07 15:24:03 server: 4 args: {ID:1384698804140868238 Key:k Value:{"Id":1,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 4 args: {ID:3783242132577992117 Key:k Value:{"Id":0,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 4 args: {ID:7025132105560995297 Key:k Value:{"Id":4,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 0 args: {ID:4527911980207300838 Key:k Value:{"Id":2,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 server: 0 args: {ID:5460730212102918053 Key:k Value:{"Id":3,"V":0} Version:0} exist and not match
2025/08/07 15:24:03 clerk Key: k Value: {"Id":2,"V":0} Version: 0 recv reply: {Err:ErrVersion}
2025/08/07 15:24:03 clerk Key: k Value: {"Id":3,"V":0} Version: 0 recv reply: {Err:ErrVersion}
多个clerk并发向server发起Put,可以看到 {"Id":3,"V":0} Version:0
不存在key,新增数据。即日志server: 0 args: {ID:5460730212102918053 Key:k Value:{"Id":3,"V":0} Version:0} not exist and match
。
但最终返回却是ErrVersion,应该是OK才对。这是因为clerk又重新操作了一次。即日志server: 0 args: {ID:5460730212102918053 Key:k Value:{"Id":3,"V":0} Version:0} exist and not match
。
而后操作的rpc先返回,导致出现ErrVersion。虽然网络是reliable 保证可靠交付,但顺序可能出现交换。
根本原因:server 不是幂等性的,同一个操作返回值不一样。两次同一个ID的Put操作返回值不一样!
一开始没有在Put操作上增加ID,看了两天日志不知道错误原因。(╥﹏╥)
解决方案:在Put上增加ID,已经做过的操作直接返回已有的结果。Get本身是幂等的,不需要增加ID。
TestConcurrent4B测试1000次没有问题。

- 我发现网上很多人
TestSpeed4B
解决方案是增加心跳的频率,例如从100ms减到30ms等。在这个测试中已经注释了heartbeat interval should be ~ 100 ms; require at least 3 ops per
。改动心跳频率显然不合理。
附个人实现的一些参数:
// raft
HeartbeatInterval = time.Millisecond * 100
MinElectionTimeout = time.Millisecond * 300
MaxElectionTimeout = time.Millisecond * 500
CommitInterval = time.Millisecond * 10
ApplyInterval = time.Millisecond * 10
// rsm
MonitorInterval = time.Millisecond * 20
单次测试TestSpeed4B
在21s左右(通过需要在33s内)。
并行8个测试1000s 通过,个人CPU:12th Gen Intel(R) Core(TM) i5-12500H。该测试和CPU关系不是很大,大部分时间在等待心跳。如果测试失败,很有可能是Raft实现不太好。

最后4B测试1000次通过(把TestSpeed4B抽离出来单独测试,并行太多测试会导致CPU瓶颈):

4C
- rsm 快照测试框架BUG,出现DATA RACE
WARNING: DATA RACE
Read at 0x00c0004be140 by goroutine 10:
6.5840/kvraft1/rsm.(*Test).countValue()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/kvraft1/rsm/test.go:137 +0xa4
6.5840/kvraft1/rsm.(*Test).checkCounter()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/kvraft1/rsm/test.go:117 +0xa8
6.5840/kvraft1/rsm.TestSnapshot4C()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/kvraft1/rsm/rsm_test.go:364 +0x3db
testing.tRunner()
/usr/local/go/src/testing/testing.go:1690 +0x226
testing.(*T).Run.gowrap1()
/usr/local/go/src/testing/testing.go:1743 +0x44
Previous write at 0x00c0004be140 by goroutine 811:
reflect.Value.SetInt()
/usr/local/go/src/reflect/value.go:2391 +0xb7
encoding/gob.decInt64()
/usr/local/go/src/encoding/gob/decode.go:298 +0x6c
encoding/gob.(*Decoder).decodeSingle()
/usr/local/go/src/encoding/gob/decode.go:461 +0x2ce
encoding/gob.(*Decoder).decodeValue()
/usr/local/go/src/encoding/gob/decode.go:1252 +0x408
encoding/gob.(*Decoder).DecodeValue()
/usr/local/go/src/encoding/gob/decoder.go:231 +0x2e7
encoding/gob.(*Decoder).Decode()
/usr/local/go/src/encoding/gob/decoder.go:206 +0x191
6.5840/labgob.(*LabDecoder).Decode()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/labgob/labgob.go:55 +0x6b
6.5840/kvraft1/rsm.(*rsmSrv).Restore()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/kvraft1/rsm/server.go:82 +0x12d
6.5840/kvraft1/rsm.(*RSM).handleSnapshot()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/kvraft1/rsm/rsm.go:123 +0x2b9
6.5840/kvraft1/rsm.(*RSM).reader()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/kvraft1/rsm/rsm.go:152 +0x23c
6.5840/kvraft1/rsm.MakeRSM.gowrap1()
/home/tecy/Github/MIT6.5840-Spring-2025/6.5840/src/kvraft1/rsm/rsm.go:95 +0x33
读代码发现:
// 在 rsm/server.go
func DoOp()
rs.mu.Lock()
rs.counter += 1
rs.mu.Unlock()
func Snapshot()
e.Encode(rs.counter)
func Restore()
d.Decode(&rs.counter)
可以看到DoOp操作counter时加了锁,而Snapshot和Restore操作counter都没有加锁,从而出现DATA RACE。加锁访问即可通过测试。
问:为什么自己实现的server使用这三个函数操作数据时不需要加锁?
因为调用这三个函数都在RSM的reader applyCh goroutine 里,没有其他协程访问这些数据,而测试开启了一个协程检测ts.counter。

- 利用SnapshotData保存快照,出现decode/encode错误。
type SnapshotData struct {
Data map[string]Result
}
原因是没有注册labgob。
kvserver 测试1000次通过:

Comments NOTHING