源代码参见我的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"