亲手编写选举领导者的规则
目标
亲手实现三个进程互相交换选票、选出一个领导者的过程。 结束后,你会对任期(term)、投票、过半数,以及为什么要把超时设成随机值,有亲身的体验。
为什么重要
共识算法难,不是因为公式,而是因为边界。任期相同和更大的时候, 还没有投票和已经投过票的时候,票数刚好一半和超过一半的时候——这些边界 只要错位一格,系统平时运转正常,到了网络抖动的那天,就会造出两个领导者。 所以这里接线(HTTP 服务器、计时器、重发)已经预先提供,只有做判断的 函数由你来写。判断就是算法,其余的只是接线。
最后一步会用实测来展示 Raft 论文为什么要引入随机超时。 如果把三个节点的计时器设成一样,三个会同时成为候选人,各自给自己投一票, 没有任何一个能拿到过半数,任期就会无休止地上升。
步骤
- 在
/root/raft/cluster.json中写入三个节点的 id 和端口。 - 把
/opt/lab/raft/node.py复制为/root/raft/node.py,启动其中一个,并确认/status。 - 在
/root/raft/raftrules.py中编写election_timeout_ms(node_id, fixed_ms)。 - 在同一个文件中加入
on_timeout(state)。 - 在同一个文件中加入
on_request_vote(state, req)。 - 在同一个文件中加入
on_append_entries(state, req)。 - 在同一个文件中加入
has_majority(votes, total),启动三个节点,观察领导者被选出。 - 用
--timeout-ms 900启动三个节点,并把结果写入/root/raft/split-vote.json。
参考
- 启动节点的方法:
cd /root/raft && python3 node.py --id 1 --port 5001 --peers 2:5002,3:5003 & - 查看状态:
curl -s http://127.0.0.1:5001/status - 全部关闭:
pkill -f 'node.py --id' - 接线只会调用规则文件中已有的函数。不会因为还没写的函数而导致进程崩溃。
- 常见错误 1:收到过期任期的请求,却把我的任期降低到对方的值。任期只增不减。
- 常见错误 2:把过半数写成
votes >= total / 2。在 4 个节点中,2 票就成了过半数,会出现两个领导者。
三个节点的占位表
在 /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。