TT Lab
开始
学习 学习路径 课程

当时有两个领导者

多数派收到才算提交

在 TT Lab 中继续学习

目标

用规则写出领导者收到的值如何传播到三个节点,以及在哪一刻被“确定”。 结束后,你会对日志匹配、覆盖、提交条件,以及落后的节点追赶的过程,有亲身的体验。

为什么重要

复制并不是“把值发到多个地方”。而是区分发出去的和已确定的。 领导者收到值并写进自己的日志,这件事本身什么都不能保证——如果这个领导者下一刻 崩溃,这个值就可能变成从未发生过。只有过半数节点在同一位置拥有同一个条目, “任何未来的领导者都无法改变这个位置”才成立,而到那时才 能对客户端说成功。

提交条件里还附带着一个人们几乎总会漏掉的附加条件——领导者只有当自己任期的条目 到达过半数时,才会提高提交索引。如果仅仅因为前一个任期的条目到达了过半数就提交, 之后当选的另一个领导者就可能覆盖同一个位置,使已经宣布确定的值消失。

步骤

  1. 把 node.py 和 rules-for-replication.py 放到位,选出领导者,并写入 /root/raft/leader.json。
  2. 在 raftrules.py 中加入 up_to_date,并让 on_request_vote 同时检查这个条件。
  3. 在 /root/raft/raftlog.py 中编写 append_entry(log, term, value)。
  4. 在同一个文件中加入 log_ok(log, prev_index, prev_term)。
  5. 在同一个文件中加入 apply_entries(log, prev_index, entries)。
  6. 在同一个文件中加入 commit_index(counts, total, log, term)。
  7. 写入三个值,观察三个节点全部提交,并写入 /root/raft/replicated.json。
  8. 让一个追随者停掉再恢复,观察它追赶的过程,并写入 /root/raft/catchup.json。

参考

重新搭起上一个实验的成果

把 /opt/lab/raft/node.py 和 /opt/lab/raft/rules-for-replication.py 分别放到 /root/raft/node.py 和 /root/raft/raftrules.py,启动三个节点,并把选出的领导者写入 /root/raft/leader.json。

实验每次都从新的 Pod 开始,所以上一个实验中做的东西不会留下来。选举规则会直接提供上一个实验的答案。在 leader.json 中写 leader 和 term 两个键。

不给落后的候选人投票

在 raftrules.py 中加入 up_to_date(log, last_index, last_term),并修改 on_request_vote,让它在投票之前同时检查这个条件。

有了日志,选举就多了一个条件——只有候选人的日志至少与我的一样新,才投票。比较并不是先看长度。先看最后一个条目的任期,只有它相同时才用长度来决定。没有这个条件,落后的节点就可能当选领导者,覆盖已提交的条目。

客户端的值被追加到日志

在 /root/raft/raftlog.py 中编写 append_entry(log, term, value)。返回在末尾追加了带有 term 和 value 的条目之后的新列表。

条目中附带的不只是值,还有当时的任期。这个任期之后会成为比较两份日志的唯一依据。不要在原地修改传进来的列表,而要创建新列表并返回——接线另外保存着原件。

日志匹配

在 raftlog.py 中加入 log_ok(log, prev_index, prev_term)。判定 prev_index 位置的任期是否与 prev_term 相同。

Raft 不会比较整份日志。只要一个位置吻合,就认为它前面的全部都相同——这一性质叫作日志匹配(Log Matching)。prev_index 是从 1 开始计数的位置编号,0 表示“没有前面的位置”。如果我的日志比 prev_index 还短,就根本没有可比较的东西。

从对不上的位置开始截断

在 raftlog.py 中加入 apply_entries(log, prev_index, entries)。遇到对不上的位置,就从那里开始截断,并返回追加了新条目之后的列表。

关键在于“什么时候不截断”。如果只是同一任期的同一个条目再次到来,就不能动它后面的内容——在重新发送频繁的环境里,如果每次都截断,追随者的日志就会不断地变短又变长。没有任何条目的心跳也是同样的道理。

提交条件

在 raftlog.py 中加入 commit_index(counts, total, log, term)。counts 是各个服务器拥有的条目数,返回被过半数节点拥有、且该位置是当前任期条目的最高编号。

只有“过半数复制了就提交”是不够的。如果仅仅因为前一个任期的条目到达了过半数就提交,之后当选的领导者就可能用别的条目覆盖那个位置——这就是论文图 8 中的事故。所以领导者只有在自己任期的条目到达过半数时,才提高提交索引,这时它前面的条目也随之一起提交。

向三个节点写入值

启动三个节点,向领导者写入三个值,确认三个节点的 commit 都变成 3 之后,把 values、commit、log_len 写入 /root/raft/replicated.json。

值只写给领导者:curl -s -XPOST -d '{"value":"x"}' 127.0.0.1:5001/client。如果发给追随者,它会拒绝,同时告知领导者是谁。写入之后,commit 暂时还没有上升是正常的——要等追随者的响应回来,并且下一次心跳发出去之后才会反映出来。

恢复的节点追赶上来

让一个追随者停掉再恢复,确认它从空日志开始追赶了三格,并把 node、log_len、commit 写入 /root/raft/catchup.json。

这个简易实现不会把日志保存到磁盘。所以恢复的节点是在什么都不知道的状态下回来的,领导者会把发送的位置编号一格一格地往回退,找到吻合的位置,再从那里开始替它补齐。停掉它的方法,是像 pkill -f 'node.py --id 2 ' 这样把编号也指定上。恢复之后,亲眼看着 log_len 上升。