(十六)高併發redis學習筆記:哨兵底層原理以及slave選舉算法

1、sdown和odown轉換機制

sdown和odown兩種失敗狀態:

  • sdown是主觀宕機,就一個哨兵如果自己覺得一個master宕機了,那麼就是主觀宕機。sdown達成的條件很簡單,如果一個哨兵ping一個master,超過了is-master-down-after-milliseconds指定的毫秒數之後,就主觀認爲master宕機
  • odown是客觀宕機,如果quorum數量的哨兵都覺得一個master宕機了,那麼就是客觀宕機。

sdown到odown轉換的條件很簡單,如果一個哨兵在指定時間內,收到了quorum指定數量的其他哨兵也認爲那個master是sdown了,那麼就認爲是odown了,客觀認爲master宕機。

我們可以通過源碼來看看哨兵是如何工作的:
首先,在定時任務severCron()函數中,如果發現是哨兵則會執行哨兵的計時器sentinelTimer()

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    /* Run the Sentinel timer if we are in sentinel mode. */
    if (server.sentinel_mode) sentinelTimer();
    ...
}

sentinelTimer()計時器中,會針對每一個哨兵進行處理,調用函數爲sentinelHandleDictOfRedisInstances(),檢查所有的實例。

void sentinelTimer(void) {
    ...
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    ...
} 

sentinelHandleDictOfRedisInstances()函數中,主要是遞歸遍歷,調用sentinelHandleRedisInstance()對實例進行處理。


void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {

    sentinelReconnectInstance(ri);
    // 發送ping消息給這個實例
    sentinelSendPeriodicCommands(ri);
    if (sentinel.tilt) {
        if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
        sentinel.tilt = 0;
        sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
    }
    // 判斷是否主觀下線
    sentinelCheckSubjectivelyDown(ri);
    
    if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
        /* Nothing so far. */
    }
    // 只針對master節點
    if (ri->flags & SRI_MASTER) {
        // 判斷是都客觀下線
        sentinelCheckObjectivelyDown(ri);
        if (sentinelStartFailoverIfNeeded(ri))
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        sentinelFailoverStateMachine(ri);
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

如何判斷主觀下線:

void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;
    // act_ping_time:最後一個未完成的ping(之後沒有收到pong)發出的時間。當接收到一個pong時,該字段被設置爲0,如果該值爲0並且發送了一個新的ping,則再次設置爲當前時間。(說明已經出現ping過沒回復)
    if (ri->link->act_ping_time)
        elapsed = mstime() - ri->link->act_ping_time;
    else if (ri->link->disconnected)
        // 連接不上,還沒有出現ping過沒回復
        elapsed = mstime() - ri->link->last_avail_time;
    // 如果檢測到連接的活躍度(activity)很低,那麼考慮重斷開連接,並進行重連
    if (ri->link->cc &&
        (mstime() - ri->link->cc_conn_time) >
        SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
        ri->link->act_ping_time != 0 && /* There is a pending ping... */
        (mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) &&
        (mstime() - ri->link->last_pong_time) > (ri->down_after_period/2))
    {
        // 連接時長已超過最短連接間隔 ping已經發出,但在down_after_period/2的時間內沒有收到pong響應
        // 斷開實例的cc連接(命令連接)
        instanceLinkCloseConnection(ri->link,ri->link->cc);
    }
   // 最後一次從這個服務器接收消息的時間大於發佈publish的最大時間的3倍,也就是6秒時斷開訂閱頻道信息的連接
    if (ri->link->pc &&
        (mstime() - ri->link->pc_conn_time) >
         SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
        (mstime() - ri->link->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
    {
        // 斷開發布訂閱的連接 pc
        instanceLinkCloseConnection(ri->link,ri->link->pc);
    }
    
    //現在距離上次ping的時間已經超過了down_after_period,可以判定爲主觀宕機
    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)))
    {
        //判斷爲主觀宕機
        if ((ri->flags & SRI_S_DOWN) == 0) {
            // 發送事件
            sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
            // 記錄主觀宕機的時間
            ri->s_down_since_time = mstime();
            //更新主觀宕機的狀態
            ri->flags |= SRI_S_DOWN;
        }
    } else {
        /* 它活過來了,判定會不宕機*/
        if (ri->flags & SRI_S_DOWN) {
            // 發送事件
            sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
            // 移除掉之前的宕機標識
            ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
        }
    }
}


如何判斷客觀宕機,sentinelCheckObjectivelyDown(ri)


void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    unsigned int quorum = 0, odown = 0;
    //當前的sentinel判斷它爲客觀單價
    if (master->flags & SRI_S_DOWN) {
        /* Is down for enough sentinels? */
        //需要滿足條件
        quorum = 1; /* the current sentinel. */
        /* Count all the other sentinels. */
        // 遍歷所有的sentinel
        di = dictGetIterator(master->sentinels);
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);
            if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
        //釋放迭代器
        dictReleaseIterator(di);
        //是否滿足認同數量大於等於quorum,滿足則認爲是客觀宕機(odown)
        if (quorum >= master->quorum) odown = 1;
    }
    /* 如果是odown*/
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            // 發送事件 
            sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
                // 設置odown標識
            master->flags |= SRI_O_DOWN;
            // 記錄事件
            master->o_down_since_time = mstime();
        }
    } else {
        if (master->flags & SRI_O_DOWN) {
            // 沒有進入odown,發送事件
            sentinelEvent(LL_WARNING,"-odown",master,"%@");
            // 移除標識
            master->flags &= ~SRI_O_DOWN;
        }
    }
}


2、哨兵集羣的自動發現機制

哨兵互相之間的發現,是通過redis的pub/sub系統實現的,每個哨兵都會往__sentinel__:hello這個channel裏發送一個消息,這時候所有其他哨兵都可以消費到這個消息,並感知到其他的哨兵的存在。
消息主要包括:

  • s_ip: sentinel的ip
  • s_port: sentinel的端口port
  • s_runnid:sentinel的runnid
  • s_epoch:紀元,版本
  • m_name:master的名稱
  • m_ip:master的ip
  • m_port:master 的端口
  • m_epoch:master的epoch

每隔兩秒鐘,每個哨兵都會往自己監控的某個master+slaves對應的__sentinel__:hellochannel裏發送一個消息,內容是自己的hostiprunid還有對這個master的監控配置

每個哨兵也會去監聽自己監控的每個master+slaves對應的__sentinel__:hello channel,然後去感知到同樣在監聽這個master+slaves的其他哨兵的存在

每個哨兵還會跟其他哨兵交換對master的監控配置,互相進行監控配置的同步

3、slave配置的自動糾正

哨兵會負責自動糾正slave的一些配置,比如slave如果要成爲潛在的master候選人,哨兵會確保slave在複製現有master的數據; 如果slave連接到了一個錯誤的master上,比如故障轉移之後,那麼哨兵會確保它們連接到正確的master上

4、slave->master選舉算法

如果一個master被認爲odown了,而且majority哨兵都允許了主備切換,那麼某個哨兵就會執行主備切換操作,此時首先要選舉一個slave來,優先級會考慮slave的一些信息。

  • (1)跟master斷開連接的時長,如果一個slave跟master斷開連接已經超過了down-after-milliseconds的10倍,外加master宕機的時長,也就是:(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state ,那麼slave就被認爲不適合選舉爲master,被排除掉。(這個僅僅是用於排除,不用於排序),剩下的執行後面的三個比較。
  • (2)slave優先級,slave priority越低,優先級就越高,這個是可以進行配置的。默認是100,越小越優先。
  • (3)複製offset,如果slave priority相同,那麼看replica offset,哪個slave複製了越多的數據,offset越靠後,優先級就越高
  • (4)run id,選擇一個run id比較小的那個slave

5、quorum和majority

每次一個哨兵要做主備切換,首先需要quorum數量的哨兵認爲odown,然後選舉出一個哨兵來做切換,這個哨兵還得得到majority哨兵的授權,才能正式執行切換

如果quorum < majority,比如5個哨兵,majority就是3,quorum設置爲2,那麼就3個哨兵授權就可以執行切換

但是如果quorum >= majority,那麼必須quorum數量的哨兵都授權,比如5個哨兵,quorum是5,那麼必須5個哨兵都同意授權,才能執行切換。

這個quorum是我們自己設置的, majority是客觀存在的。

6、configuration epoch

哨兵會對一套redis master+slave進行監控,有相應的監控的配置,也就是可以理解爲配置版本。

執行切換的那個哨兵,會從要切換到的新master(salve->master)那裏得到一個configuration epoch,這就是一個version號,每次切換的version號都必須是唯一的

如果第一個選舉出的哨兵切換失敗了,那麼其他哨兵,會等待failover-timeout時間,然後接替繼續執行切換,此時會重新獲取一個新的configuration epoch,作爲新的version號。

7、configuraiton傳播

哨兵完成切換之後,會在自己本地更新生成最新的master配置,然後同步給其他的哨兵,就是通過之前說的pub/sub消息機制。要知道,主備切換是一個哨兵在幹,幹完了總得讓別人知道是不是。

這裏之前的version號就很重要了,因爲各種消息都是通過一個channel去發佈和監聽的,所以一個哨兵完成一次新的切換之後,新的master配置是跟着新的version號的。其他的哨兵會訂閱這個消息,發現更新的epoch就認爲master已經切換過了,接着把自己存的信息更新。這樣,就可以保持哨兵裏面的信息一致性了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章