当前位置: 首页 > news >正文

MIT_65840测试流程详解

MIT_65840 测试网络环境的搭建与实现

MIT 6.5840 的实验代码把真实网络通信细节封装了起来, 让我们把注意力集中在分布式算法本身. 但从调试和理解整个测试框架的角度看, 测试环境是如何搭起来的同样非常重要.

一开始我只关注了 src/labrpc 中的 RPC、NetworkServerService, 但如果想真正理解“一个测试是怎么把 client、server 和网络跑起来的”, 就必须把 src/tester1 也一起看进去. 实际上, 完整的测试环境是由下面几层共同组成的:

  1. labgob: 负责 RPC 参数与返回值的序列化/反序列化.
  2. labrpc: 提供一个可控的、可模拟故障的 RPC 网络.
  3. tester1: 在 labrpc 之上搭建测试环境, 负责启动 server、创建 client、连接网络、注入故障、收尾清理.
  4. 各个实验自己的 test.go / *_test.go: 描述本实验想怎么使用 tester, 比如启动几个 server, server 的类型、创建什么 clerk、检查什么性质.

本文会以 kvsrv1/lock/lock_test.go 这个“分布式锁测试”为例, 串起整个测试网络搭建流程.

一、整体分层

从职责上看, 这套测试框架可以理解成四层:

  • 业务层:kvsrv1/server.gokvsrv1/client.gokvsrv1/lock/lock.go
  • 测试封装层:kvsrv1/test.gokvtest1/kvtest.go
  • 测试环境层:tester1/config.gotester1/group.gotester1/srv.gotester1/clnts.go
  • RPC 网络层:labrpc/labrpc.go

最容易混淆的一点是:

  • kvsrv1.KVServer 是你实现的业务服务对象.
  • labrpc.Server 是一个RPC 分发器, 它并不关心具体业务.
  • tester1.Server 是 tester 用来包装单台测试节点的测试时 server 容器.

也就是说, 这里其实有三个不同层次的 “server” 概念, 它们分工不同:

  • KVServer 负责实现具体服务的 Get / Put 函数.
  • labrpc.Server 负责把 RPC 请求路由给某个 Service
  • tester1.Server 负责持有测试时的一台节点、它的端点、persister 和 service 列表.

后续我们可以看到这三层 Server 是如何协作的.

二、序列化与反序列化:labgob

RPC 传输的基础是序列化与反序列化. 这里使用的是对 Go 标准库 encoding/gob 的封装 labgob.

encoding/gob 在实验里有两个常见坑:

1. 小写字段不会被编码

type BadStruct struct {PublicField  intprivateField int
}

只有导出字段(大写开头)才能参与 gob 编码. RPC 参数或 reply 如果用了小写字段, 会导致非常隐蔽的问题.

2. 解码到已有非默认值变量时, 默认值可能不会覆盖旧值

这会影响:

  • RPC reply 解码
  • 持久化状态恢复
  • 复用变量时的正确性

因此在这套框架里, 实验非常依赖严格的编码/解码约定.

三、labrpc:可控的 RPC 网络

labrpc 不是简单地“帮你调函数”, 而是实现了一个可模拟故障的网络层. 它支持:

  • 丢失请求
  • 丢失回复
  • 延迟回复
  • 长时间重排回复
  • 断开某些 client/server 的连接
  • 删除 server

1. ClientEnd:客户端端点

labrpc 里, 一个 RPC 发送端抽象成 ClientEnd. 它最关键的工作是:

  1. args 编码成字节流
  2. 把请求投递到 Network.endCh
  3. 等待 reply
  4. 把 reply 解码回结构体

可以把它理解成“一个 RPC socket 的抽象”.

2. Network:整个测试中的共享网络

Network 是整个 RPC 环境的核心, 它内部维护:

  • 所有端点 ends
  • 哪些端点当前可用 enabled
  • 端点连向哪个 server connections
  • 当前网络上注册了哪些 server servers
  • 用于接收请求的总入口 endCh

MakeNetwork() 创建网络时, 会起一个 goroutine 持续监听 endCh, 收到请求后交给 processReq() 处理. 这里的设计很关键:

  • 同一个测试用例内部, 多个 client 和 server 共享同一个 Network
  • 不同测试用例之间, 每次都会重新 MakeNetwork(), 彼此隔离

这也是为什么不同测试不会互相污染, 而同一个测试里的所有 client/server 又确实工作在同一张“虚拟网络”上.

3. processReq():网络如何处理 RPC

processReq() 是整个网络层最重要的函数之一. 它负责:

  1. 检查这个 ClientEnd 当前是否可用
  2. 检查它连接到哪个 server
  3. 根据网络配置决定是否丢包或延迟
  4. 调用 server 的 RPC dispatcher
  5. 决定是否正常返回 reply、延迟 reply、或者伪装成超时

这一步不是直接调用业务逻辑, 而是先调 labrpc.Server.dispatch().

4. labrpc.ServerService

labrpc.Server 是一个通用的 RPC 分发器:

type Server struct {mu       sync.Mutexservices map[string]*Servicecount    int
}

这里的 Server 并不是 KVServerRaft, 而是一个“RPC 容器”. 一个 labrpc.Server 可以挂多个 Service, 例如:

  • 一个 Raft service
  • 一个 KVServer service

当请求形如 "KVServer.Put" 到来时:

  1. Server.dispatch() 先按 . 切成服务名和方法名
  2. services 里找到 KVServer
  3. 再由 Service.dispatch() 调用具体方法 Put

5. 为什么这里用了反射

MakeService(rcvr interface{}) 会扫描传入对象的导出方法, 把形如下面签名的方法注册为 RPC handler:

  • 导出方法
  • 参数个数为 3(含接收者)
  • 第三个参数是指针
  • 没有返回值

之所以要用反射, 是因为 labrpc 作为一个通用框架, 在编译时并不知道你会注册的是 KVServerRaft、还是 ShardCtrler. 它只能在运行时读取对象的类型信息、方法名和方法签名.

四、tester1:真正把测试环境搭起来

如果只看 labrpc, 我们只能理解“RPC 是怎么发出去的”;但还不知道:

  • 测试开始时谁创建了网络
  • 谁创建了 server
  • 谁把 server 注册到网络上
  • 谁创建了 client clerk
  • 谁负责清理这些资源

这些工作都在 tester1 里完成.

五、config.go:测试环境的总控制器

tester1.Config 可以看作一次测试的总控对象. 它里面最核心的成员是:

  • net *labrpc.Network
  • *Groups
  • *Clnts

它把“网络、server 组、client 集合”组织到一起.

初始化入口是:

func MakeConfig(t *testing.T, n int, reliable bool, mks FstartServer) *Config

这个函数会完成几件事:

  1. 创建一个全新的 labrpc.Network
  2. 创建 Groups
  3. 创建默认 group, 并启动其中的 servers
  4. 创建 Clnts
  5. 根据参数设置网络是否可靠

这里最值得注意的是最后一个参数 mks FstartServer. 这个参数是一个“服务工厂函数”, tester 自己并不知道你到底要起的是 KVServer 还是 Raft, 所以它只负责搭环境, 真正如何构造业务 service, 由调用方通过 FstartServer 传进来.

六、group.go:server 组的生命周期管理

group.go 负责把一组 server 管起来. 对于后面的 Raft 或 shardkv, 这一层尤其重要;不过即使是 lock_test.go 这种单机 KV 测试, 也一样会走这套流程.

1. ServerGrp

ServerGrp 表示一个 server group, 它维护:

  • group 的 id
  • 这组里的所有 tester server
  • 每个 server 的名字
  • 当前连接状态
  • 如何启动 server 的回调 mks

2. FstartServer

group.go 里定义了:

type FstartServer func(ends []*labrpc.ClientEnd, grp Tgid, srv int, persister *Persister) []IService

这个函数类型的意义是:

  • tester 准备好一台测试节点的基础设施
  • 然后把这些基础设施传给你
  • 由你返回真正要注册到 RPC server 上的业务 service 列表

对于 kvsrv1 来说, 传进去的是 StartKVServer;对于 kvraft1 来说, 传进去的是另一个版本的 StartKVServer.

3. StartServer() 的关键作用

ServerGrp.StartServer(i) 做了整条链路里最关键的桥接:

srv.svcs = sg.mks(srv.clntEnds, sg.gid, i, srv.saved)
labsrv := labrpc.MakeServer()
for _, svc := range srv.svcs {s := labrpc.MakeService(svc)labsrv.AddService(s)
}
sg.net.AddServer(ServerName(sg.gid, i), labsrv)

这几行代码把三层东西接起来了:

  1. 通过 sg.mks(...) 创建业务 service, 例如 KVServer 中的 mks 函数就是 StartKVServer.
  2. labrpc.MakeService(...) 把业务对象包装成 RPC service.
  3. labrpc.MakeServer() 把这些 service 装进通用 RPC server.
  4. 再调用 net.AddServer(...) 挂到网络上

所以真正的“服务启动”不是一行完成的, 而是 tester1 和 labrpc 协作完成的.

4. 连接、断开、分区

group.go 还负责:

  • ConnectAll()
  • DisconnectAll()
  • ShutdownServer()
  • Partition()

这就是为什么后面可以很方便地写各种故障测试. tester 不需要改业务代码, 只要改网络连通性即可.

七、srv.go: tester 对单台 server 的包装

tester1.Server 不是业务 server, 而是 tester 对“一个测试节点”的封装. 它主要保存:

  • 这台节点使用的 net
  • 它的 Persister
  • 返回出来的业务 service 列表 svcs
  • 这台节点到其它节点的 ClientEnd

其中有一个非常关键的函数:

func makeServer(net *labrpc.Network, gid Tgid, nsrv int) *Server

它会为这台节点预先创建一组 ClientEnd, 这些端点主要给多副本 server 之间相互通信使用. 对当前 kvsrv1 这个 lab 来说, 这些 ends 基本可以忽略;但对于后面的 raft1kvraft1 就非常重要了.

另外, startServer()shutdownServer() 会处理 persister 复制和 Kill(), 从而支持重启与清理.

八、clnts.go: 测试中的 client 是怎么创建的

clnts.go 负责管理测试过程中的客户端集合.

1. Clnt: 一个 clerk 对应的连接集合

一个 Clnt 不是单个 ClientEnd, 而是一组“按 server 名字索引的端点”. 它内部的 makeEnd(server)懒创建的:

  • 如果还没连过这个 server, 就创建一个新的 ClientEnd
  • 然后调用 net.Connect(...) 把它挂到对应 server 名字上
  • 最后按连通性策略决定是否 Enable

这意味着:

  • 每个 clerk 都有自己独立的一组端点
  • 不同 clerk 不会共用同一个 ClientEnd
  • 但它们都共享同一个 Network

2. Clnts: 所有 clerk 的集合

Clnts 负责:

  • MakeClient(): 创建测试用的 clerk 连接器
  • DeleteClient(): 删除这个 clerk 的所有端点
  • cleanup(): 测试结束时统一删除所有 clerk

九、persister.goannotation.go

这两个文件在当前 kvsrv1/lock_test.go 里不是主角, 但它们属于 tester 框架的重要配套:

  • persister.go:提供持久化状态抽象, 主要服务于 Raft / kvraft
  • annotation.go:记录测试过程中的故障、检查器信息、可视化标注

对于简单 KV 测试, 可以把它们看作“为更复杂实验预留的基础设施”.

十、以 lock_test.go 为例看完整启动流程

下面用 src/kvsrv1/lock/lock_test.go 把整个流程串起来.

1. 测试入口

测试入口类似:

  • TestOneClientReliable
  • TestManyClientsReliable
  • TestOneClientUnreliable
  • TestManyClientsUnreliable

这些测试都会调用 runClients().

2. runClients() 创建测试环境

runClients() 先调用 kvsrv.MakeTestKV(t, reliable).

MakeTestKV() 的关键是:

cfg := tester.MakeConfig(t, 1, reliable, StartKVServer)

这表示:

  • 创建一个 tester 配置
  • 启动 1 台 server
  • 网络是否可靠由参数决定
  • StartKVServer 来生成业务 service

3. MakeConfig() 创建网络和 group

MakeConfig() 会:

  1. cfg.net = labrpc.MakeNetwork()
  2. cfg.Groups = newGroups(cfg.net)
  3. cfg.MakeGroupStart(GRP0, 1, StartKVServer)
  4. cfg.Clnts = makeClnts(cfg.net)

注意顺序:先有网络, 再有 group/server, 最后再有 client 集合.

4. MakeGroupStart() 启动 server

这一步会继续走到:

  • MakeGroup(...)
  • StartServers()
  • StartServer(0)

5. StartServer(0) 创建真正的 KV 服务

在这里, tester 调用你自己的:

func StartKVServer(...) []tester.IService {kv := MakeKVServer()return []tester.IService{kv}
}

可以看到:

  • 真正的业务对象是 KVServer
  • 但返回给 tester 的是 []IService
  • tester 再把它包装成 labrpc.Service 并注册到 labrpc.Server

所以这里的关系是:

KVServer -> Service -> labrpc.Server -> Network

6. 测试并发创建 clerk

runClients() 后面调用 SpawnClientsAndWait(...). 这个函数会起多个 goroutine, 每个 goroutine 都会执行:

  1. ts.MakeClerk()
  2. 调用 oneClient(...)
  3. 测试结束后删除该 clerk

TestKV.MakeClerk() 里会:

  1. clnt := ts.Config.MakeClient()
  2. ck := MakeClerk(clnt, tester.ServerName(tester.GRP0, 0))

这里的 clnt 是 tester 层的连接器, ck 才是 kvsrv1 里真正的 KV clerk.

7. clerk 发 RPC 的完整路径

lock.go 里的 Acquire() 调用 ck.Get() / ck.Put() 时, 请求路径是:

  1. kvsrv1.Clerk.Get/Put
  2. tester.Clnt.Call(server, method, args, reply)
  3. labrpc.ClientEnd.Call(...)
  4. Network.processReq(...)
  5. labrpc.Server.dispatch(...)
  6. Service.dispatch(...)
  7. KVServer.Get/Put(...)

这条链路就是整个实验里最重要的调用路径.

8. 锁测试到底在测什么

这里测试的并不是一个单独的“锁服务器”, 而是:

  • KVServer 提供的版本化 Get/Put
  • 在客户端 lock.go 里实现锁逻辑
  • 多个 client 通过 RPC 并发争抢同一个 key

也就是说, 锁本身是构建在 KV 服务之上的客户端协议, 而不是 server 端内建的一个专门对象.

十一、几个关键技术细节

1. 每个测试都有自己的网络

不同测试用例之间不会共享同一个 Network. 每次调用 MakeConfig(), 都会新建一个 Network.

2. 同一个测试内, 所有 client 和 server 共享同一个网络

这是测试能够模拟统一网络环境的前提.

3. 每个 clerk 有自己独立的 ClientEnd

虽然共享同一个 Network, 但每个 clerk 的端点是独立创建的. 这让 tester 可以细粒度控制不同 client 的连接状态.

4. FstartServer 是解耦的关键

tester 不知道怎么构造 KVServerRaft, 它只知道“我要起一台 server, 你给我一个 service 列表”.

5. labrpc.Server 是通用 RPC 容器, 不是业务 server

这也是为什么不同实验都能复用同一套 labrpc.

6. processReq() 中的 goroutine 很关键

它一方面异步等待业务 handler 执行, 另一方面允许网络层持续检测 server 是否已被删除, 从而正确模拟“处理中宕机”的场景.

十二、总结

如果只看 src/labrpc, 我们看到的是“RPC 怎么发送与分发”;但如果把 src/tester1 一起看进去, 才能真正理解整个测试环境是怎么搭起来的.

lock_test.go 为例, 完整链路可以概括为:

  1. 测试代码调用 MakeTestKV()
  2. MakeTestKV() 调用 tester.MakeConfig()
  3. MakeConfig() 创建 NetworkGroupsClnts
  4. Groups.StartServer() 通过 FstartServer 构造 KVServer
  5. KVServer 被包装成 Service, 注册到 labrpc.Server
  6. client 通过 tester.Clnt 创建自己的端点
  7. clerk 发起 Get/Put, 请求经 Networkdispatch 路由到 KVServer
  8. 锁逻辑在 client 侧完成, KV server 提供原子读写基础

从这个角度看, MIT 6.5840 的测试框架并不是“简单封装了一个 RPC”, 而是搭了一整套可控、可扩展、可模拟故障的分布式实验环境. 理解这一层之后, 后续分析 raft1kvraft1shardkv1 的测试代码会清晰很多.

十三、一张简化的调用链

lock_test.go-> MakeTestKV()-> tester.MakeConfig()-> labrpc.MakeNetwork()-> MakeGroupStart()-> StartServers()-> StartServer()-> StartKVServer()-> labrpc.MakeService()-> labrpc.MakeServer()-> net.AddServer()-> makeClnts()-> SpawnClientsAndWait()-> MakeClerk()-> tester.MakeClient()-> kvsrv1.MakeClerk()-> Clerk.Get/Put()-> tester.Clnt.Call()-> labrpc.ClientEnd.Call()-> Network.processReq()-> Server.dispatch()-> Service.dispatch()-> KVServer.Get/Put()

这张图基本就是整个测试网络环境搭建与执行的核心脉络.

十四、一个常见误区

很多时候会把 kvsrv1.KVServertester1.Serverlabrpc.Server 混在一起. 实际上三者并不是一回事:

  • kvsrv1.KVServer: 你写的业务逻辑
  • tester1.Server: tester 中的一台测试节点包装
  • labrpc.Server: RPC 服务分发器

只要把这三层区分清楚, 整个框架的结构就会非常清晰.

总结成一句话

labrpc 负责“模拟网络和 RPC”, tester1 负责“搭测试环境和管理节点”, 而具体 lab 的 test.goserver.go 负责“把业务逻辑接入这套环境并接受测试”.

这三层合起来, 才构成了 MIT 6.5840 实验里的完整测试网络环境.

http://www.jsqmd.com/news/836767/

相关文章:

  • 解析靠谱的岩棉板定制厂家有哪些 - myqiye
  • 如何评估不错的辣条包装机工厂哪家好? - 工业品牌热点
  • 干货!家用冷凝炉如何挑选?靠谱品牌推荐 - myqiye
  • 选316L不锈钢水管,这家供应商靠谱吗? - 工业品牌热点
  • 缝包机选购指南,奇威包装是佳选 - myqiye
  • 2026年成都散酒铺品牌TOP7权威排行榜,为你实战推荐! - 品牌推荐官方
  • 仪创时代混凝土徐变仪选购指南,如何挑选合适的产品 - 工业品牌热点
  • 2026年4月行业内可靠的大件物流服务商口碑推荐,大件物流/大件运输,大件物流厂家推荐 - 品牌推荐师
  • 折弯机品牌哪家好?江苏隆旭重工靠谱吗? - 工业品牌热点
  • 包装礼盒优质厂家推荐,上海万通印务服务多样 - mypinpai
  • 涡街流量计价格与性价比分析,靠谱品牌及民用应用解读 - myqiye
  • 重庆市 CPPM 注册采购经理授权中心及电话|重庆官方 CPPM 报考中心 - 中供国培
  • 学校安全用电科学管理选购指南,飞凌佳杰经验谈 - 工业品牌热点
  • 气体采样管自动解析机器人选购指南:这些厂商靠谱吗 - mypinpai
  • 孖仔鸽皇菜品丰富吗?靠谱乳鸽供应商推荐2026 - 工业品牌热点
  • 2026年5月广东化妆品代加工厂家推荐指南:化妆品贴牌代加工,化妆品制造生产,化妆品OEM,国际大牌同源化妆品代工,跨境爆款化妆品加工公司优选! - 品牌鉴赏师
  • 拒绝维修烦恼!电动推杆哪家质量好及核心参数对比 - 博客万
  • 天津欧卡卡全屋定制费用高吗?详细分析 - 工业品牌热点
  • 有实力的薄蜂窝纸箱品牌,美迅机械性价比高 - mypinpai
  • 靠谱的武汉做无错误教辅印刷的厂家,费用如何 - mypinpai
  • 聚醚定制产品选购,南通汉晟化工靠谱吗 - 工业品牌热点
  • 天津市 CPPM 注册采购经理授权中心及电话|天津官方 CPPM 报考中心 - 中供国培
  • 青岛靠谱名包回收盘点,收的顶连锁平台优势突出 - 奢侈品回收测评
  • 招聘求职软件app排行榜:2026权威榜单,易直聘领跑 - 博客万
  • 2026年5月靠谱的苏州陶瓷设备生产厂家推荐榜,流延机/叠层机/等静压机/整线定制设备选择指南 - 品牌企业推荐师(官方)
  • 彩钢夹芯净化板靠谱厂家推荐,尚泰净化板上榜 - mypinpai
  • 性价比高的个性化餐饮全案设计公司大盘点 - mypinpai
  • 2026年5月丨办公家具厂家推荐:理性消费趋势下的选择指南 - 品牌企业推荐师(官方)
  • 原厂直供进口轴承公司推荐,靠谱的有哪些 - 工业品牌热点
  • 桂林瓷砖空鼓修复公司排行 基于工艺与口碑的实测榜单 - 奔跑123