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

当时有两个领导者

当时有两个领导者

在 TT Lab 中继续学习

一句话总结

网络一旦分裂,同一时刻就可能出现两个领导者。Raft 阻止的并不是这种状态,而是 同一任期内出现两个领导者;少数派一侧的领导者什么都确定不了,之后才会发现自己的记录 被丢弃了。

为什么需要它

分布式系统的事故,通常不是来自故障本身,而是来自不完整的信息。被隔离的领导者 不知道自己被隔离了。它所知道的只有“最近没有响应”,而这与 对方已经崩溃的情形没有区别。然而 Raft 的领导者没有自行退位的机制。 在看到更大的任期之前,它会一直相信自己是领导者。

所以它会继续像领导者一样行事。接收客户端的值,写进自己的日志,返回 成功。同一时刻,在另一侧,过半数节点聚在一起,选出任期更高的新领导者。这时 领导者确实是两个。无法阻止这件事,正是这个算法的出发点, 而它所阻止的,是两个领导者在同一个位置上确定不同的值。

工作原理

少数派一侧的领导者得不到过半数的确认。它可以在自己的日志里写入条目,但提交 索引不会上升。所以那个条目停留在“已记录但未确定”的状态。

분할 중                        임기  로그                  커밋
  옛 리더 (혼자)                 1   alpha, ghost           1
  새 리더 + 팔로워 (둘)          2   alpha, real            2

该代码块中的韩文标签依次说明:分区期间,旧领导者(独自一个)任期为 1,日志是 alpha、ghost,提交索引为 1;新领导者加上追随者(共两个)任期为 2,日志是 alpha、real,提交索引为 2。

关键在于,两个条目在争夺同一个位置编号 2。一个位置上只能 留下一个,而留下哪一个已经确定——得到过半数确认的那一个。

恢复是悄无声息地发生的。连接恢复后,旧领导者看到任期 2 的心跳,把自己的任期 提高到 2,并退回追随者。接着新领导者说“如果你的日志第 1 个位置的任期是 1,就把 real 接在它后面”,而旧领导者的第 2 个位置由于任期对不上,被截掉了。 ghost 消失了。没有任何警告。

到这里,选举限制为什么必要就清楚了。假如恢复之后旧领导者先超时, 成为候选人,并且只凭日志长度就能拿到票,那么它就可以用 ghost 覆盖已经提交的 real。先看最后一个条目任期的那条规则,堵住了这条路。

还值得一并看看,为什么接受这样的设计。阻止少数派一侧接收写入, 是放弃可用性的选择。如果让分区的两侧都接收写入,服务虽然能继续运转, 但恢复时,两条分叉的记录就得靠人来合并。使用 Raft 的系统 选了相反的路——少数派停下来,换来的是恢复自动完成。在元数据这类 “与其错值,不如没有值”的场合,这样做是对的。

所以本课程标题的含义,也不是“发现了 bug”。出现两个领导者的那一刻 是正常行为,而即使在那一刻,已确定的值也只有一个,这就是这个算法的承诺。 事故发生的地方不在算法之内,而在它之外——客户端把少数派一侧领导者 返回的成功响应,当成了已确定。

在现场相遇的样子

在运维中,这起事件会以“说是写入成功了,值却不见了”的形式出现。客户端从少数派一侧的 领导者那里收到了成功,几秒钟后查询,那个值不存在。日志里没有错误—— 因为没有任何东西失败。

实际的实现都在努力缩小这个窗口。具有代表性的做法是:如果领导者在一段时间内 收不到过半数对心跳的响应,就自行退位(基于租约)。etcd 文档写道,因临时的分区 失去法定人数时,网络恢复后会自动安全地恢复,但永久地失去法定 人数则是致命的(etcd FAQ)。同一份文档里 有一张表:3 个节点的过半数是 2,可容忍的故障是 1 台;5 个节点则分别是 3 和 2。

不要使用偶数台的建议也来自这里。4 个节点的过半数是 3,可容忍的故障 是 1 台,与 3 个节点相同,而可能出故障的机器却多了一台。etcd 文档建议把集群控制在 七台以内,并同时写明 Google Chubby 建议使用五台。

还有一点要记住:“分区”不只意味着线缆断开。一条防火墙规则, 一次安全组变更,只堵住一个方向的非对称路径,长时间停顿的垃圾回收—— 表现出来都是同样的样子。节点是活的,进程也正常,只是消息到不了。 只堵一个方向的情形尤其棘手——心跳发出去了,响应却回不来, 领导者相信自己在说话,而追随者什么都没听到,于是开启了选举。 所以这个实验也不去断线。拒绝消息就足够了。

下一项实验要做什么

把三个节点中的领导者从其余节点上隔离开,再恢复。由于没有权限在 Pod 上设置防火墙, 我们不断线,而是自己做一个拒绝彼此消息的开关。记录两个领导者同时存在的 瞬间,分别向两侧各写入一个值,然后亲眼确认恢复后哪一侧会消失。