Mit分布式系统课程-Lab2-PartB

前两天删了在github上的6.824, 因为课程里说了不要把源码放在公共的地方, 之前没删一直心有歉意, 还好进度没这么快, 应该对别人也不会造成影响.

言归正传, 这星期空余时间一直在看Lab2的Part B, 今天因为身体不适, 请假一天在家, 开始实现思路, 并记录一下自己遇到的一些大小问题.

目标

实现基于viewservice(Lab1实现的视图服务器, 用来记录主副服务器的地址)的键值服务器, 分别为primary(主服务器),Backup(备份服务器), 客户端通过viewservice获得primary的地址, 发送键值的存取请求, primary在响应请求的同时, 将数据备份到backup服务器. 实现完成后, 通过所有的unit test.

架构

2个kv服务器, viewservice以及客户端

  1. viewservice
    • 存储并提供primary以及backup的信息
    • 接受primary, backup的定时ping请求, 更新或保持两个服务器的位置
    • 提供查询当前服务器状态
  2. primary
    • 接受client的请求
    • 定期向viewservice报备自己的状态
    • 在backup服务器更换时负责将自身全部数据同步过去
  3. backup
    • 作为primary的备份存储
    • 定期向viewservice报备自己的状态
    • 在primary挂了之后, 提升为primary, 负责client的请求
  4. client
    • 向primary发送数据存储,查询的请求
    • 若primary返回”错误的服务器”时, 询问viewservice, 更新primary

遇到的挑战

  1. 实现时有些golang的特性不是特别了解, 一边打开golang的官方文档, 一边poc.
  2. 决定在什么时候需要容错? 是否可恢复? 在反复重试时, 是否要定最大重试次数, 以及失败的处理.

单元测试

当我运行go test来验证逻辑的时候, 实在不得不赞叹老师们详细的测试用例, 有几个case让我挠头.

  • TestAtMostOnce
    模拟服务器返回报文丢失的情况, 利用puthash-get验证, puthash是put操作的一种变体, 它要存储的值是先前的值和当前值的hash结果, 所以这个地方引入了存储状态, 我通过每次请求要求客户端发送一个unique id来记录此次操作, 尽管报文丢失, 客户端重试时还是能用这个id找回上次通信的内容.
  • TestConcurrentSame
    验证primary服务器只有在backup也处理成功的情况才算成功.
  • TestRepeatedCrash
    模拟存储服务器间断崩溃的情况, 就是要保证客户端的每个请求都有重试的可能, primary和backup的通信过程中, backup随时可能崩溃, 这个测试使用put-get验证.
  • TestRepeatedCrashUnreliable
    模拟存储服务器间断崩溃的同时, 网络也不稳定, 服务器的返回报文会丢失. 这个case的难点在它是使用puthash-get来验证的, 就是说, 你要在服务器不时崩溃的情况下保证以前的值不仅在primary服务器上存在, 也要在backup服务器上保存, 以免primary下一秒挂了.
  • TestPartition1
    模拟primary已经过期的情况下, 向他发送的请求, 能被它拒绝.

POC

backup服务器更换时, primary需要同步当前数据库所有数据到backup, 我不想用foreach来每条数据进行同步, 所以想既然rpc能传递这么多类型的参数, 那直接将map传递过去不就不用那么麻烦了?所以进行了一次poc.

server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import "net"
import "net/rpc"
import "os"
import "fmt"

type Server struct {
db map[string]string
}

func (server *Server) Sync(database map[string]string, reply *int) error {
fmt.Println("Start syncing...")
server.db = database
for k, v := range server.db {
fmt.Println(k, " => ", v)
}
*reply = 1
fmt.Println("Finish syncing...")
return nil
}

func main() {
server := new(Server)
server.db = make(map[string]string)
rpcs := rpc.NewServer()
rpcs.Register(server)
me := "/var/tmp/test1122"
os.Remove(me)
l, e := net.Listen("unix", me)
if e != nil {
fmt.Println("listen error")
}

for true {
conn, e := l.Accept()
if e != nil {
fmt.Println("accept error")
conn.Close()
return
}
go func() {
rpcs.ServeConn(conn)
}()
}
}
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "net/rpc"
import "fmt"

func main() {
me := "/var/tmp/test1122"
c, e := rpc.Dial("unix", me)
if e != nil {
fmt.Println("dial error")
return
}
defer c.Close()

db := make(map[string]string)
db["name"] = "srg"
db["age"] = "25"
reply := 1
e = c.Call("Server.Sync", db, &reply)
if e != nil {
fmt.Println("sync error")
return
}
}

开两个terminal, 在$GOPATH目录先运行server.go, 再运行client.go, 最后server.go运行后的输出, 看来是可以的

1
2
3
4
5
$ go run server.go                                                             
Start syncing...
name => srg
age => 25
Finish syncing...

在我把这页的hint也看完的时候, 发现里面提到这个问题, 建议直接把这个map作为参数传递.

最后贴上过关纪念

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×