redis哨兵高可用-源碼篇

前段時間寫過兩篇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和端口,自動完成新主節點的配置切換。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/533841.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/533841.shtml
英文地址,請注明出處:http://en.pswp.cn/news/533841.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python獲取網頁數據對電腦性能_【Python】網頁數據爬取實戰

由于網頁結構跟之前有變化&#xff0c;還不是很熟悉。代碼待完善&#xff0c;問題記錄&#xff1a;騰訊新聞二級網頁內容爬取有問題。鏈家網站頭文件沒有用到。爬取一條騰訊視頻的header內容&#xff0c;存入txt。要求&#xff1a;包含網頁鏈接包含title包含所有headers信息imp…

python集合去重_python集合去重

[python中對list去重的多種方法 怎么快速的對列表進行去重呢&#xff0c;去重之后原來的順序會不會改變呢&#xff1f; 1.以下的幾種情況結果是一樣的&#xff0c;去重之后順序會改變: i [1,2,3,3,4,2,3,4,5,6,1] news_i [] for id in i: if id not in news_i: news_i.append(…

linux進程pid分配規則,Linux進程pid分配法【轉】

一. 概述Android系統創建進程&#xff0c;最終的實現還是調用linux fork方法&#xff0c;對于linux系統每個進程都有唯一的 進程ID(值大于0)&#xff0c;也有pid上限&#xff0c;默認為32768。 pid可重復利用&#xff0c;當進程被殺后會回收該pid&#xff0c;以供后續的進程pid…

sqlserver date類型和字符串比較_基于SQL Server數據庫搭建主從復制實現讀寫分離實戰演練...

一、課程介紹讀寫分離(主從同步)從字面意思就可以理解&#xff0c;就是把對數據庫的讀操作和寫操作分離開。讀寫分離在網站發展初期可以一定程度上緩解讀寫并發時產生鎖的問題&#xff0c;將讀寫壓力分擔到多臺服務器上。讀寫分離的基本原理是讓主數據庫處理事務性增、改、刪操…

linux非標準頭文件,Linux學習:unix的標準化的實現(Linux中各種限制-數據類型-各種標準化頭文件介紹)...

作為Linux的前身&#xff0c;unix標準化是十分重要的。我在這里挑幾個重要的點說明。1&#xff1a;Linux中各種限制。Linux中限制有編譯時限制和運行時限制&#xff0c;另外有一些限制是由于我們的實現不同而不同&#xff0c;因此我們需要調用對應的函數獲取對應的值不同。(eg&…

python怎么導入大小字母_python遍歷小寫英文字母的方法

python遍歷小寫英文字母的方法在c、c等語言中&#xff0c;可以用字符1的for循環來遍歷小寫的26個英文字母&#xff0c;但是由于python語言的特殊性&#xff0c;通過a 1這種代碼并不能成功遍歷&#xff0c;以下是在python中遍歷英文字母的簡潔代碼&#xff1a;import stringfor…

51單片機怎么顯示當前時間_(進階篇)51單片機之按鍵控制蜂鳴器、數碼管、按鍵值移位顯示...

一、實操演示- 按鍵控制蜂鳴器1、圖文詳細獨立按鍵硬件電路蜂鳴器硬件電路2、連接方式&#xff1a;J20的第3號引腳連接到J7引腳&#xff0c;即P15連接J7。J29的第7、8號引腳連接到JP1的第1、2號引腳&#xff0c;即P31連接k1&#xff0c;P30連接k2。下載程序后&#xff0c;觀察現…

linux怎么運行g77,Linux安裝g77編譯器的技巧

在Ubuntu10.10系統中&#xff0c;g77已經被gfortran完全替代了&#xff0c;但并不能完全兼容過去的g77&#xff0c;這樣就不能使用一些用977編譯的程序了。所以我們只能自己再安裝g77了。今天華軍小編給大家展示的是Linux安裝g77編譯器的技巧&#xff0c;精心挑選的內容希望大家…

oracle 結果集已耗盡_java.sql.SQLException: 結果集已耗盡

編寫了jsp誰能幫忙看下有什么問題編寫了jsp 誰能幫忙看下 有什么問題技術交流ResultSet rsnull;String str"select publish_id,publish_name,publish_time,publish_text from comment_tb order by publish_time desc ";query.connect();rsquery.select(str);while(rs…

vs使用未初始化的內存怎么解決_遇到C語言內存錯誤怎么辦?一定要找準這六個原因...

一、沒有為指針分配內存定義了指針變量&#xff0c;但是沒有為指針分配內存&#xff0c;即指針沒有指向一塊合法的內存。淺顯的例子就不舉了&#xff0c;這里舉幾個比較隱蔽的例子。1、結構體成員指針未初始化struct student { char *name; int score; }stu,*pstu; int main() …

linux服務器如何設置雙網卡,linux服務器設置(雙網卡共享上網)

一、網絡拓補結構&#xff1a;服務器&#xff1a;兩網卡的設置&#xff1a;eth0:202.96.168.100 掩碼&#xff1a;255.255.255.0 網關&#xff1a;202.96.168.68 #與 Inte.Net 相聯eth1:192.168.1.1掩碼&#xff1a;255.255.255.0#與局域網相聯客戶機子網段&#xff1a;192.1…

pwn環境搭建_pwndbg、pwntools環境搭建(Unix系統)

目錄[TOC]pwndbg環境搭建項目地址https://github.com/pwndbg/pwndbg搭建過程1、安裝環境基礎gitpythonpython-pip2、安裝過程使用git命令克隆遠程項目到本地。git clone https://github.com/pwndbg/pwndbg進入項目根目錄并執行一鍵安裝腳本cd pwndbg && ./setup.sh該腳…

cad求和插件_黑科技 | 無BIM建模下平面CAD自動生成門窗表

如果你接到的施工圖既不是用天正出的&#xff0c;也不是用revit出的&#xff0c;還得統計門窗表&#xff0c;那么你需要讀完這篇文章。為了能夠讓自己和所有底層同行們從這項無腦又燒腦的機械勞動中解脫&#xff0c;C君近期利用茶余飯后的時間開發了一個小插件&#xff0c;可以…

linux數據庫實例開機啟動,linux下數據庫實例開機自啟動設置

linux下數據庫實例開機自啟動設置 1、修改/oratab [rootorg54 ~]# vi/etc/oratab --把N改為Y&#xff0c;如下提示 # This file is used by ORACLEutilities. It is created by root.sh # and updated by the Database ConfigurationAssistant when creating # a datablinux下數…

panic 蘋果aop_Go Web開發之Revel - 攔截器

一個攔截器是一個框架在調用action方法前或后調用的函數. 它允許一種AOP的形式, 它經常被用于做下面幾種事情:Request loggingError handlingStats keeping在Revel里, 一個攔截器能接受兩種形式:1. 函數攔截器: 一個函數滿足沒有訪問特定的應用程序Controller被調用在應用程序中…

make找不到linux內核函數,linux內核make menuconfig出錯

今天實驗剛從服務器上遷移過來的維護的linux 9260的內核&#xff0c;使用make menuconfig時出錯&#xff0c;報錯為&#xff1a;yongtaoyongtao-desktop:~/public/linux_release/linux-2.6.24$ make menuconfigHOSTCC scripts/kconfig/conf.oHOSTCC scripts/kconfig/kxgettex…

tensorboard ckpt pb 模型的輸出節點_算法工程化系列——模型固化

摘要基于tensorflow訓練的模型一般被保存為ckpt形式的文件&#xff0c;隨著當前深度學習模型網絡越來越大&#xff0c;對應模型也會非常大。當對外提供服務的時候&#xff0c;如果采用ckpt的形式&#xff0c;服務進程被調起來非常困難&#xff0c;且推理服務一般速度也較慢(會達…

深度linux內核升級,深度操作系統 2020.11.11 更新發布:內核升級

原標題&#xff1a;深度操作系統 2020.11.11 更新發布&#xff1a;內核升級IT之家11月11日消息 今日&#xff0c;深度操作系統宣布2020.11.11 更新現已發布。本次更新包括升級內核、Debian 10.6 倉庫以及系統安全性更新。系統安全方面&#xff0c;本次更新修復了 Firefox-ESR 安…

unity 使用mysql實現登錄注冊_用mysql實現登錄注冊功能

1、創建用戶表表結構如下idunameupwdisdelete注意&#xff1a;需要對密碼進行加密。如果使用md5加密&#xff0c;則密碼包含32個字符。如果使用sha1加密&#xff0c;則密碼包含40個字符&#xff0c;這里使用這種方式。md5加密方式&#xff1a;import hashlibpwd 123456my_md5 …

python爬電影_使用Python多線程爬蟲爬取電影天堂資源

最近花些時間學習了一下Python&#xff0c;并寫了一個多線程的爬蟲程序來獲取電影天堂上資源的迅雷下載地址&#xff0c;代碼已經上傳到GitHub上了&#xff0c;需要的同學可以自行下載。剛開始學習python希望可以獲得寶貴的意見。 先來簡單介紹一下&#xff0c;網絡爬蟲的基本實…