6.5840 Lab2
Misc
- 这是 6.5840 2024 年课程的 Lab 2,在以前年份的课程中是没有的。因为标记的难度为 easy,于是在正式进入 Raft 的实验之前顺手把这个实验做了
- 内存释放的实现有点粗糙了…
Go
- 安装某个依赖:
go get
go test
:在许多的编程语言中,可以通过注解的形式标记哪些函数是测试时自动执行的(比如 SpringBoot 中的@SpringBootTest
)。Go 中没有注解,于是用一套源文件和函数名的命名规则来标识测试函数
Task
修改 kvsrv 下的 client.go, server.go, commom.go
Implementation
-
Append 最初理解为是拉一个链表,但其实就是在原有的字符串后追加
-
RPC 标识符的处理
最开始没有看到 client.go 中提供的 nrand() 函数,于是思考了一下标识符的实现方式。一个简单的实现是使用进程号 + 时间戳的方式。询问 GPT 后发现存在若干问题:
- 时间戳精度:如果时间戳的精度不够高(例如,只精确到秒),则在高频调用的场景下可能会发生冲突。
- 系统时间调整:如果系统时间被调整,时间戳可能会重复或回退,这可能导致 ID 冲突。
- 进程号重用:在长时间运行的系统中,进程号可能在进程结束后被操作系统重用,这可能导致进程号的重复。
最终采取了
GPT 推荐的 UUID 的方式nrand() 返回标识符 -
Go 的 map,访问一个不存在的键时,返回的是值类型的空值。比如本实验中,返回的 string 类型的空值是 “”。不过,某些情景下可能需要区分键存在,但值确实为 “” 和键不存在两种情况,这可以通过额外的一个布尔型的 map 实现。或者可以使用
sync.Map
,它的 Load 方法的第二个返回值为 bool,代表映射中是否存在键 -
处理不可靠的网络
-
客户端的请求未到达。这种情况没有影响,因为客户端会一直尝试直到收到回复
-
请求正确到达,但回复未到达,使得客户端继续重发请求。为了处理这种情形,服务端必须先缓存此次请求的返回值,如果后续收到同一请求,直接返回这个值,而不对 data 进行任何处理(否则就违反了一个请求只修改数据一次的规定,即幂等性)。同时,服务端应该具有及时释放缓存的机制,这又有两种可行的实现方式
- 每次客户端的请求捎带前一次已经确认收到回复的会话的 ID。这种方式会在某些用例下不能通过,原因是每个客户端均只进行了一次请求。一种解决方法是将「前一次已经确认收到回复的会话的 ID」放在全局的角度处理,即通过 common.go 实现在不同客户端之间的共享。不过这种方式可能会丢失一些 ID
- 客户端确认收到回复后,额外发一个特殊的报文给服务器,比如一个 ID 与正常请求 ID 格式不符的 GET 报文,并附上前一次请求的 ID,这样服务器就可以删除对应的缓存。那也许会有额外的思考,比如这个确认报文丢了该怎么办?如果是在一个现实的网络中考虑,可能需要服务器返回对于客户端确认报文的确认,并且引入超时重传机制(因为连续几次丢失报文的概率是很低的,多试几次总能成)。本实验中没有考虑这些额外的处理
-
同一请求多次到达
这些请求的标识符是唯一的,因此可以识别出冗余的请求,进而返回事先缓存的结果,并且不对数据库进行任何修改,从而满足幂等性
-
Result
Reflection
- 对于内存该如何释放是一个值得深思的问题。一般的思路有超时机制、心跳检测(与某个客户端失联后,即删除与它的全部会话缓存)。针对本实验的某些用例,也许压缩存储重型对象也是可行的…
- 如何在不可靠的网络上构建可靠的服务,是分布式系统需要解决一个重要问题。应当假设所有环节均可能出错,尽可能地处理不同 case,以构建一个鲁棒的系统或算法
- 幂等性在软件系统设计中的重要性
6.5840 Lab2
https://exapricity.tech/6.5840-Lab2.html