源代码参见我的github:https://github.com/YaoZengzeng/MIT-6.824
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
Part B: The primary/backup key/value service
Clients通过创建一个Clerk object来使用service,并且调用它的方法来给service传递RPC。我们的key/value service应该能够持续执行正确的操作,如果不存在没有一个server可用的时刻。同时,当发生部分故障时,也应该要执行正确:例如一个server遇到了短暂的网络问题,但是没有崩溃,或者能够和一些机器进行通信,和其他一些机器不能通信。如果我们的service仅仅运行在一台机器上,那么它应该要能够利用起刚刚恢复的或者空闲的server(作为backup),从而能够忍受server的故障。
正确的操作意味着在调用Clerk.Get(k)返回key对应的最新的value。如果该key从未被设置过,那么value是一个空的字符串,否则它就是经过连续的Clerk.Put(k, v)或者Clerk.Append(k, v)之后的值。所有的操作应该要提供at-most-once语义
不过我们需要假设viewservice不会宕机或崩溃
我们的clients和servers都只能通过RPC进行通信,clients和servers都必须通过client.go中的call()来发送RPCs请求。
我们必须保证在每一时刻只有一个primary。我们必须非常清楚为什么要这样设计。例如:在一些view中,S1是primary;之后viewservice改变了view,S2变成了primary,但是S1并不知道新的view并且依然认为它是primary。之后,一些clients和S1进行通信,一些clients和S2进行通信,并且它们看不到互相的Puts()操作。
如果一个server不是primary,那么它不应该回复clients,或者返回给clients一个错误;它应该设置GetReply.Err或者PutReply.Err而不是返回OK。
Clerk.Get(),Clerk.Put(), Clerk.Append()应该完成了完整操作之后再返回。这意味着,Put()/Append()在更新key/value数据库之前不断尝试,而Clerk.Get()应该不断尝试直到获取key对应的当前的value(如果存在的话)。我们的server应该能够解析出因为clients不断重试产生的重复的RPC从而确保操作的at-most-once语义。我们可以假设每个clerk每一时刻只进行一个Put或Get操作。仔细想想Put操作的commit point。
一个server不应该每获取一个Put/Get就和viewservice进行对话,因为这会让viewservice成为性能和容错的瓶颈。事实上,servers应该定期地Ping viewservice去获取最新的view。同样地,client也不应该每发送一个RPC就和viewservices进行通信,相反,Clerk应该对当前的primary进行缓存,并且只在当前primary貌似已经死亡的时候才和viewservices进行通信。
one-primary-at-a-time策略部分依赖于viewservice只能将view i的backup提升为view i+1的primary。如果view i的old primary试着处理一个client request,它会先将请求发送给它的backup。如果backup还没有听到view i+1,那么它就还没像primary一样工作,所以不会有什么不好的影响。如果backup已经听到了view i+1并且已经作为primary工作了,那么它应该已经知道怎么处理来自old primary的client request了。
我们应该确保backup能够看到key/value数据库的每一次更新操作,通过primary用完整的key/value数据库对它进行初始化,以及转发之后所有的client operations。我们的primary应该只将每个Append()的参数转发给backup,不要转发结果值,因为可能很大。
源码分析之pbservice.server
PBservice结构如下所示:
type PBServer struct { mu sync.Mutext l net.Listener dead int32 // for testing unreliable int32 // for testing me string vs *viewservice.Clerk // Your declaration here. }
// src/pbservice/server.go
1、func StartServer(vshost string, me string) *PBServer
(1)、该函数主要工作是填充数据结构PBServer:pb := new(PBServer),pb.me = me,pb.vs = viewservice.MakeClerk(me, vshost)
(2)、之后再创建一个rpc server,并且调用l, e := net.Listen("unix", pb.me)和pb.l = l对地址进行监听
(3)、之后再生成两个goroutine,第一个goroutine用于处理rpc请求,另一个goroutine用于定期调用pb.tick()
// src/pbservice/server.go
func (pb *PBServer) setunreliable(what bool)
当what为true时,设置pb.unreliable为1,否则设置pb.unreliable为0
// tell the server to shut itself down.
func (pb *PBServer) kill()
设置pb.dead为1,再调用pb.l.Close()
源码分析之pbservice.client
Clerk的数据结构如下所示:
type Clerk struct { vs *viewservice.Clerk // Your declaration here }
// src/pbservice/client.go
1、func MakeClerk(vshost string, me string) *Clerk
该函数仅仅只是对Clerk进行填充
------------------------------------------------------------------------------------------------ 测试框架分析 -----------------------------------------------------------------------------------------------
Part A:
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。
primary: ck1
Test: First primary ...
每隔一个PingInterval ck1都调用一次ck1.Ping(0)操作,直到返回的view.Primary 为ck1.me退出,最多循环DeadPings * 2次
primary: ck1, backup: ck2
Test:First backup...
首先调用vx, _ := ck1.Get获取当前的view,每隔PingInterval ck1都调用一次ck1.Ping(1),之后ck2调用view, _ := ck2.Ping(0)操作,直到返回的view.Backup为ck2.me时退出,最多循环DeadPings * 2次。
primary: ck2
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次。
primary: ck2, backup: ck1
Test:Restarted server becomes backup...
首先调用vx, _ := ck2.Get()和ck2.Ping(vx.Viewnum)来ack当前的view,再执行ck1.Ping(0)和v, _ := ck2.Ping(vx.Viewnum)操作,
直到v.Primay == ck2.me && v.Backup == ck1.me为止,最多循环DeadPings * 2次
primary: ck1, backup: ck3
Test:Idle third server becomes backup if primary fails...
// start ck3, kill the primary (ck2), the previous backup (ck1) should become server, and ck3 the backup.
// this should happen in a single view change, without any period in which there's no backup
首先调用vx, _ := ck2.Get()和ck2.Ping(vx.Viewnum)确认当前的view,然后调用ck3.Ping(0); v, _ := ck1.Ping(vx.Viewnum),来等待primary的死亡,
backup变为primary,idle server变为backup。
primary: ck3
Test:Restarted primary treated as dead ...
// kill and immediately restart the primary -- dose viewservice conclude primary is down even though it's pinging
首先还是确认当前view,再循环调用ck1.Ping(0)和ck3.Ping(vx.Viewnum),直到v.Primary != ck1.me为止,最多循环DeadPings * 2次
跳出循环后再调用vy, _ := ck3.Get(),当vy.Primary != ck3.me时,报错
Test:Dead backup is removed from view...
// set up a view with just 3 as primary to prepare for the next test
循环 DeadPings * 3次,使得只有ck3这个server存在,作为primary
Test:Viewserver waits for primary to ack view...
// does viewserver wait for ack of previous view before starting the next one
// set up p=ck3, b=ck1, but do not ack
调用vx,_ := ck1.Get(),ck1.Ping(0),ck3.Ping(vx.Viewnum),使得viewservice进入下一个view。但是primary并不ack。
// ck3 is the primary , but it never acked. let ck3 die, check that ck1 is not promoted
调用vy, _ := ck1.Get(),调用DeadPings * 3次ck1.Ping(vy.Viewnum),再进行check,只要原本是backup的ck1不变为primary即可
Test:Uninitialized server can't become primary...
// if old servers die, check that a new (uninitialized) server cannot take over
循环DeadPings * 2次:v, _ := ck1.Get(),ck1.Ping(v.Viewnum),ck2.Ping(0),ck3.Ping(v.Viewnum)
Part B:
// check函数
func check(ck *Clerk, key string, value string):
首先调用v := ck.Get(key)获取key对应的值,再将该值和预期的value进行对比,有错则退出。
// check that all known appends are present in a value,
// and are in order for each concurrent client.
func checkAppends(t *testing.T, v string, counts []int):
针对每个client,检验每个扩展元素的顺序是否正确,以及是否存在重复
func proxy(t *testing.T, port string, delay *int32)
// 其中port就是s1的地址
(1)、首先创建变量portx := port + "x",再调用os.Rename(port, portx),l, err := net.Listen("unix", port)
(2)、接下来做的实际操作就是先从l调用Accept操作,休眠delay秒,然后再启动一个client Dial portx端口,最后将l中读入的数据写入client,再将这些数据从client写回l,从而达到延时的效果。
// src/test_test.go
func TestBasicFail(t *testing.T):
1、Test: Single primary, no backup...
(1)、调用vshost := port(tag+"v", 1)创建viewservice的通信端口。再调用vs := viewservice.StartServer(vshost)和vck := viewservice.MakeClerk("", vshost)分别创建viewservice和clerk。
(2)、再调用ck := MakeClerk(vshost, "")创建一个clerk名为ck,以及s1 := StartServer(vshost, port(tag, 1))
设置deadtime := viewservice.PingInterval * viewservice.DeadPings,再睡眠deadtime,调用vck.Primary() != s1.me则返回错误,再连续调用ck.Put()和ck.Append(),并用check()进行确认。
2、Test: Add a backup ...
调用s2 := StartServer(vshost, port(tag, 2))启动backup,并确认s2是backup。再进行一次Put操作,之后暂停3 * viewservice.PingInterval时间,等待backup初始化完成,最后,再调用一个一次Put操作。
3、Test: Primary failure...
调用s1.kill()杀死primary,并确认s2变成了primary,最后做一些check操作
// kill solo server, start new server, check that it does not start serving as primary
4、Test: kill last server, new one should not be active...
首先调用s2.kill()杀死s2,此时没有可用的server,再调用s3 := StartServer(vshost, port(tag, 3))启动一个新的server,再进行Get操作,如果不能成功,则测试通过
func TestAtMostOnce(t *testing.T):
(1)、首先启动一个vshost和vck,再启动nservers个server,并且对每个server调用setunreliable(true),其中nservers为1。
(2)、循环viewservice.DeadPings * 2次,直到view.Primay和view.Backup都不为空时退出循环
(3)、休眠viewservice.PingInterval * viewservice.DeadPings,give p+b time to ack, initialize
(4)、创建一个client,调用ck.Append()一百次,每次扩展索引号i,最后测试
// Put right after a backup dies
func TestFailPut(t *testing.T ):
(1)、启动viewservice和3个server,直到Primary和Backup不为空为止,休眠1s,等待backup初始化完成,确定Primary为S1,Backup为S2
(2)、进行一系列的Put操作,再kill Backup,之后马上进行Put操作。循环viewservice.DeadPings * 3次,直到Viewnum更新,Priamry和Backup不为空为止
(3)、休眠1s,直到Backup初始化完成,并且确保Primary为S1,Backup为S3,之前的Put操作结果正确
(4)、kill Primary,然后马上进行Put操作,循环DeadPings * 3次,直到Viemnum更新,Primary不为空为止,最后对结果进行测试
// do a bunch of concurrent Put()s on the same key,
// then check that primary and backup have identical values.
// i.e. that they processed the Put()s in the same order
func TestConcurrentSame(t *testing.T):
(1)、启动viewservice和两个server,分别作为Priamry和Backup。
(2)、启动三个goroutine,并行地进行Put操作
(3)、从Primary中读取之前Put的key的值
(4)、kill Primary,再从之前的Backup,现在的Primary中读取值,并检验Backup和Primary的数据是否一致
// do a bunch of concurrent Append()s on the same key,
// then check that primary and backup have identical values.
// i.e. that they processed the Append()s in the same order.
func TestConcurrentSameAppend(t *testing.T):
(1)、启动viewservice和两个server,分别作为Primary和Backup
(2)、启动三个goroutine,每个goroutine都进行顺序地append操作
(3)、检验Primary中的操作是否正确
(4)、kill Primary,再从之前的Backup,现在的Primary中读取值,并检验Backup和Primary的数据是否一致
func TestConcurrentSameUnreliable(t *testing.T):
(1)、启动viewservice和两个unreliable的server,分别作为Primary和Backup
(2)、启动三个goroutine,并行地进行Put操作
(3)、检验Primary中的操作是否正确
(4)、kill Primary,再从之前的Backup,现在的Primary中读取值,并检验Backup和Primary中的数据是否一致
// constant put/get while crashing and restarting servers
func TestRepeatedCrash(t *testing.T):
(1)、启动viewservice和三个server
(2)、启动一个goroutine,用于kill server并且重启,而且会等待足够长的时间,用于新form的形成和Backup的初始化
(3)、启动三个goroutine,并发地进行Put操作并用Get检验
(4)、最后再进行一次Put操作并检验
func TestRepeatedCrashUnreliable(t *testing.T):
(1)、启动viewservice和三个unreliable的server
(2)、启动一个goroutine,用于kill server并且重启,而且会等待足够长的时间,用于新form的形成和Backup的初始化
(3)、启动两个goroutine,针对同一个key进行持续的Append操作,最后对Append的结果进行检验
(4)、最后再进行一次Put操作,并检验结果
func TestPartition1(t *testing.T):
Test: Old primary does not server Gets ...
(1)、启动viewservice和一个server s1,该server走vshosta
(2)、创建变量vshosta := vshost + "a",并创建vshosta到vshost的软链接,调用proxy(t, port(tag, 1), &delay),proxy只是做了一个延时操作,仅此而已
(3)、再启动一个server s2作为backup,再进行一次Put("a", "1")操作并检查,最后删除vshosta
// start a client Get(), but use proxy to delay it long enough that it won't reach s1 until after s1 is no longer the primary
(4)、将delay的值设为4,创建一个管道stale_get,并启动一个goroutine,做一次Get("a")操作,如果Get的结果和之前的Put操作一致,则从管道输出true,否则输出false
// now s1 cannot talk to viewserver, so view will change, and s1 won't immediately realize
(5)、循环,直到s2变为primary
// wait long enough that s2 is guaranteed to have Pinged the viewservice, and thus that s2 must know about the new view
(6)、睡眠两个viewservice.PingInterval,并且改变键"a"的值为"111"
(7)、如果从上文的管道中得到的值不为空,则出错,最后检查键"a"的值是否为"111"
func TestPartition2(t *testing.T)
(1)、启动viewservice和一个server s1,该server走vshosta
(2)、创建变量vshosta := vshot + "a",并创建vshosta到vshost的软链接
(3)、再启动server s2作为backup,再进行一次Put("a", "1")操作并检查,最后删除vshosta
// start a client Get(),but use proxy to delay it long enough that it won't reach s1 until s1 is no longer the primary
(4)、将delay的值设为5,创建一个管道stale_get,并启动一个goroutine,做一次Get("a")操作,如果Get的结果和之前的Put操作一致,则从管道输出true,否则输出false
// now s1 cannot talk to viewserver, so view will change, so view will change
(5)、循环,直到s2变为primary
(6)、再启动一个server s3,循环等待直到s2变为primary,s3变为backup,做一次Put("a", "2")操作并检查
(7)、kill s2,如果从上文的管道中得到的值不为空,则出错,最后检查键"a"的值是否为"2"