redis哨兵主从切换过程解析

redis哨兵主从切换过程解析

redis主挂掉,从节点能升级为主的前置条件

  • redis 主节点
    • 状态为 SRI_O_DOWN,主节点master被标记为客观下线
  • redis从节点
    • 从节点没有处于主观下线、客观下线或者断链状态;
    • 距离上一次收到该从节点对于"PING"命令的正常回复的时间,不超过5倍的SENTINEL_PING_PERIOD;
    • 该从节点的优先级不是0;
    • 距离上一次收到该从节点对于"INFO"命令的回复的时间,不超过3倍或5倍(根据主节点是否客观下线而定)的SENTINEL_PING_PERIOD;
    • 从节点与主节点的断链时间(该时间值根据从节点的"INFO"命令回复中得到)不超过max_master_down_time;
    • 所有从节点都轮训完毕之后,使用qsort快速排序算法,对数组instance进行排序。这里使用的比较函数compareSlavesForPromotion;排好序的instance数组,状态越好的从节点,其位置越靠前,因此,返回instance[0]作为选中的从节点;
    • 当选择好一个从节点之后,接下来在状态为SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE时,要做的就是向该从节点发送”SLAVEOF NO ONE”命令。
  • 所有主从哨兵模式,如果从节点无法升级为主节点,在哨兵节点正常的情况下,都可以通过维修主节点正常启动完成主从集群的修复。

redis哨兵切换主从过程解析

1.redis主节点主观下线、客观下线

哨兵每隔一段时间,会向其所监控的所有实例发送一些命令,用于获取这些实例的状态。这些命令包括:”PING”、”INFO”和”PUBLISH”。

​ “PING”命令,主要用于哨兵探测实例是否活着。如果对方超过一段时间,还没有回复”PING”命令,则认为其是主观下线了。

​ “INFO”命令,主要用于哨兵获取实例当前的状态和信息,比如该实例当前是主节点还是从节点;该实例反馈的IP地址和PORT信息,是否与我记录的一样;该实例如果是主节点的话,那它都有哪些从节点;该实例如果是从节点的话,它与主节点是否连通,它的优先级是多少,它的复制偏移量是多少等等,这些信息在故障转移流程中,是判断实例状态的重要信息;

​ “PUBLISH”命令,主要用于哨兵向实例的HELLO频道发布有关自己以及主节点的信息,也就是所谓的HELLO消息。因为所有哨兵都会订阅主节点和从节点的HELLO频道,因此,每个哨兵都会收到其他哨兵发布的信息。

​ 因此,通过这些命令,尽管在配置文件中只配置了主节点的信息,但是哨兵可以通过主节点的”INFO”回复,得到所有从节点的信息;又可以通过订阅实例的HELLO频道,接收其他哨兵通过”PUBLISH”命令发布的信息,从而得到监控同一主节点的所有其他哨兵的信息。

redis哨兵源码分析

sentinel.c

  • sentinelHandleDictOfRedisInstances
    • sentinelHandleRedisInstance
      • sentinelReconnectInstance
        • 重连 重连失败 关闭连接
        • 重连成功 发送ping命令
      • sentinelSendPeriodicCommands
        • Send periodic PING, INFO, and PUBLISH to the Hello channel to the specified master or slave instance
        • 所有节点
          • 实例超时则关闭连接,(ri->link->cc_conn_time / ri->link->act_ping_time / ri->link->last_pong_time / ri->link->pc_last_activity / ) 关闭连接
          • if ((ri->flags & SRI_S_DOWN) == 0) -----> sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
        • 主节点
          • 主节点 SRI_S_DOWN 后,查询主节点的所有sentinel节点,sentinel节点投票主节点下线数大于 主节点定义的投票数,则 SRI_O_DOWN
          • 主节点SRI_O_DOWN后才会触发迁移
          • 判断实例是否客观下线
            • 当前哨兵一旦监测到某个主节点实例主观下线之后,就会向其他哨兵发送”is-master-down-by-addr”命令,询问其他哨兵是否也认为该主节点主观下线了。如果有超过quorum个哨兵(包括当前哨兵)反馈,都认为该主节点主观下线了,则当前哨兵就将该主节点实例标记为客观下线。客观下线的概念只针对主节点实例,而与从节点和哨兵实例无关。
            • 发送”is-master-down-by-addr”命令。”is-master-down-by-addr”命令有两个作用:一是询问其他哨兵是否认为某个主节点已经主观下线;二是开始故障迁移时,当前哨兵向其他哨兵实例进行"拉票",让其选自己为领导节点。

2.redis sentinel哨兵节点选举领导节点

1:故障转移流程

​ 当哨兵监测到某个主节点客观下线之后,就会开始故障转移流程。具体步骤就是:

​ a:在所有哨兵中发起一次“选举”,让其他哨兵选择“我”(当前哨兵)为领导节点;

​ b:如果“我”能赢得大部分的选票,也就是在共有n个哨兵节点的情况下,如果有超过n/2个哨兵都将选票投给了“我”,则“我”就赢得了本界选举,成为领导节点,从而可以继续下面的流程。如果我没有赢得本界选举,则不能进行下面的流程了,而是随机等待一段时间后,开始下一轮选举;

​ c:“我”赢得选举后,就会从客观下线主节点的所有下属从节点中,按照一定规则选择一个从节点,使其升级为新的主节点;

​ d:当选中的从节点升级为主节点之后,“我”就会向剩下的从节点发送”SLAVEOF”命令,使它们与新的主节点进行同步;

​ e:最后,更新新主节点的信息,并通过”PUBLISH”命令,将新主节点的信息传播给其他哨兵。

2:选举领导节点原理

​ 故障转移流程中,最难理解的部分就是选举领导节点的过程。因为多个哨兵实际上是组成了一个分布式系统,它们之间需要相互协作,通过交换信息,最终选出一个领导节点。

​ sentinel选举的过程,借鉴了分布式系统中的Raft协议。Raft协议是用来解决分布式系统一致性问题的协议,在很长一段时间,Paxos被认为是解决分布式系统一致性的代名词。但是Paxos难于理解,更难以实现。而Raft协议设计的初衷就是容易实现,保证对于普遍的人群都可以十分舒适容易的去理解。

​ 有关Raft算法,可以参考官网https://raft.github.io/中的介绍。如果想要以最快的速度了解Raft算法的基本原理,可以参考这个PPT,非常形象且容易理解:http://thesecretlivesofdata.com/raft/

​ 要理解哨兵的选举过程,关键就在于理解选举纪元(epoch)的概念。所谓的选举纪元,直白的解释就是“第几届选举”。

​ 选举纪元实际上就是一个计数器。当哨兵进程启动时,其选举纪元就被初始化,默认的初始化值为0,不过该值也可以在配置文件中进行配置。

​ 哨兵运行起来之后,哨兵之间通过HELLO消息来交换信息。HELLO消息中,除了有主节点信息之外,还包含哨兵本地的选举纪元值(sentinel.current_epoch)。当哨兵收到其他哨兵发布的HELLO消息后,解析其中的选举纪元值,如果该值大于“我”本地的选举纪元值,则会用它的选举纪元更新“我”的选举纪元。

​ 因此,同一个监控单位内的所有哨兵,他们的选举纪元最终就会达成一个统一的值,这也就是Raft中,最终一致性的意思。

​ 当哨兵A发现某个主节点客观下线后,它就会发起新一届的选举。第一件事就是将本地的选举纪元加1,这个加1的意思,实际上就是表示“发起新一届选举”。之后,哨兵A就会向其他哨兵发送”is-master-down-by-addr”命令,用于拉票,其中就包含了A的选举纪元。

​ 投票采用先到先得的策略,因此当哨兵B收到A发来的”is-master-down-by-addr”命令之后,得到A的选举纪元,如果其值大于本地的选举纪元,说明本界选举中还没有投过票,则会更新本地的选举纪元,同时把票投给A。

​ 现实当然不会这么简单,分布式系统因为涉及多个机器,就会有各种可能的情况发生。比如哨兵C几乎同时也发起了新一届的选举,它也会把本地的选举纪元加1,并发送”is-master-down-by-addr”命令。当B收到C发来的命令之后,得到C的选举纪元,发现其值并不大于本地的选举纪元(因为刚才已经根据A的选举纪元更新了),因此就不会再次投票了,而是将之前投票给A的结果反馈给C。

​ 通过上面的介绍可知,在同一届选举(同一个选举纪元的值)中,每个哨兵只会投一次票。因此,在一界选举中,只可能有一个哨兵能获得超过半数的投票,从而赢得选举。

​ 当然,也有可能产生选举失败的情况。也就是没有一个哨兵能获得超过半数的投票。比如有4个哨兵节点A、B、C、D。哨兵A和C几乎同时发起了新的选举,最终B和C将选票投给了A,而A和D将选票投给了C。因此,A和C都只得到了2票,没有超过半数,因此都不能成为新的领导节点。这种情况下,A和C都会随机等待一段时间之后,重新发起新的选举。这种随机性能减少下一轮选举的冲突,从而降低选举失败的可能。

3. redis 故障转移流程

是否能开始一次新的故障转移流程,需要满足下面三个条件:

​ a:主节点master被标记为客观下线了;

​ b:当前没有针对该master进行故障转移流程;

​ c:最重要的条件是,针对该master,当前时间与master->failover_start_time之间的时间差,已经超过了master->failover_timeout*2。也就是说,当前距离上次进行故障转移流程的开始时间,或者是距离上次投票给其他哨兵的时间,已经等待了足够长的时间;

故障转移流程中的状态转换(6个状态)
SENTINEL_FAILOVER_STATE_WAIT_START
  • 一旦哨兵开始一次故障转移流程时,该哨兵第一件事就是向其他所有哨兵发送”is-master-down-by-addr”命令进行拉票。然后就是调用sentinelFailoverWaitStart函数处理当前状态。
  • 当前哨兵,在调用sentinelStartFailover函数发起故障转移流程时,会将当前选举纪元sentinel.current_epoch记录到ri->failover_epoch中。因此,本函数首先根据ri->failover_epoch,调用函数sentinelGetLeader得到本界选举的结果leader。如果本界选举尚无人获得超过半数的选票,则leader为NULL;
  • 如果当前哨兵还没有赢得选举,并且主节点标志位中没有设置SRI_FORCE_FAILOVER标记,说明当前哨兵还没有获得足够的选票,暂时不能继续进行接下来的故障转移流程,需要直接返回。
  • 但是如果超过一定时间之后,当前哨兵还是没有赢得选举,则会终止当前的故障转移流程,因此如果当前距离开始故障转移的时间超过election_timeout,则调用函数sentinelAbortFailover,终止本次故障转移流程。
  • 如果当前哨兵最终赢得了选举,则更新故障转移的状态,置ri->failover_state属性为下一个状态:SENTINEL_FAILOVER_STATE_SELECT_SLAVE,并更新ri->failover_state_change为当前时间;
SENTINEL_FAILOVER_STATE_SELECT_SLAVE
  • 当故障转移状态转换为SENTINEL_FAILOVER_STATE_SELECT_SLAVE时,就需要在下线主节点的所有下属从节点中,按照一定的规则,选择一个从节点使其成为新的主节点。

  • 该函数首先调用函数sentinelSelectSlave选择一个符合条件的从节点;

  • 如果没有合适的从节点,则调用sentinelAbortFailover直接终止本次故障转移流程;

  • 如果找到了合适的从节点slave,则首先将标记SRI_PROMOTED增加到该从节点的标志位中;并使主节点实例的ri->promoted_slave指针指向该从节点实例,并将故障转移状态置为SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE;然后更新ri->failover_state_change_time为当前时间;

    选择合适的slave节点的规则如下
    • 从节点没有处于主观下线、客观下线或者断链状态;
    • 距离上一次收到该从节点对于"PING"命令的正常回复的时间,不超过5倍的SENTINEL_PING_PERIOD;
    • 该从节点的优先级不是0;
    • 距离上一次收到该从节点对于"INFO"命令的回复的时间,不超过3倍或5倍(根据主节点是否客观下线而定)的SENTINEL_PING_PERIOD;
    • 从节点与主节点的断链时间(该时间值根据从节点的"INFO"命令回复中得到)不超过max_master_down_time;
    • 所有从节点都轮训完毕之后,使用qsort快速排序算法,对数组instance进行排序。这里使用的比较函数compareSlavesForPromotion;排好序的instance数组,状态越好的从节点,其位置越靠前,因此,返回instance[0]作为选中的从节点;
    • 当选择好一个从节点之后,接下来在状态为SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE时,要做的就是向该从节点发送”SLAVEOF NO ONE”命令。
SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
  • 首先如果选中的从节点当前处于断链状态,则因无法向其发送命令,因此直接返回;如果该状态已经持续超过ri->failover_timeout的时间,则调用函数sentinelAbortFailover终止本次故障转移流程;
  • 调用sentinelSendSlaveOf函数,向从节点发送"SLAVEOF NO ONE"命令,然后置故障转移状态为SENTINEL_FAILOVER_STATE_WAIT_PROMOTION,并且更新ri->failover_state_change_time为当前时间;
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
  • 判断处于SENTINEL_FAILOVER_STATE_WAIT_PROMOTION状态的时间是否超过了阈值ri->failover_timeout。如果确实已经超过了,则调用函数sentinelAbortFailover终止本次故障转移流程;
SENTINEL_FAILOVER_STATE_RECONF_SLAVES
  • 当故障转移状态变为SENTINEL_FAILOVER_STATE_RECONF_SLAVES时,选中的从节点已经升级为主节点,接下来要做的就是向其他从节点发送”SLAVEOF”命令,使它们与新的主节点进行同步。
  • 因为从节点在与主节点进行同步时,有可能无法响应客户端的查询。因此为了避免过多从节点因为同步而无法响应的问题,一个时间段内,最多只能允许master->parallel_syncs个从节点正在进行同步操作;
SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
  • 故障转移流程的最后一个状态,就是要更新当前哨兵节点中的主节点实例,及其下属从节点实例的信息。

参考资料如下:
https://www.cnblogs.com/gqtcgq/p/7247048.html
https://www.cnblogs.com/gqtcgq/p/7247047.html
https://www.cnblogs.com/gqtcgq/p/7247046.html

redis 5.0.8 源码
源码阅读工具 Understand 5.1

原文地址:https://www.cnblogs.com/fb010001/p/13356070.html