首页 > 代码库 > MIT 6.824 : Spring 2015 lab2 训练笔记
MIT 6.824 : Spring 2015 lab2 训练笔记
Lab 2:Primary/Backup Key/Value Service
Overview of lab 2
在本次实验中,我们将使用primary/backup replication 来提供能够容错的key/value service。为了让所有的clients和severs都认同哪个server是primary,哪个server是backup,我们将引入一个master service,叫viewservice。viewservice将监控那些可获取的server中哪些是死的,哪些是活的。如果当前的primary或者backup死了的话,viewservice将选择一个server去替代它。client通过检查viewservice来获取当前的primary。servers通过和viewservice合作来确保在任意时间至多只有一个primary。
我们的key/value service要能够对failed servers进行替换。当一个primary故障的时候,viewservice会从backup中选择一个作为新的primary。当一个backup故障或者被选为primary之后,如果有可用的空闲的server,viewservice就会将它变成backup。primary会将整个数据库都传送给新的backup,也会将之后的Puts操作的内容传送给backup,从而保证backup的key/value数据库和primary相同。
事实上,primary必须将Gets和Puts操作传送给backup(如果存在的话),并且直到收到backup的回应之后,才回复client。这能防止两个server同时扮演primary的角色(a "split brain")。例如:S1是primary,S2是backup。viewservice(错误地)认为S1死了并且将S2提升为新的primary。但是client仍然认为S1是primary,并且向它发送了一个operation。S1会将该operation传送给S2,S2将回复一个错误,告诉S1它不再是backup了(假设S2从viewservice中获得了新的view)。于是S1将返回给client一个错误,表明S1可能不再是primary了(因为S2拒绝了operation,因此肯定是一个新的view已经形成了)。之后,client将询问viewservice获取正确的primary(S2)并且向它发送operation。
发生故障的key/value server需要进行重启,但是此时我们不需要对replicated data(那些key和value)进行拷贝。这说明,我们的key/value server是将数据保存在内存而不是磁盘上的。只将数据保存在内存中的一个后果是,如果没有backup,primary发生故障了并且进行了重启操作,那么它将不能再扮演primary。
在clients和servers之间,不同的servers之间,以及不同的clients之间,RPC是唯一的交互方式。例如,不同的server实例之间是不允许共享Go变量或者文件的。
上文描述的设计存在一些容错和性能方面的限制,使它很难在现实世界中应用:
(1)、viewservice是非常脆弱的,因为它没有进行备份
(2)、primary和backup必须一次执行一个operation,限制了它们的性能
(3)、recovering server必须从primary中拷贝整个key/value对的数据库,即使它已经拥有了几乎是最新的数据,这是非常慢的(例如,可能因为网络的问题从而少了几分钟的更新)。
(4)、因为servers不将key/value数据库存放在磁盘中,因此不能忍受server的同时崩溃(例如,整个site范围内的断电)
(5)、如果因为一个临时的问题妨碍了primary和backup之间的通信,系统只有两种补救措施:改变view,从而消除通信障碍的backup,或者不断地尝试,不管是哪种方式,如果这样的问题老是发生的话,性能都不会很好
(6)、如果primary在确认它自己是primary的view之前发生故障了,那么viewservice将不能继续执行-----它将不断自旋并且不会改变view
在之后的实验中,我们将通过更好的设计和协议来解决这些限制。而本实验会让你明白在接下来的实验中将要解决哪些问题。
本实验中的primary/backup 方案并没有基于任何已知的协议。事实上,本实验并没有指定一个完整的协议,我们必须要自己对细节进行处理。本实验其实和Flat Datacenter Storage有些类似(viewservice就像FDS的metadata center,primary/backup server就像FDS中的tractserver),不过FDS花了更多的功夫在性能优化上。本实验的设计还和MongoDB中的replica set有些类似,虽然MongoDB是通过Paxos-like的选举来选择leader的。对于primary-backup-like protocal的细节描述,可以参见Chain Replication的实现。Chain Replication比本实验的设计有更好的性能,虽然它的viewservice并不会宣布一个server的死亡,如果它仅仅只是参与的话。参见Harp and Viewstamped Replication,可以发现它对高性能primary/backup 的细节处理以及在各种各样的故障之后对系统状态的重构操作。
Part A: The Viewservice
viewservice会经过一系列标号的view,每一个view都有一个primary和一个backup(如果有的话)。一个view由一个view number和view的primary和backup severs的identity(network port number)组成。一个view的primary必须是前一个view的primary或者backup。这确保了key/value的状态能够保存下来。当然有一个例外:当viewservice刚刚启动的时候,它要能够接受任何server作为第一个primary。view中的backup可以是除了primary之外的任何一个server,如果没有可用的server的话,也可以没有backup。(通过空字符串表示,“”)
每一个key/value server都会在每隔一个PingInterval发送一个Ping RPC给viewservice,viewservice则会回复当前view的描述。Ping让viewservice知道key/value server仍然活着,同时通知了key/value server当前的view,还让viewservice了解key/value server知道的最新的view。如果viewservice经过DeadPings PingIntervals还没有从server收到一个Ping,那么viewservice认为该server已经死了。当一个server在崩溃重启之后,它需要向viewservice发送一个或多个带有参数0的Ping来告知viewservice它崩溃过了。
当(1)viewservice没有从primary和backup中获取最新的Ping,(2)primary或者backup崩溃并且重启了,(3)如果当前没有backup并且有空闲的server出现的时候(一个server ping过了,但是它既不是primary也不是backup),viewservice 都会进入一个新的view。但是在当前view的primary确认它正在当前的view进行操作之前(通过发送一个带有当前view number的Ping),viewservice是一定不能改变view的。当viewservice仍然未收到当前view的primary对于当前view的acknowledgment之前,它不能改变view,即使它认为primary或者backup已经死了。简单地说就是,viewservice不能从view X进入view X+1,如果它还没有从view X的primary接收到Ping(X)。
这个acknowledge规则防止了viewservice的view超过key/value server一个以上。如果viewservice能领先任意个view,那么我们需要更加复杂的设计,从而保证在viewservice中保存view的历史,从而能让key/value server能够获得之前老的view,并且要在合适的时候对老的view进行回收。这种acknowledgment规则的缺陷是,如果primary在它确认自己是primary的view之前出现故障了,那么viewservice就不能再改变view了。
源码分析之ViewService部分
ViewServer结构如下所示:
type ViewServer struct { mu sync.Mutex l net.Listener dead int32 // for testing rpccount int32 // for testing me string // Your declaration here. }
// src/viewservice/server.go
func StartServer(me string) *ViewServer
(1)、首先填充一个*ViewServer的数据结构
(2)、调用rpcs := rpc.NewServer()和rpcs.Register(vs),注册一个rpc server
(3)、调用l, e := net.Listen("unix", vs.me),vs.l = l建立网络连接
(4)、生成两个goroutine,一个用于接收来自client的RPC请求并生成goroutine处理,另一个goroutine每隔PingInterval调用一次tick()
源码分析之Clerk部分
Clerk结构如下所示:
// the viewservice Clerk lives in the client and maintains a little state type Clerk struct { me string // client‘s name (host:port) server string // viewservice‘s host:port }
// src/viewservice/client.go
func MakeClerk(me string, server string) *Clerk
该函数只是简单地填充一个*Clerk结构并返回而已
// src/viewservice/client.go
func (ck *Clerk) Ping(viewnum int) (View, error)
创建变量args := &PingArgs{},并进行填充,接着调用ok := call(ck.server, "ViewServer.Ping", args, &reply)且返回reply.View
源码分析之view部分
View结构如下所示:
type View struct { Viewnum int Primary string Backup string }
Ping相关的结构如下所示:
// If Viewnum is zero, the caller is signalling that it is alive and could become backup if needed type PingArgs struct { Me string // "host:port" Viewnum uint // caller‘s notion of current view # }
type PingReply struct {
View View
}
Get相关的结构如下:
// Get(): fetch the current view, without volunteering to be a server.mostly for clients of p/b service, and for testing type GetArgs struct { } type GetReply struct { View View }
// the viewserver will declare a client dead if it misses this many Ping RPCs in a row
const DeadPings = 5
------------------------------------------------------------------------------------------------ 测试框架分析 -----------------------------------------------------------------------------------------------
1、src/viewservice/test_test.go
func check(t *testing.T, ck *Clerk, p string, b string, n uint)
该函数首先调用view, _ := ck.Get()获取当前的view,并比较view的Primary,Backup和p, b是否相等,并且在n不为0的时候,比较n和view.Viewnum是否相等。
最后调用ck.Primary()比较和p是否相等。
2、src/viewservice/test_test.go
func Test1(t *testing.T)
(1)、首先调用runtime.GOMAXPROCS(4),再指定viewservice的port,vshost := port("v"),格式为“/var/tmp/824-${uid}/viewserver-${pid}-v”
(2)、调用vs := StartServer(vshost)启动viewservice
(3)、调用cki := MakeClerk(port("i"), vshost),i = 1, 2, 3,启动3个server
(4)、当ck1.Primary() 不为空时,则报错,因为此时不应该有primary。
Test: First primary ...
每隔一个PingInterval ck1都调用一次ck1.Ping(0)操作,直到返回的view.Primary 为ck1.me退出,最多循环DeadPings * 2次
Test:First backup...
首先调用vx, _ := ck1.Get获取当前的view,每隔PingInterval ck1都调用一次ck1.Ping(1),之后ck2调用view, _ := ck2.Ping(0)操作,直到返回的view.Backup为ck2.me时退出,最多循环DeadPings * 2次。
Test:Backup takes over if primary fails...
首先通过调用ck1.Ping(2)确认以下view 2。再调用vx, _ := ck2.Ping(2)获取viewservice当前的view,再每隔PingInterval 调用一次v, _ := ck2.Ping(vx.Viewnum),直到v.Primary == ck2.me并且v.Backup == ""为止,最多循环DeadPings * 2次。
MIT 6.824 : Spring 2015 lab2 训练笔记