前两天删了在github上的6.824, 因为课程里说了不要把源码放在公共的地方, 之前没删一直心有歉意, 还好进度没这么快, 应该对别人也不会造成影响.
言归正传, 这星期空余时间一直在看Lab2的Part B, 今天因为身体不适, 请假一天在家, 开始实现思路, 并记录一下自己遇到的一些大小问题.
目标 实现基于viewservice(Lab1实现的视图服务器, 用来记录主副服务器的地址)的键值服务器, 分别为primary(主服务器),Backup(备份服务器), 客户端通过viewservice获得primary的地址, 发送键值的存取请求, primary在响应请求的同时, 将数据备份到backup服务器. 实现完成后, 通过所有的unit test.
架构
viewservice
存储并提供primary以及backup的信息
接受primary, backup的定时ping请求, 更新或保持两个服务器的位置
提供查询当前服务器状态
primary
接受client的请求
定期向viewservice报备自己的状态
在backup服务器更换时负责将自身全部数据同步过去
backup
作为primary的备份存储
定期向viewservice报备自己的状态
在primary挂了之后, 提升为primary, 负责client的请求
client
向primary发送数据存储,查询的请求
若primary返回”错误的服务器”时, 询问viewservice, 更新primary
遇到的挑战
实现时有些golang的特性不是特别了解, 一边打开golang的官方文档, 一边poc.
决定在什么时候需要容错? 是否可恢复? 在反复重试时, 是否要定最大重试次数, 以及失败的处理.
单元测试 当我运行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 mainimport "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作为参数传递.