2. 一步一步带你实现raft(2A)

目标:

Implement leader election and heartbeats (AppendEntries RPCs with no log entries). The goal for Part 2A is for a single leader to be elected, for the leader to remain the leader if there are no failures, and for a new leader to take over if the old leader fails or if packets to/from the old leader are lost. Run go test -run 2A to test your 2A code.

第一步

Add any state you need to the Raft struct in raft.go. You'll also need to define a struct to hold information about each log entry. Your code should follow Figure 2 in the paper as closely as possible.

2. 一步一步带你实现raft(2A)_第1张图片
image.png

同时为了表明一台RAFT SERVER的状态,我们需要一个枚举类。

定义LOG的时候,注意大写,因为这个会在RPC传输的时候序列化。相关博客

2. 一步一步带你实现raft(2A)_第2张图片
image.png

第二步 实现GET STATE

有了上面的定义,很好实现


2. 一步一步带你实现raft(2A)_第3张图片
image.png

第三步 实现RequestVoteArgs,RequestVoteReply

2. 一步一步带你实现raft(2A)_第4张图片
image.png

2. 一步一步带你实现raft(2A)_第5张图片
image.png

下面代码框架里并没有给AppendEntries的REQUEST 和 REPLY,因为LEADER选举,需要发心跳,所以我们不妨也先实现。

第四步 实现RequestVoteArgs,RequestVoteReply

2. 一步一步带你实现raft(2A)_第6张图片
image.png

2. 一步一步带你实现raft(2A)_第7张图片
image.png

第五步(来自HINT)

Modify Make() to create a background goroutine that will kick off leader election periodically by sending out RequestVote RPCs when it hasn't heard from another peer for a while. This way a peer will learn who is the leader, if there is already a leader, or become the leader itself.

  1. 初始化成员变量


    2. 一步一步带你实现raft(2A)_第8张图片
    image.png
  2. 因为Make() must return quickly, so it should start goroutines, 所以开一个goroutine

  3. kick off leader election periodically by sending out RequestVote RPCs when it hasn't heard from another peer for a while, 这里涉及到超时机制,我们需要用到SELECT 和CHANNEL

  4. 定义channel, 当收到了rpc,需要打破SELECT等待


    2. 一步一步带你实现raft(2A)_第9张图片
    image.png
  5. 实现select , channel(根据当前SERVER不同的身份,来监听不同的CHANNEL)


    2. 一步一步带你实现raft(2A)_第10张图片
    image.png

上面的代码完成了下图的状态转换的红框部分


2. 一步一步带你实现raft(2A)_第11张图片
image.png

第6步,实现beCandidate()

2. 一步一步带你实现raft(2A)_第12张图片
image.png

第7步, 实现startElection

重读下段论文

为了开始选举,一个追随者会自增它的当前任期并且转换状态为候选人。然后,它会给自己投票并且给集群中的其他服务器发送 RequestVote RPC。一个候选人会一直处于该状态,直到下列三种情形之一发生:

  • 它赢得了选举;
  • 另一台服务器赢得了选举;
  • 一段时间后没有任何一台服务器赢得了选举

这些情形会在下面的章节中分别讨论。

一个候选人如果在一个任期内收到了来自集群中大多数服务器的投票就会赢得选举。在一个任期内,一台服务器最多能给一个候选人投票,按照先到先服务原则(first-come-first-served)(注意:在 5.4 节 针对投票添加了一个额外的限制)。大多数原则使得在一个任期内最多有一个候选人能赢得选举(表 -3 中提到的选举安全原则)。一旦有一个候选人赢得了选举,它就会成为领导人。然后它会像其他服务器发送心跳信息来建立自己的领导地位并且组织新的选举。

当一个候选人等待别人的选票时,它有可能会收到来自其他服务器发来的声明其为领导人的 AppendEntries RPC。如果这个领导人的任期(包含在它的 RPC 中)比当前候选人的当前任期要大,则候选人认为该领导人合法,并且转换自己的状态为追随者。如果在这个 RPC 中的任期小于候选人的当前任期,则候选人会拒绝此次 RPC, 继续保持候选人状态。

第三种情形是一个候选人既没有赢得选举也没有输掉选举:如果许多追随者在同一时刻都成为了候选人,选票会被分散,可能没有候选人能获得大多数的选票。当这种情形发生时,每一个候选人都会超时,并且通过自增任期号和发起另一轮 RequestVote RPC 来开始新的选举。然而,如果没有其它手段来分配选票的话,这种情形可能会无限的重复下去。

Raft 使用随机的选举超时时间来确保第三种情形很少发生,并且能够快速解决。为了防止在一开始是选票就被瓜分,选举超时时间是在一个固定的间隔内随机选出来的(例如,150~300ms)。这种机制使得在大多数情况下只有一个服务器会率先超时,它会在其它服务器超时之前赢得选举并且向其它服务器发送心跳信息。同样的机制被用于选票一开始被瓜分的情况下。每一个候选人在开始一次选举的时候会重置一个随机的选举超时时间,在超时进行下一次选举之前一直等待。这能够减小在新的选举中一开始选票就被瓜分的可能性。9.3 节 展示了这种方法能够快速的选出一个领导人。

实现的思路

开始选举,无非就是向其他节点发送REQUEST VOTE RPC。那么就要构造REQUEST VOTE ARG, 同时把 REQUEST VOTE REPLY 的指针传进去,好拿到结果。


2. 一步一步带你实现raft(2A)_第13张图片
image.png
2. 一步一步带你实现raft(2A)_第14张图片
allserver.png
2. 一步一步带你实现raft(2A)_第15张图片
image.png

这里因为是开协程去做,所以更新票数的时候要用atomic

2. 一步一步带你实现raft(2A)_第16张图片
image.png
2. 一步一步带你实现raft(2A)_第17张图片
image.png

第8步, 实现beFollower,beLeader

2. 一步一步带你实现raft(2A)_第18张图片
image.png

第9步(来自HINT)

Implement the RequestVote() RPC handler so that servers will vote for one another.

2. 一步一步带你实现raft(2A)_第19张图片
allserver.png

2. 一步一步带你实现raft(2A)_第20张图片
image.png

2. 一步一步带你实现raft(2A)_第21张图片
image.png

现在状态转换又实现了3个步骤


2. 一步一步带你实现raft(2A)_第22张图片
image.png

第9步 (来自HINT)

To implement heartbeats, define an AppendEntries RPC struct (though you may not need all the arguments yet), and have the leader send them out periodically. Write an AppendEntries RPC handler method that resets the election timeout so that other servers don't step forward as leaders when one has already been elected.

2. 一步一步带你实现raft(2A)_第23张图片
image.png
2. 一步一步带你实现raft(2A)_第24张图片
image.png
2. 一步一步带你实现raft(2A)_第25张图片
image.png

因为只是做一个心跳,所以LOG不用真的传真的写


2. 一步一步带你实现raft(2A)_第26张图片
image.png

2. 一步一步带你实现raft(2A)_第27张图片
image.png
2. 一步一步带你实现raft(2A)_第28张图片
image.png

调试阶段

BUG 1

image.png

2. 一步一步带你实现raft(2A)_第29张图片
image.png

2. 一步一步带你实现raft(2A)_第30张图片
image.png

i 应该变成 idx

BUG 2

2. 一步一步带你实现raft(2A)_第31张图片
image.png

做边界检查,代码改动如下

2. 一步一步带你实现raft(2A)_第32张图片
image.png

WARNING 1

虽然过了测试,但是打了个WARNING给我。我修改了TESTCODE,加了点输出,发现即使网络没有坏,LEADER也换了。为什么呢?


2. 一步一步带你实现raft(2A)_第33张图片
image.png

image.png

那一定就是FOLLOWER或者CANDIDATE等待超时了,才会去更新CURRENT TERM。那么超时是受HEARTBEAT来维持的。检查HEARTBEAT代码,发现问题。

再最后发送REPLY了之后,要去SEND APPLY ENTRY RPC 的CHANNEL

加上代码


2. 一步一步带你实现raft(2A)_第34张图片
image.png

PASS 2A

2. 一步一步带你实现raft(2A)_第35张图片
image.png

Add lock

上面的实现是没有带锁的,所以在run go test -race的时候会收到warning

阅读下边这个链接里的文章。

  • If you are puzzled about locking, you may find this advice helpful.

按照里面的思路,

我的策略如下:

  1. 在所有用到共享数据的地方加锁。
  2. 不要在阻塞等待的地方持有锁。
  3. 一些方法 是为了简化实现的,在外层加锁。比如beCandidate,因为Golang中的sync.Mutex是不可重入的
  4. Similarly, reply-handling code after the Call() must re-check all relevant assumptions after re-acquiring the lock

红色为加锁,蓝色为阻塞


2. 一步一步带你实现raft(2A)_第36张图片
image.png

2. 一步一步带你实现raft(2A)_第37张图片
image.png

2. 一步一步带你实现raft(2A)_第38张图片
image.png

2. 一步一步带你实现raft(2A)_第39张图片
image.png

2. 一步一步带你实现raft(2A)_第40张图片
image.png

2. 一步一步带你实现raft(2A)_第41张图片
image.png
2. 一步一步带你实现raft(2A)_第42张图片
image.png

你可能感兴趣的:(2. 一步一步带你实现raft(2A))