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

当时有两个领导者

亲手编写选举领导者的规则

在 TT Lab 中继续学习

目标

亲手实现三个进程互相交换选票、选出一个领导者的过程。 结束后,你会对任期(term)、投票、过半数,以及为什么要把超时设成随机值,有亲身的体验。

为什么重要

共识算法难,不是因为公式,而是因为边界。任期相同和更大的时候, 还没有投票和已经投过票的时候,票数刚好一半和超过一半的时候——这些边界 只要错位一格,系统平时运转正常,到了网络抖动的那天,就会造出两个领导者。 所以这里接线(HTTP 服务器、计时器、重发)已经预先提供,只有做判断的 函数由你来写。判断就是算法,其余的只是接线。

最后一步会用实测来展示 Raft 论文为什么要引入随机超时。 如果把三个节点的计时器设成一样,三个会同时成为候选人,各自给自己投一票, 没有任何一个能拿到过半数,任期就会无休止地上升。

步骤

  1. 在 /root/raft/cluster.json 中写入三个节点的 id 和端口。
  2. 把 /opt/lab/raft/node.py 复制为 /root/raft/node.py,启动其中一个,并确认 /status。
  3. 在 /root/raft/raftrules.py 中编写 election_timeout_ms(node_id, fixed_ms)。
  4. 在同一个文件中加入 on_timeout(state)。
  5. 在同一个文件中加入 on_request_vote(state, req)。
  6. 在同一个文件中加入 on_append_entries(state, req)。
  7. 在同一个文件中加入 has_majority(votes, total),启动三个节点,观察领导者被选出。
  8. 用 --timeout-ms 900 启动三个节点,并把结果写入 /root/raft/split-vote.json。

参考

三个节点的占位表

在 /root/raft/cluster.json 中写入 id 1、2、3 和端口 5001、5002、5003。

节点不是容器,而是只有端口不同的进程。先把之后三次启动要用的占位表写下来。在名为 nodes 的列表下,放三个带有 id 和 port 的对象。

把接线放到位并启动

把 /opt/lab/raft/node.py 复制为 /root/raft/node.py,启动其中一个,确认 /status 有响应。

HTTP 服务器和计时器之类的接线已经做好了。你要填的只有“判断”。用 python3 /root/raft/node.py --id 1 --port 5001 --peers '' 启动,然后在另一个窗口执行 curl 127.0.0.1:5001/status。规则文件还没有,所以它不做任何判断,静静地待着才是正常的。

选举超时

在 /root/raft/raftrules.py 中编写 election_timeout_ms(node_id, fixed_ms)。如果 fixed_ms 不为 0,就原样返回该值;如果为 0,就返回 800 到 1500 之间的随机值。

random.randint(a, b) 包含两个端点。为什么必须是随机的,最后一步会亲眼看到。一定要留下原样返回 fixed_ms 的路径——没有这条路径,最后一步的实验就无法成立。

超时就成为候选人

在 raftrules.py 中加入 on_timeout(state)。返回一个字典:任期加 1,把 state 改为 candidate,把 voted_for 改为自己的 id。

state 是带有 {'id', 'term', 'state', 'voted_for', 'leader', 'log', 'commit'} 的字典。只要把想改的键放进去返回,接线就会把它反映进去。候选人给自己投票,是 Raft 的第一票——漏掉它,就没有任何节点能拿到过半数。

一个任期一票

在 raftrules.py 中加入 on_request_vote(state, req)。返回包含 term、voted_for、state、granted 的字典。

按顺序看三件事。(1)如果 req 的任期比我的任期大,就提高我的任期并退位,同时清除本任期的投票记录。(2)如果任期比我的小,就拒绝——此时不能降低我的任期。(3)在同一个任期内,只有还没有投给任何人,或者投给的是同一个候选人时,才投票。如果对同一个候选人的重新发送也拒绝,那么丢一次包,选举就停住了。

接收心跳的一方

在 raftrules.py 中加入 on_append_entries(state, req)。返回包含 term、state、leader、voted_for、ok 的字典。

领导者即使没有话要说,也会持续发话——因为那是“我还活着”的唯一信号。如果 req 的任期比我的任期小,就把 ok 设为 False 返回,并保持我的任期不变。除此之外,就回到追随者状态,记录领导者,并把 ok 设为 True。作为候选人时,如果看到同一个任期的领导者,就必须退位。

数票并成为领导者

在 raftrules.py 中加入 has_majority(votes, total),然后在 5001、5002、5003 上启动三个节点,确认选出一个领导者。

过半数是“超过”一半,而不是“达到”一半以上。在 4 个节点中,2 票不是过半数——如果用整数除法来写,在这个边界上很容易出错,所以比较时把两边都乘以 2。启动三个节点之后,对三个端口都执行 curl 127.0.0.1:5001/status,同时查看 state、term 和 leader。

计时器相同,就没有谁能赢

用 --timeout-ms 900 启动全部三个节点,查看 9 秒后的状态,并把 fixed_timeout_ms、leader、max_term、states 写入 /root/raft/split-vote.json。

给了 --timeout-ms,接线就会把三个节点醒来的时刻对齐到物理时钟——这是为了看清计时器真的相同时会发生什么而设的装置。如果三个节点在同一瞬间成为候选人,各自给自己投一票,没有任何一个拿到过半数,只有任期在上升。leader 写成 null。