前段时间写过两篇redis哨兵的文章,一篇是redis哨兵模式的搭建。另外一篇是redis哨兵主从切换的原理,。
当时写的原理篇,是手动模拟主节点故障,然后查看主从切换的日志推算哨兵主从切换的流程。但是感觉这样搞出来的流程太粗,忽略了很多细节,真正要搞明白原理,还是要看源码才行。找时间学习了一下C语言的语法,然后梳理哨兵模式主从切换的相关源码。研究了一段时间,算是把主流程基本搞懂了,今天把真正的原理篇补上。
直接进入正题。
redis故障转移是由定时任务实现的。说到定时任务,就离不开serverCron方法,redis很多的定时任务逻辑都是在serverCron方法中实现的。在serverCron方法中有一段逻辑,调用了sentinelTimer方法。主从切换逻辑的入口就是这个sentinelTimer方法。源码逻辑如下
//哨兵模式下,serverCron方法中会循环执行sentinelTimer方法,100ms一次。
if (server.sentinel_mode) sentinelTimer();
sentinelTimer方法中包含了从哨兵发现主节点下线,哨兵选主,以及最终完成主从切换的一系列流程。我把这些流程进行了分类,整理出了下面的目录。
文章目录
- 1、哨兵发现主节点客观下线
- 2、哨兵询问其他哨兵对于主节点下线的意见,防止误判
- 3、哨兵leader选举
- 3.1、Raft协议
- 3.2、哨兵leader选举过程
- 3.2.1、投票过程
- 3.2.2、计票
- 4、哨兵leader选择一台从节点作为新主节点
- 5、从节点切换为新主节点
- 6、客户端如何获知新的主节点地址
按照这个目录结构,我们聊一下主从切换的流程
1、哨兵发现主节点客观下线
首先是哨兵发现主节点下线。
进入sentinelTimer方法,先是判断redis哨兵是否进入了TILT模式。如果未进入,会执行一个sentinelHandleDictOfRedisInstances(sentinel.masters)方法,方法入参是sentinel.masters,这个入参是:当前哨兵监听的所有主节点
我们进入sentinelHandleDictOfRedisInstances方法里,可以看到一个循环,从sentinel.masters哈希表中挨个获取主节点,
然后执行sentinelHandleRedisInstance方法,该方法有4个主要的逻辑。
1)、sentinelReconnectInstance(ri)方法。作用:如果哨兵和主节点断连,重新建立与主节点的连接
2)、sentinelSendPeriodicCommands(ri)方法。作用:给主节点发送PING、INFO指令,PING主要是用来与主节点交互,确认其是否在线。INFO主要是用来获取主节点或者从节点的信息
3)、sentinelCheckSubjectivelyDown(ri)方法。作用:判断主节点是否客观下线
4)、如果节点是主节点,还会继续判断是否执行哨兵选主以及故障切换等逻辑。
又分为3个小流程
a)、检查主节点是否客观下线
b)、询问其他哨兵对于主节点下线的意见
c)、启动故障切换
这3小步中,有一个状态机的概念,很重要,提前说一下。状态机,就是某个状态对应着不同的操作,redis的主从切换是靠定时任务实现,这里面不可避免的就要涉及到状态机。我们看一下redis的状态机源码实现:
void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {serverAssert(ri->flags & SRI_MASTER);if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return;switch(ri->failover_state) {case SENTINEL_FAILOVER_STATE_WAIT_START://开始执行故障转移sentinelFailoverWaitStart(ri);break;case SENTINEL_FAILOVER_STATE_SELECT_SLAVE://选择一个从节点作为新主节点sentinelFailoverSelectSlave(ri);break;case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE://切换从节点为主节点sentinelFailoverSendSlaveOfNoOne(ri);break;case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION://等待切换成功sentinelFailoverWaitPromotion(ri);break;case SENTINEL_FAILOVER_STATE_RECONF_SLAVES://通知所有的从节点,新主节点已经产生sentinelFailoverReconfNextSlave(ri);break;}
}
逻辑写的很清晰,这个状态机里包含了故障转移的主要流程。
说回sentinelHandleRedisInstance方法。
哨兵确定主节点是否主观下线的逻辑是由sentinelHandleRedisInstance方法的4个步骤中的第二步和第三步共同实现的,我们分别看下。
sentinelSendPeriodicCommands方法的主要逻辑如下:
//给主节点和从节点发送INFO命令if ((ri->flags & SRI_SENTINEL) == 0 &&(ri->info_refresh == 0 ||(now - ri->info_refresh) > info_period)){retval = redisAsyncCommand(ri->link->cc,sentinelInfoReplyCallback, ri, "%s",sentinelInstanceMapCommand(ri,"INFO"));if (retval == C_OK) ri->link->pending_commands++;}
//给所有类型的节点发送PING命令if ((now - ri->link->last_pong_time) > ping_period &&(now - ri->link->last_ping_time) > ping_period/2) {sentinelSendPing(ri);}
第二步做的事就是发送PING,然后记录发送PING的时间、以及响应PONG的时间。
接下来,我们看第三步
sentinelCheckSubjectivelyDown,在这个方法中,就利用上面记录的时间判断主节点是否下线。
先计算一下上次收到PONG距离当前时间的时间差。如果哨兵和主节点已经断连,则时间差就是上次主节点可用的时间距离当前时间的时间差。
//计算当前距离上一次发送PING命令的时长if (ri->link->act_ping_time)elapsed = mstime() - ri->link->act_ping_time;else if (ri->link->disconnected)//如果哨兵和主节点断开了,计算当前距离连接最后可用的时长elapsed = mstime() - ri->link->last_avail_time;
拿到时间差了,紧接着就开始判断时间差是否超过了设定值,如果超过了设定值,就认为主节点客观下线
if (elapsed > ri->down_after_period ||(ri->flags & SRI_MASTER &&ri->role_reported == SRI_SLAVE && //当前节点是主节点,但是向哨兵报告即将成为从节点mstime() - ri->role_reported_time >(ri->down_after_period+SENTINEL_INFO_PERIOD*2)))//经过down_after_period时长+2个INFO命令的间隔后{/* Is subjectively down *///主节点依旧没有切换为从节点if ((ri->flags & SRI_S_DOWN) == 0) {//判断主节点为主观下线sentinelEvent(LL_WARNING,"+sdown",ri,"%@");ri->s_down_since_time = mstime();ri->flags |= SRI_S_DOWN;}}
判断条件有两个。一个是时间差是否大于sentinel.conf配置文件中配置的
down-after-milliseconds的值,此时会认定主节点已下线 另外一个是:如果主节点报告自己即将切换为从节点,但是过了down-after-milliseconds+两个INFO命令的间隔后,依然没有切换成功,此时也会认为主节点下线。
哨兵认为主节点客观下线后,会给+sdown频道发送一个消息,向外告知自己的发现。同时将主节点的flags字段记为宏 SRI_S_DOWN,这样就代表,哨兵监听的这个主节点客观下线。
2、哨兵询问其他哨兵对于主节点下线的意见,防止误判
这一步的作用,是防止误判。比如:哨兵自己的机器网络不好,导致误判了主节点下线。所以,发现主节点下线的哨兵需要给其他哨兵发送消息,询问他们与主节点的连接情况。这部分逻辑在sentinelCheckObjectivelyDown(ri)中,在下面的逻辑中,在主节点的sentinels变量中获取到监听当前主节点的其他哨兵实例,之后获取他们的flags变量,如果变量值为SRI_MASTER_DOWN,则将quorum值+1,最终的结果如果大于配置文件中配置的quorum值,此时会将odown值置为1,此时代表主节点客观下线,代表主节点下线已经是一个客观的事实。这部分逻辑如下:
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {......if (master->flags & SRI_S_DOWN) {//获取其他的哨兵节点di = dictGetIterator(master->sentinels);while((de = dictNext(di)) != NULL) {sentinelRedisInstance *ri = dictGetVal(de);//获取哨兵节点中的flags标识。如果flags标识为SRI_MASTER_DOWN,此时quorum+1if (ri->flags & SRI_MASTER_DOWN) quorum++;}dictReleaseIterator(di);//如果quorum的值大于等于配置文件中配置的quorum的值时,odown的值设置为1,代表主节点客观下线if (quorum >= master->quorum) odown = 1;}//odown值为1时,给+odown频道发送消息,同时将主节点的flags变量标记为SRI_O_DOWN,主节点客观下线if (odown) {if ((master->flags & SRI_O_DOWN) == 0) {//客观下线,给+odown频道发送消息sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",quorum, master->quorum);//主节点的flags标记为客观下线master->flags |= SRI_O_DOWN;//记录主节点的下线时间master->o_down_since_time = mstime();}}......
}
通过判断SRI_MASTER_DOWN的值来判断主节点客观下线的情况。那其他哨兵节点的SRI_MASTER_DOWN变量是何时赋值的呢?
是在sentinelAskMasterStateToOtherSentinels方法中,该方法在sentinel.c文件中。首先获取到其他监听该主节点的哨兵节点集合,然后遍历给这些哨兵节点发送is-master-down-by-addr命令。
回调方法是:sentinelReceiveIsMasterDownReply
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {......di = dictGetIterator(master->sentinels);while((de = dictNext(di)) != NULL) {......retval = redisAsyncCommand(ri->link->cc,sentinelReceiveIsMasterDownReply, ri,"%s is-master-down-by-addr %s %s %llu %s",sentinelInstanceMapCommand(ri,"SENTINEL"),master->addr->ip, port,sentinel.current_epoch,(master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?sentinel.myid : "*");if (retval == C_OK) ri->link->pending_commands++;......}dictReleaseIterator(di);
}
处理is-master-down-by-addr命令的方法是sentinelCommand(client *c)。主要逻辑就是获取哨兵节点中的主节点flags标识,是否是SRI_S_DOWN,这个标识是哨兵通过发送PING命令是否超时来赋值的,在前面哨兵判断主节点主观下线的部分已经说过了。如果flags标识是SRI_S_DOWN,就代表该哨兵认为主节点已下线,将判断结果以PONG命令返回给询问的哨兵。响应体中包含三部分内容,第一部分是当前哨兵对主节点下线的判断,第二、第三部分是哨兵leader选举相关的信息,后面再说。源码如下:
void sentinelCommand(client *c) {......
//获取主节点的结构体sentinelRedisInstance
ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,c->argv[2]->ptr,port,NULL);//确定节点是主节点,并且确定主节点已经下线
if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&(ri->flags & SRI_MASTER))//设置isdown变量值为1isdown = 1;......//返回的响应中包含三部分内容
addReplyMultiBulkLen(c,3);//第一部分是哨兵对主节点下线的判断,0:未下线,1:已下线
addReply(c, isdown ? shared.cone : shared.czero);//第二部分是哨兵leader的ID或者*。这部分是哨兵选举时的逻辑
addReplyBulkCString(c, leader ? leader : "*");//第三部分是哨兵leader的纪元。这部分是哨兵选举时的逻辑
addReplyLongLong(c, (long long)leader_epoch);......
}
该响应结果的处理在sentinel.c文件的sentinelReceiveIsMasterDownReply方法中,拿到相应的结果后,首先判断返回的结果是不是ARRAY类型,然后判断ARRAY中的元素类型是不是is-master-down-by-addr命令返回的三个元素。如果是,获取第一个元素的值是不是1,如果是1,就给主节点的flags赋值SRI_MASTER_DOWN,这就和我们上面的内容呼应起来了,下面是源码部分的解析
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {......//返回的数据是一个ARRAY类型。并且第一个数据是Integer类型,第二个数据是String类型,第三个数据是Integer类型if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 &&r->element[0]->type == REDIS_REPLY_INTEGER &&r->element[1]->type == REDIS_REPLY_STRING &&r->element[2]->type == REDIS_REPLY_INTEGER){ri->last_master_down_reply_time = mstime();//被询问的哨兵返回的一共有三部分数据,如果第一部分的值是1,此时会将SRI_MASTER_DOWN赋值到flags变量中,ri->flags变量,是当前哨兵监听的主节点的状态判断标识if (r->element[0]->integer == 1) {ri->flags |= SRI_MASTER_DOWN;} else {ri->flags &= ~SRI_MASTER_DOWN;}......
}
我们小结一下。redis哨兵每100ms运行一次,通过PING来检测主节点是否下线,如果发现了主节点下线,会通过频道将消息广播出去。同时为了防止自己的误判,会通过给其他哨兵发送is-master-down-by-addr命令来询问其他哨兵对于主节点下线的意见,如果同意主节点下线的哨兵数大于配置文件中的quorum,此时会认为主节点客观下线。接下来的流程就是从哨兵集群中选举一个哨兵来执行主从切换
3、哨兵leader选举
选举leader前,首先判断是否满足举行选举的条件
比如:是否正在举行选举、上一次主从切换的时间距当前时间的时间差是否满足条件等等。
如果举行选举的条件没有问题,就会将主节点的failover_state变量设置为SENTINEL_FAILOVER_STATE_WAIT_START,该变量的初始值是:SENTINEL_FAILOVER_STATE_NONE,failover_state是一个状态机,主要就是为了控制主从切换的流程,该状态机有以下值,每一个值的意思,作者都有注释,看注释就行了。
#define SENTINEL_FAILOVER_STATE_NONE 0 /* No failover in progress. */
#define SENTINEL_FAILOVER_STATE_WAIT_START 1 /* Wait for failover_start_time*/
#define SENTINEL_FAILOVER_STATE_SELECT_SLAVE 2 /* Select slave to promote */
#define SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE 3 /* Slave -> Master */
#define SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 4 /* Wait slave to change role */
#define SENTINEL_FAILOVER_STATE_RECONF_SLAVES 5 /* SLAVEOF newmaster */
#define SENTINEL_FAILOVER_STATE_UPDATE_CONFIG 6 /* Monitor promoted slave. */
SENTINEL_FAILOVER_STATE_WAIT_START是流程的第二步,表明哨兵leader选举开始。
接下来执行sentinelAskMasterStateToOtherSentinels方法。这个方法,我们前面已经提到过,作用是:询问其他哨兵对于主节点下线的意见,防止因为自己的原因导致了误判。那现在已经到了leader选举阶段,选leader,选新主节点了,为什么还要再回到之前的步骤,去询问其他哨兵对于主节点下线的意见呢?其实,并不是要询问其他哨兵对于主节点下线的意见,而是因为该方法中除了询问的逻辑,还隐含着leader选举的逻辑。那该方法是如何区分到底执行的是“询问主节点是否下线”还是“执行leader选举”呢?
实际上,是根据is-master-down-by-addr命令的最后一个参数。该参数有两种情况,一种是 “ * ”,另外一种是配置文件中配置的哨兵的myid值
retval = redisAsyncCommand(ri->link->cc,sentinelReceiveIsMasterDownReply, ri,"%s is-master-down-by-addr %s %s %llu %s",sentinelInstanceMapCommand(ri,"SENTINEL"),master->addr->ip, port,sentinel.current_epoch,(master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?sentinel.myid : "*");
再看is-master-down-by-addr命令的处理方法sentinelCommand,在该方法中,有一段逻辑就会判断入参client的第6个参数是不是 *,如果不是 *,就会执行哨兵leader选举,源码逻辑如下:
//如果run_id的值不为*,则执行哨兵leader选举if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,c->argv[5]->ptr,&leader_epoch);}
哨兵leader选举的方法入口,就是这个sentinelVoteLeader方法。
哨兵leader选举使用了Raft一致性协议,在介绍哨兵选举之前,简单说一下这个Raft协议,对理解哨兵选举流程有帮助。
3.1、Raft协议
Raft协议的诞生是为了解决分布式一致性的问题。在Raft协议中,有三种角色,Leader、Follower、Candidate。
在一个稳定的Raft系统中,只有Leader和Follower。Leader不定时的给Follower发送心跳消息,来维护自己的Leader地位。
1)、如果某个Follower,一定时间内未收到Leader的消息,此时这个Follower会转变成Candidate节点,开始发起Raft选举
2)、Candidate节点首先给自己投一票,然后给其他Follower节点,发送投票请求,并等待其他节点的回复
3)、开启一个定时器,判断选举过程是否超时
4)、如果在等待其他Follower节点回复时,收到了Leader的消息,说明新的Leader已经产生,此时Candidate节点会转为Follower节点
5)、如果这个Candidate节点收到的投票确认个数超过了超过了半数节点,并且大于配置文件中的quorum数,此时该Candidate节点变成新的Leader节点,从而可以执行Leader节点需要运行的流程逻辑
在这个选举流程中,每个Follower节点只能投一次票,如果选举期间,Candidate节点收到的选票相同,此时会再重新发起选举,直到选出Leader
3.2、哨兵leader选举过程
3.2.1、投票过程
redis的哨兵Leader选举并不是完全符合Raft协议,主要原因是:在redis哨兵模式稳定运行时,哨兵之间是平等的,没有Leader和Follower之分,只有当主节点下线,准备选举时,此时才会出现Leader和Follower。
我们看一下sentinelVoteLeader方法的具体实现。可以看到,整个选举过程,依赖纪元实现。如果请求参数的纪元比哨兵记录的主节点的纪元大,比哨兵自身的纪元也大,此时当前哨兵就会给请求投票的哨兵一票。
char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {//请求哨兵的纪元大于当前哨兵的纪元if (req_epoch > sentinel.current_epoch) {sentinel.current_epoch = req_epoch;sentinelFlushConfig();sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",(unsigned long long) sentinel.current_epoch);}//master记录的哨兵leader的纪元小于请求哨兵的纪元。//当前哨兵的纪元小于等于请求哨兵的纪元//以上两个条件确保了当前哨兵只能投一次票if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch){//leader节点的runid设置为请求投票节点的runid,也就是Candidate节点的runidmaster->leader = sdsnew(req_runid);......//主节点记录的leader的纪元更新为请求哨兵的纪元,代表给请求投票的哨兵投了一票master->leader_epoch = sentinel.current_epoch;......//当前哨兵将自己的投票结果,发送事件消息sentinelEvent(LL_WARNING,"+vote-for-leader",master,"%s %llu",master->leader, (unsigned long long) master->leader_epoch);......}//重置leader的纪元*leader_epoch = master->leader_epoch;//返回哨兵leader的runidreturn master->leader ? sdsnew(master->leader) : NULL;
}
当前哨兵投票完后,将自己的投票结果返回给请求投票的哨兵。
请求投票哨兵发起is-master-down-by-addr命令时,设置的回调函数是sentinelReceiveIsMasterDownReply,所以我们看下当请求投票哨兵收到其他哨兵的投票结果后,是如何进行处理的,sentinelReceiveIsMasterDownReply的逻辑如下:
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {//返回的数据是一个ARRAY类型。并且第一个数据是Integer类型,第二个数据是String类型,第三个数据是Integer类型if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 &&r->element[0]->type == REDIS_REPLY_INTEGER &&r->element[1]->type == REDIS_REPLY_STRING &&r->element[2]->type == REDIS_REPLY_INTEGER){......//哨兵返回的第二部分不是*,那返回的就是哨兵leader的runidif (strcmp(r->element[1]->str,"*")) {......//master设置哨兵leader的runidri->leader = sdsnew(r->element[1]->str);//master设置哨兵leader的纪元ri->leader_epoch = r->element[2]->integer;......}}
}
执行完这部分逻辑,A哨兵就算拿到了B哨兵的选票,等到其他哨兵也回复了A之后,就开始进行下一步,计票。看看A能否当选leader
3.2.2、计票
计票的工作,其实就是计算一下所有的哨兵数量,然后再计算一下自己手里拿到的投票结果,根据一定的规则计算自己是否赢得了选举。
char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {......//计算所有的哨兵数量voters = dictSize(master->sentinels)+1; /* All the other sentinels and me.*/......//计算选票......voters_quorum = voters/2+1;//当选leader成功的条件有两个。//1、得票超过半数//2、得票数大于配置文件中预设的quorumif (winner && (max_votes < voters_quorum || max_votes < master->quorum))winner = NULL;winner = winner ? sdsnew(winner) : NULL;sdsfree(myvote);dictRelease(counters);//返回选举出来的哨兵leader的runidreturn winner;
}
经过选举,最终会得到一个哨兵leader。选出了leader,就要开始故障转移了,那故障转移就要选出一台从节点,来作为新的主节点,那具体选择哪台呢?
4、哨兵leader选择一台从节点作为新主节点
选择新主节点的逻辑,在sentinel.c文件的sentinelFailoverSelectSlave方法中。
void sentinelFailoverSelectSlave(sentinelRedisInstance *ri) {//选择一个从节点作为新的主节点sentinelRedisInstance *slave = sentinelSelectSlave(ri);if (slave == NULL) {sentinelEvent(LL_WARNING,"-failover-abort-no-good-slave",ri,"%@");sentinelAbortFailover(ri);} else {//将选举出来的slave节点广播出去sentinelEvent(LL_WARNING,"+selected-slave",slave,"%@");slave->flags |= SRI_PROMOTED;ri->promoted_slave = slave;//故障转移状态机进入下一个阶段ri->failover_state = SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE;ri->failover_state_change_time = mstime();sentinelEvent(LL_NOTICE,"+failover-state-send-slaveof-noone",slave, "%@");}
}
选择的逻辑主要在sentinelSelectSlave中。我们看一下方法的实现。首先,剔除一些不满足条件的从节点
sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master) {......if (master->flags & SRI_S_DOWN)max_master_down_time += mstime() - master->s_down_since_time;max_master_down_time += master->down_after_period * 10;......//遍历master节点的所有从节点di = dictGetIterator(master->slaves);while((de = dictNext(di)) != NULL) {sentinelRedisInstance *slave = dictGetVal(de);mstime_t info_validity_time;//从节点是主观下线或者客观下线,则不参加主节点的选举if (slave->flags & (SRI_S_DOWN|SRI_O_DOWN)) continue;//从节点断连,不参加选举if (slave->link->disconnected) continue;//从节点上一次可用的事件距离现在超过5秒,则不参加主节点的选举if (mstime() - slave->link->last_avail_time > SENTINEL_PING_PERIOD*5) continue;//如果从节点的优先级配置是0,则不参加主节点的选举if (slave->slave_priority == 0) continue;/* If the master is in SDOWN state we get INFO for slaves every second.* Otherwise we get it with the usual period so we need to account for* a larger delay. */if (master->flags & SRI_S_DOWN)info_validity_time = SENTINEL_PING_PERIOD*5;elseinfo_validity_time = SENTINEL_INFO_PERIOD*3;//计算当前slave上一次收到info输出的时间差,不符合条件的剔除if (mstime() - slave->info_refresh > info_validity_time) continue;//计算当前slave上一次断连的时间,不符合条件的剔除if (slave->master_link_down_time > max_master_down_time) continue;instance[instances++] = slave;}dictReleaseIterator(di);if (instances) {qsort(instance,instances,sizeof(sentinelRedisInstance*),compareSlavesForPromotion);selected = instance[0];}zfree(instance);return selected;
}
之后,对满足条件的从节点进行排序,排序逻辑在compareSlavesForPromotion中,具体逻辑为:
//入参的两个参数,a和b都是从节点
//返回的int值小于0时,a在前b在后,否则,a在后b在前
int compareSlavesForPromotion(const void *a, const void *b) {sentinelRedisInstance **sa = (sentinelRedisInstance **)a,**sb = (sentinelRedisInstance **)b;char *sa_runid, *sb_runid;//优先级高的从节点被选举为主节点if ((*sa)->slave_priority != (*sb)->slave_priority)return (*sa)->slave_priority - (*sb)->slave_priority;//优先级相同,选择处理复制数据最多的从节点,处理能力强if ((*sa)->slave_repl_offset > (*sb)->slave_repl_offset) {return -1; /* a < b */} else if ((*sa)->slave_repl_offset < (*sb)->slave_repl_offset) {return 1; /* a > b */}//如果复制能力相同。按字典顺序选择runid最小的从节点作为主节点。//老版本的redis,INFO命令中没有runid,所以runid为NULL。runid为NULL的节点比runid有值的节点大,要排在后面sa_runid = (*sa)->runid;sb_runid = (*sb)->runid;if (sa_runid == NULL && sb_runid == NULL) return 0;else if (sa_runid == NULL) return 1; /* a > b */else if (sb_runid == NULL) return -1; /* a < b */return strcasecmp(sa_runid, sb_runid);
}
通过该逻辑就选择出了一个从节点。然后故障转移状态机就变为:SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE,代表开始执行主从切换。
5、从节点切换为新主节点
主从切换的具体逻辑实现是在sentinel.c文件中的sentinelSendSlaveOf方法中,通过发送SLAVEOF NO ONE指令实现,将自己的角色由从节点切换成主节点。
int sentinelSendSlaveOf(sentinelRedisInstance *ri, char *host, int port) {......//执行SLAVEOF NO ONE命令,切换当前从节点为主节点。5.0版本后,此命令废弃,使用replicaof命令替代retval = redisAsyncCommand(ri->link->cc,sentinelDiscardReplyCallback, ri, "%s %s %s",sentinelInstanceMapCommand(ri,"SLAVEOF"),host, portstr);if (retval == C_ERR) return retval;ri->link->pending_commands++;......return C_OK;
}
从节点变成新的主节点,那客户端需要修改配置,连接到这个新的主节点,否则继续连接到旧的主节点就会有异常发生。那这个过程是如何实现的呢?
6、客户端如何获知新的主节点地址
是通过频道实现。在哨兵模式执行主从切换的过程中,关键的步骤都会向频道中发送消息,订阅了相关频道就可以获取到对应的消息。我们这个场景也不例外,哨兵模式选举出新的主节点并完成主从切换后,会向名为+switch-master的频道发送新主节点的ip和端口,客户端订阅这个频道,就可以获取到新主节点的ip和端口,自动完成新主节点的配置切换。