Redis Sentinel原理解析

1、概述

這篇文章主要用來說述redis高可用的實現原理,學習redis高可用實現,可讓咱們更好的進行系統設計。也能夠學習redis中的設計,將其遷移到其餘系統的實現中去。redis

2、總體架構

  • 監控:Sentinel節點會按期檢測Redis數據節點(主從)、其他Sentinel節點是否可達。
  • 主節點故障轉移:主節點不可用,將從節點晉升爲主節點並維護後續正確的主從關係。
  • 通知:Sentinel節點會將故障轉移的結果通知給應用方。
  • 配置提供者:在Redis Sentinel架構中,客戶端在初始化的時候鏈接的是Sentinel節點集合,從中獲取主節點信息

3、實現原理

節點通訊(互相通訊,實現功能)

1.每10s,sentinel會發送info給全部data節點。算法

  • info命令獲取從節點信息
  • 新節點加入可馬上感知,並加入集羣

2.每2s,向_sentinel_:hello頻道發送主節點判斷以及當前sentinel節點的信息。服務器

  • 發現其餘sentinel節點信息
  • 交換主節點狀態。作一些邏輯處理(好比發現其餘節點的master比本身新,則進行故障轉移)。
<Sentinel節點IP> <Sentinel節點端口> <Sentinel節點runId> <Sentinel節點配置版本> <主節點名字> <主節點Ip> <主節點端口> <主節點配置版本>
複製代碼

3.每1s,向其餘節點發送ping(alive check)markdown

  • 判斷節點是否存活

經過1和2,在sentinel啓動的時候只須要配置要監控的master,便可實現對其餘slave和sentinel的自動發現機制。主要依賴對redis data節點的發佈訂閱和info操做去作這個事情。網絡

主觀下線

主觀上判斷某個節點下線(在超時時間沒有ping經過)數據結構

客觀下線

若是主觀下線的是主節點,則會經過sentinel is- master-down-by-addr命令向其餘sentinel節點詢問對主節點的判斷。超高quorum數量,會進行客觀下線。架構

is-master-down-by-addr命令

全部sentinel節點除了定時pub hello消息,還經過這個命令進行p2p通訊。及時告知其餘sentinel信息。 該命令做用:app

  • 向其餘sentinel尋問對主節點的判斷
  • 領主選舉投票
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
複製代碼

若是runid爲*表明主節點下線命令運維

若是不爲*表明請求投票命令異步

主節點下線命令

sentinel is-master-down-by-addr 127.0.0.1 6379 0 *
複製代碼

領導選舉投票 使用raft的領主選舉算法,經過該命令投票選舉

命令返回結果

  • down_state
  • leader_runid *表明用來作主節點不可達,有具體的runid表明贊成runid成爲領導者。
  • leader_epoch 領導者任期

故障轉移

主節點故障,選舉從節點 1.選哪一個?

(1)過濾不健康的

(2)選slave-priority最高的從節點列表

(3)選擇複製偏移量最大的。

(4)選擇runid最小的

2.執行slaveof no one

3.向剩餘從節點發送命令,讓其成爲新master的從

4.將原來的主更新爲從。並保持對其關注

4、源碼分析

sentienl的存儲結構。

圖中每一個粉色的方框都表明一個SentinelRedisInstance,這是sentienl中統一的一個數據結構,簡單來說就是一塊內存,裏面存儲了節點的信息。每個redis data節點或者sentinel節點都會有一個SentinelRedisInstance(除了當前sentienl)。就比如每一條網絡鏈接都有一個socket同樣。

master表明監控的master信息。每一個sentinel有多個master(上面只畫了一個),表明能夠同時監控多個master節點。

SentinelRedisInstance結構有兩個hash:key爲名字,val爲SentinelRedisInstance

一個用來存儲當前master的slave,另外一個用來存儲一樣監控這個master的其餘sentinel節點(如上圖結構),使用hash 是爲了經過key進行快速查找對應結構。

// 其餘一樣監控這個主服務器的全部 sentinel
dict *sentinels; /* Other sentinels monitoring the same master. */
// 若是這個實例表明的是一個主服務器
// 那麼這個字典保存着主服務器屬下的從服務器
// 字典的鍵是從服務器的名字,字典的值是從服務器對應的 sentinelRedisInstance 結構
dict *slaves; /* Slaves for this master instance. */
複製代碼

固然只有SentinelRedisInstance爲master的時候,這兩個hash纔會被用到。因此sentinel的全部操做都圍繞這個master結構進行。sentinel經過info命令以及publish hello消息發現其餘的slave和sentinel,新加入的節點會加入對應hash中。

flags

惟一須要強調的是SentinelRedisInstance中的flags變量。該變量記錄了當前節點的狀態。(flags經過標誌位記錄多種狀態,每一個狀態一個位,經過|~進行更新)

  • 若是是master,則記錄master的狀態。
  • 若是是sentinel,則記錄當前sentienl一些判斷狀態。

因此sentinel徹底圍繞這些SentinelRedisInstance去維護整個集羣的邏輯。好比ping,info,就是遍歷SentinelRedisInstance,對其hostport進行ping,info等操做。

遍歷邏輯:先拿到master的SentinelRedisInstance,執行邏輯,而後再遍歷兩個hash處理。

服務啓動

sentinel 模式啓動會執行下面邏輯

if (server.sentinel_mode) {
    initSentinelConfig();
    initSentinel();
}
複製代碼

initSentinel主要就是一些初始化工做,包括sentinel結構建立、一些全局變量的初始化、以及註冊命令處理器。

sentinelHandleConfiguration

服務啓動會先加載配置。這個方法主要是sentinel配置的解析邏輯。

if (!strcasecmp(argv[0],"monitor") && argc == 5) {
    /* monitor <name> <host> <port> <quorum> */
    // 讀入 quorum 參數
    int quorum = atoi(argv[4]);
    // 檢查 quorum 參數必須大於 0
    if (quorum <= 0) return "Quorum must be 1 or greater.";

    // 建立主服務器實例
    if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2],
                                    atoi(argv[3]),quorum,NULL) == NULL)
    {
        switch(errno) {
        case EBUSY: return "Duplicated master name.";
        case ENOENT: return "Can't resolve master instance hostname.";
        case EINVAL: return "Invalid port number";
        }
    }
}else if (!strcasecmp(argv[0],"down-after-milliseconds") && argc == 3) {

    /* down-after-milliseconds <name> <milliseconds> */
    // 查找主服務器
    ri = sentinelGetMasterByName(argv[1]);
    if (!ri) return "No such master with specified name.";

    // 設置選項
    ri->down_after_period = atoi(argv[2]);
    if (ri->down_after_period <= 0)
        return "negative or zero time parameter.";
    sentinelPropagateDownAfterPeriod(ri);
}
複製代碼

核心就是對master的SentinelRedisInstance結構建立。而後將其餘配置初始化到該結構中。上面只列舉了部分代碼。其餘配置相似down-after-milliseconds的解析邏輯。

createSentinelRedisInstance

用來建立sentinel中實例。包括sentinel、被監控的redis的主從。每次有新增都會調用該方法建立一個實例,並加入到master的SentinelRedisInstance中(除了master自身)

  • SRI_MASTER 建立一個被監控的master實例。並將其加入到sentinel.masters這個hash表中
  • SRI_SLAVE 建立一個被監控的slave實例。並將其加入到master->slaves這個hash表中
  • SRI_SENTINEL 建立一個被監控的sentinel實例。並將其加入到master->sentinels這個hash表中

上面基本就是一些初始化工做。sentinel核心邏輯是須要時間函數驅動的,因此咱們直接看sentinelTimer邏輯。

sentinelTimer

該函數100ms執行一次。

void sentinelTimer(void) {
    // 記錄本次 sentinel 調用的事件,判斷是否須要進入 TITL 模式
    sentinelCheckTiltCondition();
    // 執行按期操做
    // 好比 PING 實例、分析主服務器和從服務器的 INFO 命令
    // 向其餘監視相同主服務器的 sentinel 發送問候信息
    // 並接收其餘 sentinel 發來的問候信息
    // 執行故障轉移操做,等等
    sentinelHandleDictOfRedisInstances(sentinel.masters);

    // 運行等待執行的腳本
    sentinelRunPendingScripts();

    // 清理已執行完畢的腳本,並重試出錯的腳本
    sentinelCollectTerminatedScripts();

    // 殺死運行超時的腳本
    sentinelKillTimedoutScripts();
    /* We continuously change the frequency of the Redis "timer interrupt" * in order to desynchronize every Sentinel from every other. * This non-determinism avoids that Sentinels started at the same time * exactly continue to stay synchronized asking to be voted at the * same time again and again (resulting in nobody likely winning the * election because of split brain voting). */
    server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}
複製代碼

sentinelCheckTiltCondition方法用來判斷是否進入ttl,而且記錄執行時間。

TITL模式:由於sentinel依賴本機時間驅動,若是系統時間出問題,或者由於進程阻塞致使的時間函數延遲調用。這時再去參與集羣邏輯會出現不正確的決策。所以若是當前時間和上一次執行時間差爲負值或者超過2s,該節點會進入TILT模式。

sentinelHandleDictOfRedisInstances

void sentinelHandleDictOfRedisInstances(dict *instances) {
    dictIterator *di;
    dictEntry *de;
    sentinelRedisInstance *switch_to_promoted = NULL;

    /* There are a number of things we need to perform against every master. */
    // 遍歷多個實例,這些實例能夠是多個主服務器、多個從服務器或者多個 sentinel
    di = dictGetIterator(instances);
    while((de = dictNext(di)) != NULL) {

        sentinelRedisInstance *ri = dictGetVal(de);
        // 執行調度操做
        sentinelHandleRedisInstance(ri);

        // 若是被遍歷的是主服務器,那麼遞歸地遍歷該主服務器的全部從服務器
        if (ri->flags & SRI_MASTER) {

            // 全部從服務器
            sentinelHandleDictOfRedisInstances(ri->slaves);
            // 全部 sentinel
            sentinelHandleDictOfRedisInstances(ri->sentinels);

            if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
                // 已選出新的主服務器
                switch_to_promoted = ri;
            }
        }
    }
    // 將原主服務器(已下線)從主服務器表格中移除,並使用新主服務器代替它
    if (switch_to_promoted)
        sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
    dictReleaseIterator(di);
}
複製代碼

這個方法就遞歸遍歷全部SentinelRedisInstance。。核心就是對被 Sentinel 監視的全部實例(包括主服務器、從服務器和其餘 Sentinel ) 進行按期操做。

邏輯很簡單,其實就是執行sentinelHandleRedisInstance方法。

若是有故障轉移(從節點升級爲主節點),則調用sentinelFailoverSwitchToPromotedSlave替換新主服務。

sentinelHandleRedisInstance方法

Sentinel的主邏輯流程,後面會一一介紹每一個方法的邏輯。該方法按期調用,非阻塞(中間的io命令都會異步出去)。

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    /* Every kind of instance */
    // 若是有須要的話,建立連向實例的網絡鏈接
    sentinelReconnectInstance(ri);

    // 根據狀況,向實例發送 PING、 INFO 或者 PUBLISH 命令
    sentinelSendPeriodicCommands(ri);
    /* ============== ACTING HALF ============= */
    /* We don't proceed with the acting half if we are in TILT mode. * TILT happens when we find something odd with the time, like a * sudden change in the clock. */
    if (sentinel.tilt) {
        // 若是 TILI 模式未解除,那麼不執行動做
        if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;

        // 時間已過,退出 TILT 模式
        sentinel.tilt = 0;
        sentinelEvent(REDIS_WARNING,"-tilt",NULL,"#tilt mode exited");
    }
    /* Every kind of instance */
    // 檢查給定實例是否進入 SDOWN 狀態
    sentinelCheckSubjectivelyDown(ri);

    /* Masters and slaves */
    if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
        /* Nothing so far. */
    }
    /* Only masters */
    /* 對主服務器進行處理 */
    if (ri->flags & SRI_MASTER) {

        // 判斷 master 是否進入 ODOWN 狀態
        sentinelCheckObjectivelyDown(ri);
        // 若是主服務器進入了 ODOWN 狀態,那麼開始一次故障轉移操做
        if (sentinelStartFailoverIfNeeded(ri))
            // 強制向其餘 Sentinel 發送 SENTINEL is-master-down-by-addr 命令
            // 刷新其餘 Sentinel 關於主服務器的狀態
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        // 執行故障轉移
        sentinelFailoverStateMachine(ri);
        // 若是有須要的話,向其餘 Sentinel 發送 SENTINEL is-master-down-by-addr 命令
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}
複製代碼

1.sentinelReconnectInstance主要就是建立和實例的鏈接。若是是master或者slave實例,則訂閱對應節點的__sentinel__:hello頻道,用來接受其餘sentinel廣播的消息。每一個sentinel會按期給該頻道publish。 由於啓動的時候只是初始化了實例數據,並無建立鏈接。對於每個實例,都是在這裏去建立鏈接的。

2.sentinelSendPeriodicCommands主要發送ping、info、publish命令。

3.若是當前在tilt模式中,直接返回,非tilt模式纔會執行後續操做。

4.sentinelCheckSubjectivelyDown,檢查給定實例是否進入 SDOWN 狀態

5.若是當前實例爲主服務器,則執行一些故障判斷和故障轉移操做。

能夠看出來sentienl的全部邏輯都是圍繞一個主服務進行的。

sentinelSendPeriodicCommands方法

這是咱們的核心關注點,sentinel之間的通訊邏輯。

這個方法主要是ping、info以及publish hello消息。到達執行時刻會執行對應邏輯。

  • ping每s執行一次,若是比設置的down_after_period(超時下線時間)小,則爲down_after_period。
  • info每10s執行一次。若是一次未執行則直接執行。若是主節點SDOWN或SRI_FAILOVER_IN_PROGRESS(故障轉移)則每s執行一次,爲了更快的捕捉服務器變更。
  • publish每2s執行一次。(用於發現其餘sentinel以及和其餘sentinel通訊)

redis經過pending_commands記錄當前正在異步執行的命令。若是超過100,則再也不發送,避免命令堆積。由於sentinel是依賴上一次響應時間來判斷是否發送命令,若是出現網絡阻塞或者波動,會致使頻繁發送。

Ping命令

發送邏輯在sentinelSendPeriodicCommands方法中。

Ping命令回調方法sentinelPingReplyCallback:,若是回調正常,更新對應字段(last_avail_time、last_pong_time),sentienl依賴這些字段進行判活。若是節點執行lua超時,則調用SCRIPT KILL嘗試殺死腳本。

info命令

info爲獲取redis節點信息的命令。處理返回結果的方法爲sentinelRefreshInstanceInfo

sentinelRefreshInstanceInfo方法主要是解析並提取須要的數據信息。下面介紹一些核心信息。

1.若是是主節點,提取其從節點的hostport,併爲從節點建立實例信息(若是是新發現的),sentinel依賴此方式發現其餘的slave節點。

if (sentinelRedisInstanceLookupSlave(ri,ip,atoi(port)) == NULL) {
    if ((slave = createSentinelRedisInstance(NULL,SRI_SLAVE,ip,
                atoi(port), ri->quorum, ri)) != NULL)
    {
        sentinelEvent(REDIS_NOTICE,"+slave",slave,"%@");
    }
}
複製代碼

2.若是是從節點,記錄當前從服務器對應主節點的信息。由於sentinel的記錄的主節點不必定是正確的(網絡分區致使切換延遲),因此經過info獲取到該從服務器最新信息。以供後面邏輯處理。

這裏其實兩種主(從)節點狀態:一種是sentienl認爲當前節點爲主(從)節點,另外一種是當前節點認爲的主(從)節點。

3.若是發生了角色轉變(info返回的和當前sentinel記錄的節點狀態不一致),更新轉變時間。若是tilt,直接返回。不然,根據該節點返回的role和sentinel記錄的role進行一些邏輯,具體邏輯咱們後面再研究。

Publish Hello消息

給對應channel發送的信息以下,主要包括對應sentinel的信息和當前master的信息

snprintf(payload,sizeof(payload),
    "%s,%d,%s,%llu," /* Info about this sentinel. */
    "%s,%s,%d,%llu", /* Info about current master. */
    ip, server.port, server.runid,
    (unsigned long long) sentinel.current_epoch,
    /* --- */
    master->name,master_addr->ip,master_addr->port,
    (unsigned long long) master->config_epoch);
複製代碼
訂閱處理hello消息 (sentinelProcessHelloMessage)

該方法爲sentinel處理hello信息的方法。

1.若是hello消息中sentinel節點爲新發現的節點(當前setinel不存在的),爲新節點建立實例,加入到列表中。這是sentinel發現其餘sentinel的惟一方式。

2.更新current_epoch=max(當前current_epoch,hello的current_epoch)

3.若是hello消息的master_config_epoch比該節點master的config_epoch大。則調用sentinelResetMasterAndChangeAddress方法切換當前master。config_epoch爲故障轉移使用的紀元。故障轉移以後會遞增。若是發現比較大的,說明進行了故障轉移,則信任hello中的master爲最新master

if (master->config_epoch < master_config_epoch) {
    master->config_epoch = master_config_epoch;
    if (master_port != master->addr->port ||
        strcmp(master->addr->ip, token[5]))
    {
        sentinelAddr *old_addr;

        sentinelEvent(REDIS_WARNING,"+config-update-from",si,"%@");
        sentinelEvent(REDIS_WARNING,"+switch-master",
            master,"%s %s %d %s %d",
            master->name,
            master->addr->ip, master->addr->port,
            token[5], master_port);

        old_addr = dupSentinelAddr(master->addr);
        sentinelResetMasterAndChangeAddress(master, token[5], master_port);
        sentinelCallClientReconfScript(master,
            SENTINEL_OBSERVER,"start",
            old_addr,master->addr);
        releaseSentinelAddr(old_addr);
    }
}
複製代碼
sentinelCheckSubjectivelyDown方法

檢查實例是否進入 SDOWN 狀態

1.若是實例符合斷線重連的條件,則斷開該實例鏈接,等待下次從新鏈接。其實就是對不活躍實例進行斷線重連。

2.設置或者取消SDOWN標誌

達到下面兩個條件設置爲SDOWN,不然取消SDOWN標誌位

  • 超過超時時間沒有回覆命令,則設置爲SDOWN
  • Sentinel認爲實例是主服務器,這個服務器向 Sentinel 報告它將成爲從服務器,但在超過給定時限以後,服務器仍然沒有完成這一角色轉換

主節點單獨邏輯

if (ri->flags & SRI_MASTER) {
    // 判斷 master 是否進入 ODOWN 狀態
    sentinelCheckObjectivelyDown(ri);
    // 若是主服務器進入了 ODOWN 狀態,那麼開始一次故障轉移操做
    if (sentinelStartFailoverIfNeeded(ri))
        // 強制向其餘 Sentinel 發送 SENTINEL is-master-down-by-addr 命令
        // 刷新其餘 Sentinel 關於主服務器的狀態
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);

    // 執行故障轉移
    sentinelFailoverStateMachine(ri);
    // 若是有須要的話,向其餘 Sentinel 發送 SENTINEL is-master-down-by-addr 命令
    // 刷新其餘 Sentinel 關於主服務器的狀態
    // 這一句是對那些沒有進入 if(sentinelStartFailoverIfNeeded(ri)) { /* ... */ }
    // 語句的主服務器使用的
    sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
複製代碼

sentinelCheckObjectivelyDown

判斷當前主節點是否進入ODown狀態。

經過遍歷全部sentinel實例的flags標誌位進行判斷。若是一半以上主觀下線,則變動爲客觀下線。這個狀態位是在is-master-down-by-addr命令回調中更新的。

sentinelStartFailoverIfNeeded

判斷是否須要進行故障轉移

void sentinelStartFailover(sentinelRedisInstance *master) {
    redisAssert(master->flags & SRI_MASTER);

    // 更新故障轉移狀態
    master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;

    // 更新主服務器狀態
    master->flags |= SRI_FAILOVER_IN_PROGRESS;

    // 更新紀元
    master->failover_epoch = ++sentinel.current_epoch;

    sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
        (unsigned long long) sentinel.current_epoch);

    sentinelEvent(REDIS_WARNING,"+try-failover",master,"%@");

    // 記錄故障轉移狀態的變動時間
    master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
    master->failover_state_change_time = mstime();
}
複製代碼

判斷條件故障轉移:

1.進入ODOWN而且沒有在故障轉移中。

2.若是發現故障轉移過於頻繁也不執行。

若是須要故障轉移,則更新當前master的信息,主要是failover_state、failover_epoch等字段。

failover_epoch爲當前master紀元+1。

failover_epoch做用:

  • 其餘sentinel依賴這個字段判斷是否須要進行故障轉移。這個在以前的hello中有說到。
  • 當前sentinel依賴這個紀元選出執行故障轉移的leader。由於選舉使用的也是該紀元。選舉出來的leader的紀元應該一致。每次選舉都會產生一個新的leader,最新的紀元最權威,這是raft領導選舉的核心概念。雖然raft使用的是term。

failover_state表明當前故障轉移狀態。故障轉移操做須要依賴該狀態。

故障轉移操做須要從sentienl中選舉一個執行。因此這只是先更新狀態。

sentinelAskMasterStateToOtherSentinels

向其餘sentine詢問master狀態。

這會遍歷全部sentinel,若是當前sentinel認爲master下線,而且鏈接正常,會發送is-master-down-by-addr命令

sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
複製代碼

若是本sentinel檢測到master 主觀下線(經過failover_state判斷),則runid爲當前server的runid,表明讓其餘sentienl給本身投票。

若是是master客觀下線,則runid=*,表明告訴其餘sentienl,主節點下線。這是sentinel通知其餘sentinel主節點下線的惟一方式。

is-master-down-by-addr命令處理

該邏輯在sentinelCommand中執行

ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,
    c->argv[2]->ptr,port,NULL);

if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
                            (ri->flags & SRI_MASTER))
    isdown = 1;

/* Vote for the master (or fetch the previous vote) if the request * includes a runid, otherwise the sender is not seeking for a vote. */
if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
    leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
                                    c->argv[5]->ptr,
                                    &leader_epoch);
}

/* Reply with a three-elements multi-bulk reply: * down state, leader, vote epoch. */
// 多條回覆
// 1) <down_state> 1 表明下線, 0 表明未下線
// 2) <leader_runid> Sentinel 選舉做爲領頭 Sentinel 的運行 ID
// 3) <leader_epoch> 領頭 Sentinel 目前的配置紀元
addReplyMultiBulkLen(c,3);
addReply(c, isdown ? shared.cone : shared.czero);
addReplyBulkCString(c, leader ? leader : "*");
addReplyLongLong(c, (long long)leader_epoch);
複製代碼

若是節點下線回覆down_state1,不然爲0

根據ip和port獲取ri(sentinelRedisInstance),若是ri是主節點,而且runid不爲*,則進行選舉投票。

投票邏輯

請求的req_epoch>當前sentinel的current_epoch(更新sentinel的current_epoch)

req_epoch>master的master.leader_epoch,而且>=sentinel.current_epoch,更新master的leader爲req_runid。而後投票給當前req_runid節點。

其實就是判斷epoch。若是請求的epoch比較大,那就投票便可。和raft的領導選舉同樣。

is-master-down-by-addr命令回調(sentinelReceiveIsMasterDownReply)

// 更新最後一次回覆詢問的時間
ri->last_master_down_reply_time = mstime();

// 設置 SENTINEL 認爲主服務器的狀態
if (r->element[0]->integer == 1) {
    // 已下線
    ri->flags |= SRI_MASTER_DOWN;
} else {
    // 未下線
    ri->flags &= ~SRI_MASTER_DOWN;
}
// 若是運行 ID 不是 "*" 的話,那麼這是一個帶投票的回覆
if (strcmp(r->element[1]->str,"*")) {
    /* If the runid in the reply is not "*" the Sentinel actually * replied with a vote. */
    sdsfree(ri->leader);
    // 打印日誌
    if (ri->leader_epoch != r->element[2]->integer)
        redisLog(REDIS_WARNING,
            "%s voted for %s %llu", ri->name,
            r->element[1]->str,
            (unsigned long long) r->element[2]->integer);
    // 設置實例的領頭
    ri->leader = sdsnew(r->element[1]->str);
    ri->leader_epoch = r->element[2]->integer;
}
複製代碼
  • <down_state> 1 表明下線, 0 表明未下線
  • <leader_runid> Sentinel 選舉做爲領頭 Sentinel 的運行 ID
  • <leader_epoch> 領頭 Sentinel 目前的配置紀元

1.更新回覆時間

2.更新被詢問的sentinel的flag。用於ODOWN判斷。

3.若是leader_runid非*,表明投票信息,更新該sentinel的leader信息。

ri的leader字段有兩個狀態:

  • 若是ri一個主服務器實例,那麼 leader 將是負責進行故障轉移的 Sentinel 的運行 ID 。
  • 若是ri一個 Sentinel 實例,那麼 leader 就是被選舉出來的領頭 Sentinel 。

執行故障轉移(sentinelFailoverStateMachine)

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;

    // 等待升級生效,若是升級超時,那麼從新選擇新主服務器
    // 具體狀況請看 sentinelRefreshInstanceInfo 函數
    case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
        sentinelFailoverWaitPromotion(ri);
        break;

    // 向從服務器發送 SLAVEOF 命令,讓它們同步新主服務器
    case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
        sentinelFailoverReconfNextSlave(ri);
        break;
}
複製代碼

整個流程如上:

1.sentinelFailoverWaitStart會根據當前全部sentinel的leader以及當前故障轉移紀元選出來leader。若是發現leader是本身,則切換failover_state爲SENTINEL_FAILOVER_STATE_SELECT_SLAVE,執行下一個case。不然跳出。

2.根據邏輯選舉從服務器做爲新的主節點,若是沒有選出,清空故障轉移狀態。選出成功後繼續更新狀態執行下一個case。選擇邏輯上面有說過。

3.升級新節點爲主節點,若是發現斷線並超時,則終止故障轉移邏輯。這個其實就是異步發送slave of命令。讓從節點升級爲主節點。這個命令在redis複製源碼解析中說過。

4.SENTINEL_FAILOVER_STATE_WAIT_PROMOTION狀態只是等待升級的另外一個邏輯,若是升級超時則終止故障轉移。 如何檢測從節點升級主節點成功?

其實在info中有一段邏輯:

if ((ri->master->flags & SRI_FAILOVER_IN_PROGRESS) &&
    (ri->master->failover_state ==
        SENTINEL_FAILOVER_STATE_WAIT_PROMOTION))
{
    // 更新從服務器的主服務器(已下線)的配置紀元
    ri->master->config_epoch = ri->master->failover_epoch;
    // 設置從服務器的主服務器(已下線)的故障轉移狀態
    // 這個狀態會讓從服務器開始同步新的主服務器
    ri->master->failover_state = SENTINEL_FAILOVER_STATE_RECONF_SLAVES;
    // 更新從服務器的主服務器(已下線)的故障轉移狀態變動時間
    ri->master->failover_state_change_time = mstime();
    // 將當前 Sentinel 狀態保存到配置文件裏面
    sentinelFlushConfig();
    // 發送事件
    sentinelEvent(REDIS_WARNING,"+promoted-slave",ri,"%@");
    sentinelEvent(REDIS_WARNING,"+failover-state-reconf-slaves",
        ri->master,"%@");
    // 執行腳本
    sentinelCallClientReconfScript(ri->master,SENTINEL_LEADER,
        "start",ri->master->addr,ri->addr);
}
複製代碼

在這裏表明當前從節點已經升級成功,則更新config_epoch。config_epoch其實在hello消息中被用到。若是其餘sentienl發現它的config_epoch小於hello消息中的config_epoch,則會重置master的地址。

5.SENTINEL_FAILOVER_STATE_RECONF_SLAVES

Info更新成功後,會執行sentinelFailoverReconfNextSlave方法。其實就是向全部從服務器發送slave of命令。 這裏會受parallel_syncs參數限制。控制並行slave of數量,避免主節點網絡壓力。

成功以後更新failover_state狀態爲SENTINEL_FAILOVER_STATE_UPDATE_CONFIG

sentinelFailoverSwitchToPromotedSlave

最終會調用該方法切換主服務器,整個轉移過程結束。

故障轉移失敗

若是執行故障轉移的leader存活的狀況,轉移超時,則會調用sentinelAbortFailover方法終止故障轉移。 若是轉移過場中leader宕機,其餘節點會繼續執行故障轉移邏輯。

5、開發運維

配置參數

down-after-milliseconds 節點alive check的ttl

sentinel parallel-syncs 主節點宕機,容許並行複製數量

sentinel failover-timeout 故障轉移的超時時間,若是當前時間-上次轉移狀態更新時間大於該值,則會終止轉移。

sentinel notification-script 故障監控,sentinel警告級別的事件發生,會出發對應路徑下的腳本,經過腳本可接收參數。進行監控。

客戶端鏈接sentinel

啓動經過遍歷sentinel獲取redis主節點。而後訂閱每一個sentinel的switch事件,保證在主備切換的時候能監聽到。若是監聽到變化 從新初始化鏈接池便可。

須要注意的是,只有故障轉移完成纔會發送此事件。

6、總結

整個實現比較複雜,可是按照每一個點仍是能夠理清楚。經過ping、info、以及pubsub hello消息實現通訊。保證整個系統的一致性。每次故障轉移只會由一個sentinel執行,這個選舉過程依賴raft算法leader選舉邏輯。故障轉移邏輯依賴超時時間避免死狀態。整個邏輯依賴狀態機進行切換,有條不紊,值得借鑑。

相關文章
相關標籤/搜索