Raft Prevote--如何避免惊群效应

591 阅读3分钟

背景

image.png 在Raft算法中,每次Candidate节点想要开启新一轮的选举时,都必须先将自身的Term增加1,然后在集群中广播RequestVote请求。但我们考虑这样一种情况:在集群中,一个Follower节点A因为物理机掉线等原因与集群产生了网络隔离。此时A无法收到来自Leader的心跳,因此将自身Term+1,并开启新一轮的选举。一段时间后,A与外界的网络连接恢复,重新加入到集群中。此时可能有以下几种可能:

  1. 原集群commit了更多的日志,或Term增加。A重新加入集群后,由于没有足够新的日志或Term较小,它的选举请求将会被绝大多数节点拒绝。A将会在Leader节点控制下,重新成为Follower。
  2. 原集群没有commit更多的日志且Term没有增加。A重新加入集群后,将可能引发集群中新一轮的选举,并产生一个新的Leader。 在第二种情况中,新的Leader选举是完全没有必要的。考虑到IDC内部网络故障通常发生在极少的机器上(网络隔离),而且很快会恢复(重新加入集群且期间没有日志产生),对第二种情况的优化是有必要的。

Follower_2在electionTimeout没收到心跳之后,会发起选举,并转为Candidate。每次发起选举时,会把Term加一。由于网络隔离,它既不会被选成Leader,也不会收到Leader的消息,而是会一直不断地发起选举。Term会不断增大。

一段时间之后,这个节点的Term会非常大。在网络恢复之后,这个节点会把它的Term传播到集群的其他节点,导致其他节点更新自己的term,变为Follower。然后触发重新选主,但这个旧的Follower_2节点由于其日志不是最新,并不会成为Leader。整个集群被这个网络隔离过的旧节点扰乱,显然需要避免的。

PreVote工作原理

造成上述问题的根本原因在于每次Candidate发起选举的时候,都需要先将自身的Term+1,而更高的Term在Raft协议中代表更“新“的日志(在原集群没有更多的日志时候)。那么要解决这个问题也方法非常简单——节点在开始选举前,先试探性地询问集群中是否有足够的“友军”支持其选举。如果有,节点才正式将自己的Term+1,然后发起选举。

可以用如下更形式化的语言来描述PreVote的工作机制:

  1. 确认节点A将要发起选举
  2. 向集群内所有的节点(除了自己)发送一个PreVote消息。该消息携带的内容与RequestVote一致(Term都是A的Term+1),名字仅用于区别两种消息。同时A更新自身的投票状态(即PreVote发起时,不投票给自己)。
  3. 其他节点收到消息后,判断是否支持该选举请求,并返回结果。判断方式与RequestVote相同,但不更新自身的投票状态。
  4. A统计所有的票数(包括自己的一票),如果确认可以当选,那么A正常发起普通的选举流程。

整个过程有几个需要额外描述的关键点。首先,发起PreVote并不会改变发起者的任何状态(包括Term和投票状态) 。这样做的目的是一旦集群中有人发起了正式的选举流程,PreVote不会阻塞正式选举。其次,其他节点在响应PreVote消息时,不会更新自身的投票状态。原因同上。