多数派收到才算提交
目标
用规则写出领导者收到的值如何传播到三个节点,以及在哪一刻被“确定”。 结束后,你会对日志匹配、覆盖、提交条件,以及落后的节点追赶的过程,有亲身的体验。
为什么重要
复制并不是“把值发到多个地方”。而是区分发出去的和已确定的。 领导者收到值并写进自己的日志,这件事本身什么都不能保证——如果这个领导者下一刻 崩溃,这个值就可能变成从未发生过。只有过半数节点在同一位置拥有同一个条目, “任何未来的领导者都无法改变这个位置”才成立,而到那时才 能对客户端说成功。
提交条件里还附带着一个人们几乎总会漏掉的附加条件——领导者只有当自己任期的条目 到达过半数时,才会提高提交索引。如果仅仅因为前一个任期的条目到达了过半数就提交, 之后当选的另一个领导者就可能覆盖同一个位置,使已经宣布确定的值消失。
步骤
- 把
node.py和rules-for-replication.py放到位,选出领导者,并写入/root/raft/leader.json。 - 在
raftrules.py中加入up_to_date,并让on_request_vote同时检查这个条件。 - 在
/root/raft/raftlog.py中编写append_entry(log, term, value)。 - 在同一个文件中加入
log_ok(log, prev_index, prev_term)。 - 在同一个文件中加入
apply_entries(log, prev_index, entries)。 - 在同一个文件中加入
commit_index(counts, total, log, term)。 - 写入三个值,观察三个节点全部提交,并写入
/root/raft/replicated.json。 - 让一个追随者停掉再恢复,观察它追赶的过程,并写入
/root/raft/catchup.json。
参考
- 本实验大约需要 60 分钟。如果会话时间不够,可以在界面中延长。实验 Pod 没有挂载
卷,会话结束后
/root中的工作成果会消失——想保留的代码要另外复制保存。 - 写入值:
curl -s -XPOST -d '{"value":"x"}' 127.0.0.1:5001/client - 查看日志:
curl -s 127.0.0.1:5001/log - 查看状态:
curl -s 127.0.0.1:5001/status(同时查看 commit 和 log_len) - 常见错误 1:对没有条目的心跳也截断日志。只有对不上的时候才截断。
- 常见错误 2:在原地修改传进来的日志列表。要创建新的列表并返回。
重新搭起上一个实验的成果
把 /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 上升。