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 后发现存在若干问题:

    1. 时间戳精度:如果时间戳的精度不够高(例如,只精确到秒),则在高频调用的场景下可能会发生冲突。
    2. 系统时间调整:如果系统时间被调整,时间戳可能会重复或回退,这可能导致 ID 冲突。
    3. 进程号重用:在长时间运行的系统中,进程号可能在进程结束后被操作系统重用,这可能导致进程号的重复。

    最终采取了 GPT 推荐的 UUID 的方式 nrand() 返回标识符

  • Go 的 map,访问一个不存在的键时,返回的是值类型的空值。比如本实验中,返回的 string 类型的空值是 ""。不过,某些情景下可能需要区分键存在,但值确实为 ""键不存在两种情况,这可以通过额外的一个布尔型的 map 实现。或者可以使用 sync.Map,它的 Load 方法的第二个返回值为 bool,代表映射中是否存在键

  • 处理不可靠的网络

    • 客户端的请求未到达。这种情况没有影响,因为客户端会一直尝试直到收到回复

    • 请求正确到达,但回复未到达,使得客户端继续重发请求。为了处理这种情形,服务端必须先缓存此次请求的返回值,如果后续收到同一请求,直接返回这个值,而不对 data 进行任何处理(否则就违反了一个请求只修改数据一次的规定,即幂等性)。同时,服务端应该具有及时释放缓存的机制,这又有两种可行的实现方式

      • 每次客户端的请求捎带前一次已经确认收到回复的会话的 ID。这种方式会在某些用例下不能通过,原因是每个客户端均只进行了一次请求。一种解决方法是将「前一次已经确认收到回复的会话的 ID」放在全局的角度处理,即通过 common.go 实现在不同客户端之间的共享。不过这种方式可能会丢失一些 ID
      • 客户端确认收到回复后,额外发一个特殊的报文给服务器,比如一个 ID 与正常请求 ID 格式不符的 GET 报文,并附上前一次请求的 ID,这样服务器就可以删除对应的缓存。那也许会有额外的思考,比如这个确认报文丢了该怎么办?如果是在一个现实的网络中考虑,可能需要服务器返回对于客户端确认报文的确认,并且引入超时重传机制(因为连续几次丢失报文的概率是很低的,多试几次总能成)。本实验中没有考虑这些额外的处理
    • 同一请求多次到达

      这些请求的标识符是唯一的,因此可以识别出冗余的请求,进而返回事先缓存的结果,并且不对数据库进行任何修改,从而满足幂等性

Result

Lab2 实验结果

Reflection

  • 对于内存该如何释放是一个值得深思的问题。一般的思路有超时机制、心跳检测(与某个客户端失联后,即删除与它的全部会话缓存)。针对本实验的某些用例,也许压缩存储重型对象也是可行的...
  • 如何在不可靠的网络上构建可靠的服务,是分布式系统需要解决一个重要问题。应当假设所有环节均可能出错,尽可能地处理不同 case,以构建一个鲁棒的系统或算法
  • 幂等性在软件系统设计中的重要性

6.5840 Lab2
https://balddemian.github.io/6.5840-Lab2/
作者
Peiyang He
发布于
2024年4月14日
许可协议