上一篇文章我们将哨兵主观下线的核心流程都分析完成,这一篇我们将接着之前的思路,将哨兵获取客观下线结果并结合raft协议完成哨兵leader选举完成故障转移的流程分析完成,希望对你有帮助。
详解哨兵选举与故障转移流程
1. 获取客观下线结果判断
当前哨兵主观认定master下线之后,为了明确知晓master节点是否真的下线,哨兵节点还会通过cc即异步命令指针所维护的socket连接发起is-master-down-by-addr的sentinel指令进行询问,其他哨兵所回复的结果都会通过回调函数sentinelReceiveIsMasterDownReply函数处理。
这段请求最终会被其他哨兵sentinel命令所对应的函数sentinelCommand执行,他们各自会在内部查看自己对于master判断是否是主观下线,如果是则返回1。
最后我们的哨兵收到这个结果1,则通过位运算加master节点状态flags类加上客观下线的判断标识64,这里redis为了提升运算效率,采用的二进制|=运算,这一点我们在阅读大量的redis中源码都会看到二进制运算这一点优化:
对此我们也给出哨兵处理每一个master实例的函数入口,可以看到在调用sentinelCheckSubjectivelyDown完成主观下线的检查之后,又会调用sentinelAskMasterStateToOtherSentinels并传入SENTINEL_NO_FLAGS即仅仅检查其他哨兵对于当前master的主观判断结果:
步入sentinelAskMasterStateToOtherSentinels即可看到哨兵询问其他哨兵对于master判断的逻辑,可以看到它遍历出每一个哨兵实例,通过异步连接cc指针所指向的连接发起SENTINEL is-master-down-by-addr指令获取其他哨兵节点对于master下线的看法,并注册sentinelReceiveIsMasterDownReply函数处理返回结果:
其他哨兵收到sentinel指令后就会调用sentinelCommand处理这条指令,其内部会判断自己所维护的master的flags二进制位是否包含SRI_S_DOWN,如果是则说明被请求的哨兵节点同样认为master已下线,则直接回复master的leaderid以及shared.cone即1(代表确认当前master确实下线):
最终我们的sentinel的回调函数sentinelReceiveIsMasterDownReply处理对端的结果,发现返回值为1,说明该节点对于我们的来说客观认为master下线了。
所以我们的哨兵就需要记录这个消息,因为我们维护master->sentinels的字典记录其他哨兵信息,所以定位到其他哨兵客观下线的回复后,我们就会从这个字典中找到这个哨兵的结构体将其flags累加一个SRI_MASTER_DOWN的常数值64,意味这个哨兵客观认定这个master下线了:
2. 启动故障转移
上一步收集其他哨兵的判断并更新到各自的flags位后,当前哨兵的定时任务再次遍历master调用sentinelHandleRedisInstance处理当前master,其内部会遍历当前哨兵维护的哨兵数组获取这些哨兵对于master下线的看法,如果累加到的哨兵对于下线的看法大于或者等于我们配置quorum之后,则会判定会客观下线:
我们还是从sentinelHandleRedisInstance方法查看方法入口,可以看到哨兵定时执行该方法时会调用sentinelCheckObjectivelyDown检查客观下线状态:
步入其内部即可看到笔者所说的,遍历哨兵查看下线结果并更新master下线状态的逻辑:
3. 发起新纪元leader选举
基于上述结果redis会判断是否发起故障转移,若需要则通知其他哨兵进行leader选举,收到通知的哨兵会检查当前纪元是否小于发起选举的哨兵纪元,若符合要求且在此期间没有别的哨兵发起选举,则向其投票。
后续我们的哨兵收到并收集这些响应之后,更新自己所维护的哨兵数组中的leader_epoch,通过遍历这个哨兵数组中的leader_epoch是否和自己所生成的leader_epoch一致,如果统计结果超过半数,则说明自己当选leader,由此开始进行故障转移:
(1) 选举源码入口
我们还是以sentinelHandleRedisInstance作为程序入口,可以看到其内部调用sentinelStartFailoverIfNeeded判断是否需要进行故障转移,然后调用sentinelAskMasterStateToOtherSentinels并传入SENTINEL_ASK_FORCED发起leader选举请求:
(2) 确认故障转移
我们步入sentinelStartFailoverIfNeeded即可看到其对于是否进行故障转移的判断,逻辑比较简单:
- 明确是否客观认定下线。
- 明确是否处于故障转移。
- 近期是否有进行故障转移。
如果伤处条件都排除则:
- failover_state 即故障转移状态设置为等待故障转移,后续的函数状态机会根据这个标识进行故障转移处理。
- flags标识累加处于故障转移中。
- 更新master纪元为哨兵纪元+1,用于后续哨兵leader选举后更新纪元使用。
对此我们给出sentinelStartFailoverIfNeeded的判断,可以看到它会按照上文所说的流程进行判断,明确排除三种情况后调用sentinelStartFailover设置故障转移状态:
步入sentinelStartFailover即可看到我们上文所说故障转移状态更新:
结果上述步骤明确知晓redis需要进行故障转移之后,哨兵会再次调用sentinelAskMasterStateToOtherSentinels方法传入当前哨兵的server.runid向其他哨兵发起投票请求,并通过sentinelReceiveIsMasterDownReply处理响应结果:
(4) 对端哨兵处理发起选举的投票结果
上述步骤发起投票的哨兵节点发起投票后,收到投票请求的哨兵实例就会进行如下检查:
- master纪元小于发起投票请求的哨兵纪元req_epoch。
- 当前哨兵纪元小于req_epoch。
如果符合要求则说明发起投票请求的哨兵可以作为leader,当前实例将leader 设置为该节点,然后回复结果给发送结果的实例:
(5) 处理投票结果
收到响应后sentinelReceiveIsMasterDownReply回调函数就会解析出其他哨兵的leader_epoch 信息,作为后续选举leader的依据,如果半数以上的leader_epoch 为当前哨兵所设置的run_id,则说明当前哨兵作为leader进行故障转移:
最后基于状态机模式,根据当前master状态为SENTINEL_FAILOVER_STATE_WAIT_START于是调用sentinelFailoverWaitStart选举leader
步入sentinelFailoverWaitStart即可看到该方法调用sentinelGetLeader,如果发现是自己则发送广播告知自己为leader进行故障转移:
对此我们也给出选举哨兵leader的核心方法sentinelGetLeader,核心步骤为:
- 如果投票结果给出的leader值不为空(这个leader记录的是其他哨兵投票的实例的run_id)且纪元和当前选举纪元一致,则给对应的leader票数+1。
- 将这个投票结果存入counter这个字典中。
- 遍历counter如果这个值大于配置的quorum或哨兵的半数以上,则将其设置为winner,即最后的leader,由此让这个leader哨兵进行故障转移:
对应的我们也给出这段代码的实现:
小结
自此我们来小结一下哨兵选举与故障转移的大体过程:
- 当前哨兵主观认定下线之后,通过异步连接询问其它哨兵是否客观认定master下线。
- 超过半数的哨兵认为下线则当前哨兵就认为master下线于是开启发起投票选举。
- 更新自己的纪元并携带runid到其它哨兵节点上拉票。
- 基于回调函数获取其它哨兵选票结果进行遍历汇总,用以一个字典以哨兵runid为key,投票值为value进行维护。
- 汇总后通知全局哨兵leader。
- leader进行故障转移。