在sentinelHandleRedisInstance函数中,如果是主节点,需要做如下处理:
1 | void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { |
节点的状态定义1
2
3
4
5
6
7
8
客观下线
sentinelCheckObjectivelyDown
sentinelCheckObjectivelyDown函数用于判断master节点是否客观下线:
- 首先确认主节点是否已经被哨兵标记为主观下线,如果已经主观下线,quorum的值置为1,表示当前已经有1个哨兵认为master主观下线,进行第2步
- master->sentinels存储了监控当前master的其他哨兵节点,遍历其他哨兵节点,通过flag标识中是否有SRI_MASTER_DOWN状态判断其他哨兵对MASTER节点下线的判断,如果有则认为MASTER下线,对quorum数量加1
- 遍历结束后判断quorum的数量是否大于master->quorum设置的数量,也就是是否有过半的哨兵认为主节点下线,如果是将odown置为1,认为主节点客观下线
- 根据odown的值判断主节点是否客观下线
- 如果客观下线,确认master->flags是否有SRI_O_DOWN状态,如果没有,发布+odown客观下线事件并将master->flags置为SRI_O_DOWN状态
- 如果没有客观下线,校验master->flags是否有SRI_O_DOWN状态,如果有,需要发布-odown事件,取消master的客观下线标记(master->flags的SRI_O_DOWN状态取消)
可以看到,master节点客观下线需要根据其他哨兵实例对主节点的判断来共同决定,具体是通过其他哨兵实例的flag中是否有SRI_MASTER_DOWN状态来判断的,如果认为master下线的哨兵个数超过了master节点中的quorum设置,master节点将被认定为客观下线,发布+odown客观下线事件,关于SRI_MASTER_DOWN状态是在哪里设置的在后面会讲到。
1 | void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) { |
是否需要执行故障切换
sentinelStartFailoverIfNeeded
sentinelStartFailoverIfNeeded用于判断是否需要执行故障切换,可以开始故障切换的条件有三个:
- master节点被认为客观下线(SRI_O_DOWN)
- 当前没有在进行故障切换(状态不是SRI_FAILOVER_IN_PROGRESS)
- 距离上次执行故障切换的时间,超过了故障切换超时时间设置的2倍,意味着上一次执行故障切换的时间已超时,可以重新进行故障切换
同时满足以上三个条件,达到执行故障切换的标准,调用sentinelStartFailover函数,将故障切换的状态改为待执行。
1 | int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) { |
sentinelStartFailover
可以看到sentinelStartFailover函数并没有直接进行故障切换,而是更改了一些状态:
- 将failover_state置为了SENTINEL_FAILOVER_STATE_WAIT_START等待开始执行状态
- 将master节点的flags设置为SRI_FAILOVER_IN_PROGRESS故障切换执行中状态
- 将master的failover_epoch设置为当前哨兵的投票轮次current_epoch + 1 ,在选举leader时会用到
1 | void sentinelStartFailover(sentinelRedisInstance *master) { |
获取哨兵实例对主节点状态判断
在sentinelHandleRedisInstance函数中,可以看到sentinelStartFailoverIfNeeded条件成立时以及函数的最后都调用了sentinelAskMasterStateToOtherSentinels,接下来就去看看sentinelAskMasterStateToOtherSentinels里面都做了什么:
1 | if (ri->flags & SRI_MASTER) { |
is-master-down-by-addr命令发送
sentinelAskMasterStateToOtherSentinels
sentinelAskMasterStateToOtherSentinels函数用于向其他哨兵实例发送is-master-down-by-addr命令获取其他哨兵实例对主节点状态的判断,它会遍历监听同一主节点的其他哨兵实例进行处理:
- 获取每一个哨兵实例
- 计算距离每个哨兵实例上一次收到IS-MASTER-DOWN-BY-ADDR命令回复时间的间隔
- 如果距离上次收到回复的时间已经超过了SENTINEL_ASK_PERIOD周期的5倍,清空哨兵节点flag中的SRI_MASTER_DOWN状态和leader
- 如果master节点已经是下线状态SRI_S_DOWN,不需要进行处理,回到第一步处理下一个哨兵
- 如果哨兵节点用于发送命令的link连接处于未连接状态,不处理,回到第一步处理下一个哨兵
- 如果不是强制发送命令(入参的flag是SENTINEL_ASK_FORCED时),并且距离上次收到回复命令的时间还在SENTINEL_ASK_PERIOD周期内,不处理,回到第一步处理下一个哨兵
- 通过redisAsyncCommand函数发送发送is-master-down-by-addr命令,sentinelReceiveIsMasterDownReply为处理函数,redisAsyncCommand函数有如下参数:
- 用于发送请求的连接:ri->link->cc
- 收到命令返回结果时对应的处理函数:sentinelReceiveIsMasterDownReply
- master节点的ip:announceSentinelAddr(master->addr)
- master节点端口:port
- 当前哨兵的投票轮次:sentinel.current_epoch
- 实例ID:master->failover_state > SENTINEL_FAILOVER_STATE_NONE时表示要执行故障切换,此时传入当前哨兵的myid,否则传入*
1 | void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) { |
is-master-down-by-addr命令处理
sentinelCommand
其他哨兵实例收到is-master-down-by-addr命令之后的处理逻辑在sentinelCommand函数中可以找到:
- 根据请求传入的ip和端口信息获取主节点的sentinelRedisInstance实例对象(在发送is-master-down-by-addr命令的redisAsyncCommand函数中传入了主节点的ip和端口)
- 如果不是TILT模式,校验sentinelRedisInstance对象是否是主节点并且主节点被标记为主观下线,如果条件都成立表示主节点已经主观下线,将isdown置为1
- 判断请求参数中的runid是否不为*,如果不为*表示当前需要进行leader选举,调用sentinelVoteLeader选举哨兵Leader
- 发送is-master-down-by-addr命令的回复,将对主节点主观下线的判断、选出的leader节点的runid、投票轮次leader_epoch返回给发送命令哨兵
1 | void sentinelCommand(client *c) { |
is-master-down-by-addr回复处理
sentinelReceiveIsMasterDownReply
在sentinelCommand中对命令处理之后发送了返回数据,数据里面包含主观下线的判断、leader的runid以及投票轮次leader_epoch,对返回数据的处理在sentinelReceiveIsMasterDownReply函数中:
- 如果回复者也标记了节点主观下线,将哨兵实例的flags状态置为SRI_MASTER_DOWN下线状态,SRI_MASTER_DOWN状态就是在这里设置的
- 如果返回的leader runid不是*,意味着哨兵实例对leader进行了投票,需要更新哨兵实例中的leader和leader_epoch
1 | // 这里的privdata指向回复is-master-down-by-addr命令的那个哨兵实例 |
故障切换状态机
在sentinelHandleRedisInstance函数中,判断是否需要执行故障切换之后,就会调用sentinelFailoverStateMachine函数进入故障切换状态机,根据failover_state
故障切换状态调用不同的方法,我们先关注以下两种状态:
SENTINEL_FAILOVER_STATE_WAIT_START:等待执行状态,表示需要执行故障切换但还未开始,在sentinelStartFailoverIfNeeded函数中可以看到如果需要执行故障切换,会调用sentinelStartFailover函数将状态置为SENTINEL_FAILOVER_STATE_WAIT_START,对应的处理函数为sentinelFailoverWaitStart,sentinelFailoverWaitStart中会判断是当前哨兵节点是否是执行故障切换的leader,如果是将状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE。
SENTINEL_FAILOVER_STATE_SELECT_SLAVE:从SLAVE节点中选举Master节点的状态,处于这个状态意味着需要从Master的从节点中选举出可以替代Master节点的从节点,进行故障切换,对应的处理函数为sentinelFailoverSelectSlave。
1 | void sentinelFailoverStateMachine(sentinelRedisInstance *ri) { |
sentinelFailoverWaitStart
sentinelFailoverWaitStart函数的处理逻辑如下:
- 调用sentinelGetLeader获取执行故障切换的leader
- 对比当前哨兵是与获取到执行故障切换leader的myid是否一致,判断当前哨兵是否是执行故障切换的leader
- 如果当前哨兵不是故障切换leader, 并且不是强制执行状态SRI_FORCE_FAILOVER,当前哨兵不能执行故障切换
- 如果当前哨兵是故障切换的leader节点,将故障切换状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,在下一次执行故障切换状态机时会从slave节点选出master节点进行故障切换
1 | void sentinelFailoverWaitStart(sentinelRedisInstance *ri) { |
Leader选举
sentinelGetLeader
sentinelGetLeader函数用于从指定的投票轮次epoch中获取Leader节点,成为一个Leader节点需要获取大多数的投票,处理逻辑如下:
- 创建了一个counters字典,里面记录了每个哨兵得到的投票数,其中key为哨兵实例的id
- counters的数据来源:遍历master->sentinels获取其他哨兵实例,判断哨兵实例记录的leader是否为空并且投票轮次与当前指定的epoch一致,如果一致加入counters中并将投票数增加一票
- 从counters中获取投票数最多的哨兵实例记为winner,最大投票数记为max_votes
- 判断winner是否为空
- 如果不为空,在master节点中记录的leader节点和winner节点中,选出纪元(投票轮次)最新的节点记为myvote
- 如果为空,在master节点中记录的leader节点和当前哨兵实例中,选出纪元(投票轮次)最新的节点记为myvote
- 经过上一步之后,如果myvote不为空并且leader_epoch与调用sentinelGetLeader函数时指定的epoch一致,当前哨兵给myvote增加一票,然后判断myvote得到的投票数是否大于max_votes,如果是将winner获胜者更新为myvote
- 到这里,winner中记录了本轮投票的获胜者,也就是得到票数最多的那个,max_votes记录了获得投票数,能否成为leader还需满足以下两个条件,,保证选举出的leader得到了过半哨兵的投票:
- 条件一:得到的投票数max_votes大于voters_quorum,voters_quorum为哨兵实例个数的一半+1,也就是需要有过半的哨兵实例
- 条件二:得到的投票数max_votes大于master->quorum,这个值是在 sentinel.conf 配置文件中设置的,一般设置为哨兵总数的一半+1
- 选举结束,返回winner中记录的实例id作为leader
1 | char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) { |
投票
sentinelVoteLeader
sentinelVoteLeader函数入参中可以看到传入了master节点,和候选节点的IDreq_runid
以及候选节点的投票纪元(轮次)req_epoch
。master节点中记录了当前哨兵节点选举的执行故障切换的leader节点,在master->leader中保存。sentinelVoteLeader就用于在这两个节点中选出获胜的那个节点:
- 如果候选节点的req_epoch大于当前sentinel实例的epoch,将当前哨兵实例的current_epoch置为请求轮次req_epoch
- 如果master节点记录的leader_epoch小于候选节点的req_epoch,并且当前实例的轮次小于等于候选节点轮次,将master节点中的leader改为候选节点
- 返回master节点中记录的leader
1 | char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) { |
总结
参考
Redis版本:redis-6.2.5