隔离、两边都写、再恢复
目标
把三个节点中的一个从其余节点上隔离开再恢复,亲自观察同一时刻出现两个领导者的状态, 以及那时写入各侧的值的命运。
为什么重要
分布式系统的事故,通常不是“故障”,而是来自不完整的信息。被隔离的领导者 不知道自己被隔离了。它所知道的只有“最近没有响应”,而 Raft 的领导者没有 自行退位的机制。所以它会继续像领导者一样行事,接收客户端的值, 并返回 ok。同一时刻,在另一侧,过半数节点聚在一起,选出任期更高的新领导者。
这时领导者确实是两个。Raft 阻止的不是出现两个领导者,而是同一任期内出现两个 领导者,而少数派一侧的领导者得不到过半数的确认,所以什么都确定不了。 恢复的那一刻,它的未确定条目会悄无声息地消失。这个实验留下的问题只有一个—— 在这期间收到 ok 的客户端会怎么样?
步骤
- 把三个文件放到
/root/raft/,启动三个节点,并写入/root/raft/leader.json。 - 在
/root/raft/net.py中编写should_deliver(src, dst, cut)。 - 写入值
alpha并使其确定,写入/root/raft/before.json。 - 让领导者与其余两个节点互相断开,写入
/root/raft/isolated.json。 - 等待新领导者被选出,写入
/root/raft/two-leaders.json。 - 向旧领导者写入
ghost,写入/root/raft/minority.json。 - 向新领导者写入
real,写入/root/raft/majority.json。 - 解除断开,把结果写入
/root/raft/verdict.json。
参考
- 断开:
curl -s -XPOST -d '{"peers":[2,3]}' 127.0.0.1:5001/cut - 解除:
curl -s -XPOST -d '{"peers":[]}' 127.0.0.1:5001/cut - 查看日志:
curl -s 127.0.0.1:5001/log - 常见错误 1:只在一侧设置断开。如果反方向还是通的,就不是分区。
- 常见错误 2:在新领导者被选出之前就急着读取。选举需要相当于超时的那么长时间。
建立三个节点
把 /opt/lab/raft/ 中的 node.py、rules-for-partition.py、log-for-partition.py 分别放到 /root/raft/ 的 node.py、raftrules.py、raftlog.py,启动三个节点,并把选出的领导者写入 /root/raft/leader.json。
前两个实验中写过的规则,会以完成版的形式提供给你。这个实验中你要写的代码只有一个分区开关,其余的都是动手实验的时间。在 leader.json 中写 leader 和 term。
不断线,而是拒绝消息
在 /root/raft/net.py 中编写 should_deliver(src, dst, cut)。不收发 cut 中所列对方的消息。
这个 Pod 没有设置防火墙的权限。所以我们不去“断线”,而是让节点拒绝特定对方的请求——对对方来说,这与没有响应没有区别,所以站在共识算法的立场上,它与真正的分区是一样的。dst 是我自己,cut 是我已经断开的对方的列表。用 curl -s -XPOST -d '{"peers":[2,3]}' 127.0.0.1:5001/cut 设置,并通过 /status 的 cut 来确认。
在分区之前先确定一个值
向领导者写入值 alpha,确认三个节点的 commit 都变成 1 之后,把 value、commit、committed_on 写入 /root/raft/before.json。
要分清之后哪些被丢弃、哪些留下,就必须有一个在分区之前已经确定的值。committed_on 是提交了该值的节点数。写入的命令是 curl -s -XPOST -d '{"value":"alpha"}' 127.0.0.1:<리더포트>/client(占位符为领导者端口)。
把领导者隔离出去
让领导者与其余两个节点互相断开,并把刚断开之后旧领导者的状态,以 old_leader、state_after_cut、cut 写入 /root/raft/isolated.json。
两侧都要设置,才是真正的分区——只设置一侧,一个方向仍然是通的。看看被断开的领导者会不会自行退位。Raft 的领导者没有降级自己的机制。在看到更大的任期之前,它会一直相信自己是领导者。
同一时刻有两个领导者
等待多数派一侧选出新领导者,然后把两个领导者的编号和任期,以 old_leader、old_term、new_leader、new_term 写入 /root/raft/two-leaders.json。
这就是本课程的标题。把三个节点的 /status 并排放在一起看,会有两个节点同时回答自己是 leader。但是它们的任期不同。 Raft 阻止的不是“出现两个领导者的状态”,而是“同一任期内出现两个领导者的状态”,这个区分正是这个算法的安全性所立足的地方。
写入少数派一侧的值
向被断开的旧领导者写入值 ghost,并把 accepted、committed、log_len、commit 写入 /root/raft/minority.json。
被断开的领导者也会接收这个值。它会写进自己的日志,并向客户端返回 ok。但是由于没有来自过半数的确认,commit 停留在 1。收到的和已确定的之间的距离,在这里看得见了——如果不了解这个距离的客户端把 ok 当作成功,就会出事故。
写入多数派一侧的值
向新领导者写入值 real,并把 index、committed、commit、same_index_as_ghost 写入 /root/raft/majority.json。
关键在于这两个值拿到了同一个位置编号。ghost 和 real 在争夺日志的第 2 个位置,而一个位置上只能留下一个。留下哪一个已经确定——得到过半数确认的那一个。
恢复——什么会被丢弃
解除三个节点的全部断开,确认旧领导者的日志被整理之后,把 survived、discarded、old_leader_state_after、ghost_was_committed、acknowledged_to_client 写入 /root/raft/verdict.json。
解除断开用 curl -s -XPOST -d '{"peers":[]}' 127.0.0.1:<포트>/cut(占位符为端口)。旧领导者在看到更大任期的心跳的那一刻就会退位,之后它日志的第 2 个位置会被新领导者的内容覆盖。最后两项就是这个实验的教训——那个值从未被提交过,但客户端却收到了 ok。