Redis源碼剖析和註釋(二十四)--- Redis Sentinel實現(哨兵操作的深入剖析)

Redis Sentinel實現(下)

本文是Redis Sentinel實現(上)篇文章的下半部分剖析。主要剖析以下內容:

4. 哨兵的使命

sentinel.c文件詳細註釋:Redis Sentinel詳細註釋

我們這一部分將會詳細介紹標題3.2小節的內容,以下分析,是站在哨兵節點的視角。

我們當時已經簡單分析到

  • sentinelHandleRedisInstance()函數用來處理主節點、從節點和哨兵節點的週期性操作。
  • sentinelFailoverSwitchToPromotedSlave()函數用來處理髮生主從切換的情況。

因此,先來看第一部分。

4.1 週期性的操作

我們先來看看sentinelHandleRedisInstance()函數中到底實現了哪些操作:

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    /* ========== MONITORING HALF ============ */
    /* ========== 一半監控操作 ============ */

    /* Every kind of instance */
    /* 對所有的類型的實例進行操作 */

    // 爲Sentinel和ri實例創建一個網絡連接,包括cc和pc
    sentinelReconnectInstance(ri);
    // 定期發送PING、PONG、PUBLISH命令到ri實例中
    sentinelSendPeriodicCommands(ri);

    /* ============== ACTING HALF ============= */
    /* ============== 一半故障檢測 ============= */

    // 如果Sentinel處於TILT模式,則不進行故障檢測
    if (sentinel.tilt) {
        // 如果TILT模式的時間沒到,則不執行後面的動作,直接返回
        if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
        // 如果TILT模式時間已經到了,取消TILT模式的標識
        sentinel.tilt = 0;
        sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
    }

    /* Every kind of instance */
    // 對於各種實例進行是否下線的檢測,是否處於主觀下線狀態
    sentinelCheckSubjectivelyDown(ri);

    /* Masters and slaves */
    // 目前對主節點和從節點的實例什麼都不做
    if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
        /* Nothing so far. */
    }

    /* Only masters */
    // 只對主節點進行操作
    if (ri->flags & SRI_MASTER) {
        // 檢查從節點是否客觀下線
        sentinelCheckObjectivelyDown(ri);
        // 如果處於客觀下線狀態,則進行故障轉移的狀態設置
        if (sentinelStartFailoverIfNeeded(ri))
            // 強制向其他Sentinel節點發送SENTINEL IS-MASTER-DOWN-BY-ADDR給所有的Sentinel獲取回覆
            // 嘗試獲得足夠的票數,標記主節點爲客觀下線狀態,觸發故障轉移
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        // 執行故障轉移操作
        sentinelFailoverStateMachine(ri);
        // 主節點ri沒有處於客觀下線的狀態,那麼也要嘗試發送SENTINEL IS-MASTER-DOWN-BY-ADDR給所有的Sentinel獲取回覆
        // 因爲ri主節點如果有回覆延遲等等狀況,可以通過該命令,更新一些主節點狀態
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

很明顯,該函數將週期性的操作分爲兩個部分,一部分是對一個的實例進行監控的操作,另一部分是對該實例執行故障檢測。

接下來就針對上面代碼的每一步進行詳細剖析,每一個標題對應一步。

4.1.1 建立連接

首先,執行的第一個函數就是sentinelReconnectInstance()函數,因爲在載入配置的時候,我們將創建的主節點實例加入到sentinel.masters字典的時候,該主節點的連接是關閉的,所以第一件事就是爲主節點和哨兵節點建立網絡連接。

查看sentinelReconnectInstance()函數代碼:

void sentinelReconnectInstance(sentinelRedisInstance *ri) {
    // 如果ri實例沒有連接中斷,則直接返回
    if (ri->link->disconnected == 0) return;
    // ri實例地址非法
    if (ri->addr->port == 0) return; /* port == 0 means invalid address. */
    instanceLink *link = ri->link;
    mstime_t now = mstime();

    // 如果還沒有最近一次重連的時間距離現在太短,小於1s,則直接返回
    if (now - ri->link->last_reconn_time < SENTINEL_PING_PERIOD) return;
    // 設置最近重連的時間
    ri->link->last_reconn_time = now;

    /* Commands connection. */
    // cc:命令連接
    if (link->cc == NULL) {
        // 綁定ri實例的連接地址並建立連接
        link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
        // 命令連接失敗,則事件通知,且斷開cc連接
        if (link->cc->err) {
            sentinelEvent(LL_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",
                link->cc->errstr);
            instanceLinkCloseConnection(link,link->cc);
        // 命令連接成功
        } else {
            // 重置cc連接的屬性
            link->pending_commands = 0;
            link->cc_conn_time = mstime();
            link->cc->data = link;
            // 將服務器的事件循環關聯到cc連接的上下文中
            redisAeAttach(server.el,link->cc);
            // 設置確立連接的回調函數
            redisAsyncSetConnectCallback(link->cc,
                    sentinelLinkEstablishedCallback);
            // 設置斷開連接的回調處理
            redisAsyncSetDisconnectCallback(link->cc,
                    sentinelDisconnectCallback);
            // 發送AUTH 命令認證
            sentinelSendAuthIfNeeded(ri,link->cc);
            // 發送連接名字
            sentinelSetClientName(ri,link->cc,"cmd");

            /* Send a PING ASAP when reconnecting. */
            // 立即向ri實例發送PING命令
            sentinelSendPing(ri);
        }
    }
    /* Pub / Sub */
    // pc:發佈訂閱連接
    // 只對主節點和從節點如果沒有設置pc連接則建立一個
    if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && link->pc == NULL) {
        // 綁定指定ri的連接地址並建立連接
        link->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
        // pc連接失敗,則事件通知,且斷開pc連接
        if (link->pc->err) {
            sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
                link->pc->errstr);
            instanceLinkCloseConnection(link,link->pc);
        // pc連接成功
        } else {
            int retval;

            link->pc_conn_time = mstime();
            link->pc->data = link;
            // 將服務器的事件循環關聯到pc連接的上下文中
            redisAeAttach(server.el,link->pc);
            // 設置確立連接的回調函數
            redisAsyncSetConnectCallback(link->pc,
                    sentinelLinkEstablishedCallback);
            // 設置斷開連接的回調處理
            redisAsyncSetDisconnectCallback(link->pc,
                    sentinelDisconnectCallback);
            //  發送AUTH 命令認證
            sentinelSendAuthIfNeeded(ri,link->pc);
            // 發送連接名字
            sentinelSetClientName(ri,link->pc,"pubsub");
            // 發送訂閱 __sentinel__:hello 頻道的命令,設置回調函數處理回覆
            // sentinelReceiveHelloMessages是處理Pub/Sub的頻道返回信息的回調函數,可以發現訂閱同一master的Sentinel節點
            retval = redisAsyncCommand(link->pc,
                sentinelReceiveHelloMessages, ri, "SUBSCRIBE %s",
                    SENTINEL_HELLO_CHANNEL);
            // 訂閱頻道出錯,關閉
            if (retval != C_OK) {
                // 關閉pc連接
                instanceLinkCloseConnection(link,link->pc);
                return;
            }
        }
    }
    // 如果已經建立了新的連接,則清除斷開連接的狀態。表示已經建立了連接
    if (link->cc && (ri->flags & SRI_SENTINEL || link->pc))
        link->disconnected = 0;
}

建立連接的函數redisAsyncConnectBind()Redis的官方C語言客戶端hiredis的異步連接函數,當連接成功時需要調用redisAeAttach()函數來將服務器的事件循環(ae)與連接的上下文相關聯起來(因爲hiredis提供了多種適配器,包括事件ae,libev,libevent,libuv),在關聯的時候,會設置了網絡連接的可寫可讀事件的處理程序。接下來還會設置該連接的確立時和斷開時的回調函數redisAsyncSetConnectCallback()redisAsyncSetDisconnectCallback(),爲什麼這麼做,就是因爲該連接是異步的。

瞭解了以上這些,繼續分析節點實例和當前哨兵的連接建立。從該函數中可以很明顯的看出來:

  • 如論是主節點、從節點還是哨兵節點,都會與當前哨兵建立命令連接(Commands connection)
  • 只有主節點或從節點纔會建立發佈訂閱連接(Pub / Sub connection)

當建立了命令連接(cc)之後立即執行了三個動作

執行函數 執行函數的目的 回複函數 回複函數操作
sentinelSendAuthIfNeeded() 發送 AUTH 命令進行認證 sentinelDiscardReplyCallback() 丟棄回覆,只是將已發送但未回覆的命令個數減1
sentinelSetClientName() 發送 CLIENT SETNAME命令設置連接的名字 sentinelDiscardReplyCallback() 丟棄回覆,只是將已發送但未回覆的命令個數減1
sentinelSendPing() 發送 PING 命令來判斷連接狀態 sentinelPingReplyCallback() 根據回覆的內容來更新一些連接交互時間,等。

當建立了發佈訂閱連接(pc)之後立即執行的動作:(前兩個動作與命令連接相同,只列出不相同的第三個)

執行函數 執行函數的目的 回複函數 回複函數操作
redisAsyncCommand() 發送 SUBSCRIBE 命令來判訂閱__sentinel__:hello 頻道的事件提醒 sentinelReceiveHelloMessages() 哨兵節點處理從實例(主節點或從節點)發送過來的hello信息,來獲取其他哨兵節點或主節點的從節點信息。

如果成功建立連接,之後會清除連接斷開的標誌,以表示連接已建立。

如果不是第一次執行,那麼會判斷連接是否建立,如果斷開,則重新給建立,如果沒有斷開,那麼什麼都不會做直接返回。

4.1.2 發送監控命令

執行完建立網絡連接的函數,接下來會執行sentinelSendPeriodicCommands()函數,該函數就是定期發送一些監控命令到主節點或從節點或哨兵節點,這些節點會將哨兵節點作爲客戶端來處理,我們接下來仔細分析。

void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
    mstime_t now = mstime();
    mstime_t info_period, ping_period;
    int retval;

    // 如果ri實例連接處於關閉狀態,直接返回
    if (ri->link->disconnected) return;

    // 對於不是發送關鍵命令的INFO,PING,PUBLISH,我們也有SENTINEL_MAX_PENDING_COMMANDS的限制。 我們不想使用大量的內存,只是因爲連接對象無法正常工作(請注意,無論如何,還有一個冗餘的保護措施,即如果檢測到長時間的超時條件,連接將被斷開連接並重新連接
    // 每個實例的已發送未回覆的命令個數不能超過100個,否則直接返回
    if (ri->link->pending_commands >=
        SENTINEL_MAX_PENDING_COMMANDS * ri->link->refcount) return;

    // 如果主節點處於O_DOWN狀態下,那麼Sentinel默認每秒發送INFO命令給它的從節點,而不是通常的SENTINEL_INFO_PERIOD(10s)週期。在這種狀態下,我們想更密切的監控從節點,萬一他們被其他的Sentinel晉升爲主節點
    // 如果從節點報告和主節點斷開連接,我們同樣也監控INFO命令的輸出更加頻繁,以便我們能有一個更新鮮的斷開連接的時間

    // 如果ri是從節點,且他的主節點處於故障狀態的狀態或者從節點和主節點斷開復制了
    if ((ri->flags & SRI_SLAVE) &&
        ((ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS)) ||
         (ri->master_link_down_time != 0)))
    {
        // 設置INFO命令的週期時間爲1s
        info_period = 1000;
    } else {
        // 否則就是默認的10s
        info_period = SENTINEL_INFO_PERIOD;
    }

    // 每次最後一次接收到的PONG比配置的 'down-after-milliseconds' 時間更長,但是如果 'down-after-milliseconds'大於1秒,則每秒鐘進行一次ping

    // 獲取ri設置的主觀下線的時間
    ping_period = ri->down_after_period;
    // 如果大於1秒,則設置爲1秒
    if (ping_period > SENTINEL_PING_PERIOD) ping_period = SENTINEL_PING_PERIOD;

    // 如果實例不是Sentinel節點且Sentinel節點從該數據節點(主節點或從節點)沒有收到過INFO回覆或者收到INFO回覆超時
    if ((ri->flags & SRI_SENTINEL) == 0 &&
        (ri->info_refresh == 0 ||
        (now - ri->info_refresh) > info_period))
    {
        // 發送INFO命令給主節點和從節點
        retval = redisAsyncCommand(ri->link->cc,
            sentinelInfoReplyCallback, ri, "INFO");
        // 已發送未回覆的命令個數加1
        if (retval == C_OK) ri->link->pending_commands++;

    // 如果發送和回覆PING命令超時
    } else if ((now - ri->link->last_pong_time) > ping_period &&
               (now - ri->link->last_ping_time) > ping_period/2) {
        // 發送一個PING命令給ri實例,並且更新act_ping_time
        sentinelSendPing(ri);

    // 發送頻道的定時命令超時
    } else if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
        // 發佈hello信息給ri實例
        sentinelSendHello(ri);
    }
}

從這個函數我們可以瞭解到一下信息:

  • 一個連接對發送命令的個數有限制。因爲連接是一個異步操作,發送了不一定會立即接收到,因此會爲了節約內存而有一個限制,已發送未回覆的命令個數不能超過100個,否則不做操作。
  • 當該哨兵節點正在監控從節點時,但是從節點從屬的主節點發送了故障,那麼會設置發送INFO命令的頻率爲1s,否則就是默認的10s發送一次INFO命令。
  • PING命令的頻率是1s發送一次。

接下來,就逐個分析所發送的監控命令。

  • 第一個是INFO命令

哨兵節點只將INFO命令發送給主節點或從節點。並且設置sentinelInfoReplyCallback()函數來處理INFO命令的回覆信息。

處理函數的代碼有300多行,這裏就不列出來了,可以上github查看 sentinel.c源碼詳細註釋

INFO名的回覆正確時,會調用sentinelRefreshInstanceInfo()函數來處理INFO命令的回覆。處理INFO命令的回覆有兩部分:

  • 獲取該連接的節點實例最基本的信息,如:run_idrole,如果是發送給主節點,會獲取到從節點信息;如果是發送給從節點,會獲取到其主節點的信息。總之會獲取當前整個集羣網絡的所有活躍的節點信息,並將其保存到當前哨兵的狀態中,而且會刷新配置文件。這就是爲什麼在配置文件中不需要配置從節點的信息,因爲通過這一操作會自動發現從節點。
  • 處理角色變化的情況。當接收到INFO命令的回覆,有可能發現當前哨兵連接的節點的角色狀態發生變化,因此要處理這些情況。
    • 連接的節點實例是主節點,但是INFO命令顯示連接的是從節點。
    • 什麼也不做。
    • 連接的節點實例是從節點,但是INFO命令顯示連接的是主節點。
    • 連接的從節點是被晉升的從節點,且他的主節點處於等待該從節點晉升的狀態,那麼會更新一些屬性。
    • 連接的從節點是被晉升的從節點,但是主節點在發生故障轉移的超時時間限制內又重新上線,因此要將該晉升的從節點重新降級爲普通的從節點,並從屬原來的主節點,通過發送slaveof命令。
    • 連接的節點實例是從節點,INFO命令顯示連接的也是主節點,但是發現該從節點從屬的主節點地址發生了變化。
    • 發送slaveof命令使其從屬新的主節點。
    • 連接的節點實例是從節點,INFO命令顯示連接的也是主節點,但是該從節點處於已經接受slaveof命令(SRI_RECONF_SENT)或者正在根據slaveof命令指定的主節點執行同步操作(SRI_RECONF_INPROG)的狀態。
    • 將他們的狀態設置爲下一步狀態,表示當前狀態的操作已經完成。

當這些處理只是當收到INFO命令的回覆時纔會進行處理。我們繼續分析下一個發送監控的命令。

  • 第二個是PING命令

這個發送的函數sentinelSendPing()函數和在第一次創建命令連接時執行的函數操作一樣。

int sentinelSendPing(sentinelRedisInstance *ri) {
    // 異步發送一個PING命令給實例ri
    int retval = redisAsyncCommand(ri->link->cc,
        sentinelPingReplyCallback, ri, "PING");
    // 發送成功
    if (retval == C_OK) {
        // 已發送未回覆的命令個數加1
        ri->link->pending_commands++;
        // 更新最近一次發送PING命令的時間
        ri->link->last_ping_time = mstime();
        // 更新最近一次發送PING命令,但沒有收到PONG命令的時間
        if (ri->link->act_ping_time == 0)
            ri->link->act_ping_time = ri->link->last_ping_time;
        return 1;
    } else {
        return 0;
    }
}

該函數,發送給實例一個PING並且更新所有連接的狀態。設置sentinelPingReplyCallback()來處理PING命令的回覆。

PING命令的回覆有以下兩種:

  • 狀態回覆或者錯誤回覆
    • PONGLOADINGMASTERDOWN這三個是可以接受的回覆,會更新最近的交互時間,用來判斷實例和哨兵之間的網絡可達。
  • 忙回覆
    • BUSY這個可能會是因爲執行腳本而表現爲下線狀態。所以會發送一個SCRIPT KILL命令來終止腳本的執行。

無論如何,只要接受到回覆,都會更新最近一次收到PING命令回覆的狀態,表示連接可達。

  • 第三個是PUBLISH命令

發送PUBLISH命令,可以叫發送hello信息。因爲這個操作像是和訂閱該主節點的其他哨兵節點打招呼。

函數sentinelSendHello()用來發送hello信息,該函數主要做了兩步操作:

  • 構建hello信息的內容。hello信息的格式如下:
    • sentinel_ip,sentinel_port,sentinel_runid,current_epoch,master_name,master_ip,master_port,master_config_epoch這些信息包含有:當前哨兵的信息和主節點信息。
  • 發送PUBLISH命令,將hello信息發佈到創建連接時建立的頻道。
    • 設置sentinelPublishReplyCallback()函數爲處理PUBLISH命令的回覆。該命令主要就是更新通過頻道進行通信的時間,以便保持發佈訂閱連接的可達。

通過發送PUBLISH命令給任意類型實例,最終都是將主節點信息和當前哨兵信息廣播給所有的訂閱指定頻道的哨兵節點,這樣就可以將監控相同主節點的哨兵保存在哨兵實例的sentinels字典中。

發送完這些命令,就會獲取所有節點的新的狀態。因此,要根據這些狀態要判斷是否出現網絡故障。

4.1.3 判斷節點的主觀下線狀態

當前哨兵節點發送完所有的監控命令,有可能發送成功且順利收到回覆,也有可能發送和回覆都沒有成功收到等等可能,因此要對當前節點實例(所有類型都要進行判斷)調用sentinelCheckSubjectivelyDown()函數進行主觀下線判斷。

void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;
    // 獲取ri實例回覆命令已經過去的時長
    if (ri->link->act_ping_time)
        // 獲取最近一次發送PING命令過去了多少時間
        elapsed = mstime() - ri->link->act_ping_time;
    // 如果實例的連接已經斷開
    else if (ri->link->disconnected)
        // 獲取最近一次回覆PING命令過去了多少時間
        elapsed = mstime() - ri->link->last_avail_time;

    // 如果連接處於低活躍度,那麼進行重新連接
    // cc命令連接超過了1.5s,並且之前發送過PING命令但是連接活躍度很低
    if (ri->link->cc &&
        (mstime() - ri->link->cc_conn_time) >
        SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
        ri->link->act_ping_time != 0 && /* Ther is a pending ping... */
        /* The pending ping is delayed, and we did not received
         * error replies as well. */
        (mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) &&
        (mstime() - ri->link->last_pong_time) > (ri->down_after_period/2))
    {   // 斷開ri實例的cc命令連接
        instanceLinkCloseConnection(ri->link,ri->link->cc);
    }

    // 檢查pc發佈訂閱的連接是否也處於低活躍狀態
    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))
    {   // 斷開ri實例的pc發佈訂閱連接
        instanceLinkCloseConnection(ri->link,ri->link->pc);
    }
    // 更新主觀下線標誌,條件如下:
    /*
        1. 沒有回覆命令
        2. Sentinel節點認爲ri是主節點,但是它報告它是從節點
    */
    // ri實例回覆命令已經過去的時長已經超過主觀下線的時限,並且ri實例是主節點,但是報告是從節點
    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)))
    {
        /* Is subjectively down */
        // 設置主觀下線的標識
        if ((ri->flags & SRI_S_DOWN) == 0) {
            // 發送"+sdown"的事件通知
            sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
            // 設置實例被判斷主觀下線的時間
            ri->s_down_since_time = mstime();
            ri->flags |= SRI_S_DOWN;
        }
    } else {
        /* Is subjectively up */
        // 如果設置了主觀下線的標識,則取消標識
        if (ri->flags & SRI_S_DOWN) {
            sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
            ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
        }
    }
}

該函數主要做了兩件事:

  • 根據命令連接和發佈訂閱連接的活躍度來判斷是否要執行斷開對應連接的操作。以便下次時鐘循環在重新連接,以保證可靠性。
  • 獲取回覆PING命令過去的時間,然後進行判斷是否已經下線。如果滿足主觀下線的條件,那麼會設置主觀下線的標識。主觀下線條件有兩個:
    • 回覆PING命令超時
    • 哨兵節點發現他的角色發生變化。認爲它是主節點但是報告顯示它是從節點。

當判斷完主觀下線厚,雖然對實例設置了主觀下線的標識,但是隻有該實例是主節點,纔會執行進一步的判斷。否則對於其他類型節點來說,他們的週期性操作已經執行完成。

4.1.4 判斷主節點的客觀下線狀態

客觀下線狀態的判斷只針對主節點而言。之前已經判斷過主觀下線,因此只有被當前哨兵節點判斷爲主觀下線的主節點纔會繼續執行客觀下線的判斷。

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    unsigned int quorum = 0, odown = 0;
    // 如果該master實例已經被當前Sentinel節點判斷爲主觀下線
    if (master->flags & SRI_S_DOWN) {
        /* Is down for enough sentinels? */
        // 當前Sentinel節點認爲下線投1票
        quorum = 1; /* the current sentinel. */
        /* Count all the other sentinels. */
        di = dictGetIterator(master->sentinels);
        // 遍歷監控該master實例的所有的Sentinel節點
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);
            // 如果Sentinel也認爲master實例主觀下線,那麼增加投票數
            if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
        dictReleaseIterator(di);
        // 如果超過master設置的客觀下線票數,則設置客觀下線標識
        if (quorum >= master->quorum) odown = 1;
    }

    /* Set the flag accordingly to the outcome. */
    // 如果被判斷爲客觀下線
    if (odown) {
        // master沒有客觀下線標識則要設置
        if ((master->flags & SRI_O_DOWN) == 0) {
            // 發送"+odown"事件通知
            sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
            // 設置master客觀下線標識
            master->flags |= SRI_O_DOWN;
            // 設置master被判斷客觀下線的時間
            master->o_down_since_time = mstime();
        }
    // master實例沒有客觀下線
    } else {
        // 取消master客觀下線標識
        if (master->flags & SRI_O_DOWN) {
            // 發送"-odown"事件通知
            sentinelEvent(LL_WARNING,"-odown",master,"%@");
            master->flags &= ~SRI_O_DOWN;
        }
    }
}

該函數做了兩個工作:

  • 遍歷監控該主節點的所有其他的哨兵節點,如果這些哨兵節點也認爲當前主節點下線(SRI_MASTER_DOWN),那麼投票數加1,當超過設置的投票數,標識客觀下線的標誌。
  • 如果客觀下線的標誌(odown)爲真,那麼打開主節點的客觀下線的表示,否則取消主節點客觀下線的標識。

這種方法存在一個缺陷,那麼就是客觀下線意味這有足夠多的Sentinel節點報告該主節點在一個時間範圍內不可達。但是信息可能被延遲,不能保證N個實例在同一時間都同意該實例進入下線狀態。

執行完的客觀下線判斷,如果發現主節點打開了客觀下線的狀態標識,那麼就進一步進行判斷,否則就執行跳過判斷。執行這進一步判斷的函數是:sentinelStartFailoverIfNeeded()。該函數用來判斷能不能進行故障轉移:

  • 主節點必須處於客觀下線狀態。如果沒有打開客觀下線的標識,就會直接返回0。
  • 沒有正在對主節點進行故障轉移。
  • 一段時間內沒有嘗試進行故障轉移,防止頻繁執行故障轉移。

如果以上條件都滿足,那麼會調用sentinelStartFailover()函數,將更新主節點的故障轉移狀態,會執行下面這句關鍵的代碼

master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
master->flags |= SRI_FAILOVER_IN_PROGRESS;

並且返回1,執行if條件中的代碼:

sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);

由於指定了一個SENTINEL_ASK_FORCED標識,因此會強制發送一個SENTINEL is-master-down-by-addr命令來真正判斷是否主節點下線,不會被時間條件所拒絕執行。

void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
    dictIterator *di;
    dictEntry *de;

    di = dictGetIterator(master->sentinels);
    // 遍歷監控master的所有的Sentinel節點
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        // 當前Sentinel實例最近一個回覆SENTINEL IS-MASTER-DOWN-BY-ADDR命令所過去的時間
        mstime_t elapsed = mstime() - ri->last_master_down_reply_time;
        char port[32];
        int retval;

        /* If the master state from other sentinel is too old, we clear it. */
        // 如果master狀態太舊沒有更新,則清除它保存的主節點狀態
        if (elapsed > SENTINEL_ASK_PERIOD*5) {
            ri->flags &= ~SRI_MASTER_DOWN;
            sdsfree(ri->leader);
            ri->leader = NULL;
        }
        // 滿足以下條件向其他Sentinel節點詢問主節點是否下線
        /*
            1. 當前Sentinel節點認爲它已經下線,並且處於故障轉移狀態
            2. 其他Sentinel與當前Sentinel保持連接狀態
            3. 在SENTINEL_ASK_PERIOD毫秒內沒有收到INFO回覆
        */
        // 主節點沒有處於客觀下線狀態,則跳過當前Sentinel節點
        if ((master->flags & SRI_S_DOWN) == 0) continue;
        // 如果當前Sentinel節點斷開連接,也跳過
        if (ri->link->disconnected) continue;
        // 最近回覆SENTINEL IS-MASTER-DOWN-BY-ADDR命令在SENTINEL_ASK_PERIODms時間內已經回覆過了,則跳過
        if (!(flags & SENTINEL_ASK_FORCED) &&
            mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
            continue;

        /* Ask */
        // 發送SENTINEL IS-MASTER-DOWN-BY-ADDR命令
        ll2string(port,sizeof(port),master->addr->port);
        // 異步發送命令
        retval = redisAsyncCommand(ri->link->cc,
                    sentinelReceiveIsMasterDownReply, ri,
                    "SENTINEL is-master-down-by-addr %s %s %llu %s",
                    master->addr->ip, port,
                    sentinel.current_epoch,
                    // 如果主節點處於故障轉移的狀態,那麼發送該Sentinel的ID,讓收到命令的Sentinel節點選舉自己爲領頭
                    // 否則發送"*"表示發送投票
                    (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
                    sentinel.myid : "*");
        // 已發送未回覆的命令個數加1
        if (retval == C_OK) ri->link->pending_commands++;
    }
    dictReleaseIterator(di);
}

該函數遍歷所有監控該主節點的哨兵節點,跳過三種不符合下線的條件的哨兵節點,然後就發送SENTINEL is-master-down-by-addr命令,之前在if判斷時,就設置了主節點的故障轉移狀態爲SENTINEL_FAILOVER_STATE_WAIT_START,因此發送的SENTINEL命令中會加上自己的runid,用來請求所有收到命令的哨兵節點將自己選舉爲執行故障轉移的領頭。

由於發送的是異步命令,所以會設置回調函數sentinelReceiveIsMasterDownReply()來處理命令回覆。

如果收到的回覆的第一個整型值爲1則打開該哨兵節點的主節點下線標識(SRI_MASTER_DOWN)。這裏就是前面說的那麼缺陷,因爲是回調函數,該主節點下線標識(SRI_MASTER_DOWN)不會立即打開,可能存在延遲。

至此,主節點的客觀下線判斷完畢,如果確認了客觀下線,那麼就會執行故障轉移操作。

4.1.5 對主節點執行故障轉移

故障轉移操作的過程非常清晰,正如函數sentinelFailoverStateMachine()所寫的那樣。sentinel.c文件詳細註釋:Redis Sentinel詳細註釋

void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
    // 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;
        // 發送slaveof no one命令,使從節點變爲主節點
        case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
            sentinelFailoverSendSlaveOfNoOne(ri);
            break;
        // 等待被選擇的從節點晉升爲主節點,如果超時則重新選擇晉升的從節點
        case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
            sentinelFailoverWaitPromotion(ri);
            break;
        // 給所有的從節點發送slaveof命令,同步新的主節點
        case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
            sentinelFailoverReconfNextSlave(ri);
            break;
    }
}

在之前判斷主節點客觀下線的時候,會將故障轉移的狀態打開,就是下面這樣:

master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
master->flags |= SRI_FAILOVER_IN_PROGRESS;

所以,主節點如果沒有被判斷爲主觀下線,就不會判斷爲客觀下線,因此也就不會執行故障轉移操作。

之前設置的這些狀態正好可以執行故障轉移操作。這個過程分爲五部:

  • 故障轉移開始。
    • SENTINEL_FAILOVER_STATE_WAIT_START
  • 選擇一個要晉升的從節點。
    • SENTINEL_FAILOVER_STATE_SELECT_SLAVE
  • 發送slaveof no one命令,使從節點變爲主節點。
    • SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
  • 等待被選擇的從節點晉升爲主節點,如果超時則重新選擇晉升的從節點。
    • SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
  • 給所有的從節點發送slaveof命令,同步新的主節點。
    • SENTINEL_FAILOVER_STATE_RECONF_SLAVES

這五部是連續的,成功執行完一步操作,都會將狀態設置爲下一步狀態。而且這五部是分開執行的,意思是,每一次時間事件處理只處理一步,倘若已經執行了幾部故障轉移操作,但是在接下來的故障檢測時,發現主節點是可達的,因此在之前的下線判斷中都會將下線標識取消,會中斷執行故障轉移。

以上現象在標題4.1.2 發送監控命令中有提到,處理接收到的INFO命令時的一個場景:連接的從節點是被晉升的從節點,但是主節點在發生故障轉移的超時時間限制內又重新上線,因此要將該晉升的從節點重新降級爲普通的從節點,並從屬原來的主節點。

4.1.5.1 故障轉移開始

sentinelFailoverWaitStart()函數會處理故障轉移的開始,主要是選舉出一個領頭哨兵節點,用來領導故障轉移,並且更新故障轉移的狀態。

sentinel 自動故障遷移使用Raft算法來選舉領頭(leader) Sentinel ,從而確保在一個給定的紀元(epoch)裏,只有一個領頭產生。

動畫演示Raft算法

有關Raft算法的通俗易懂的解釋

這表示在同一個紀元中,不會有兩個 Sentinel 同時被選中爲領頭,並且各個 Sentinel 在同一個紀元中只會對一個領頭進行投票。

更高的配置紀元總是優於較低的紀元,因此每個 Sentinel 都會主動使用更新的紀元來代替自己的配置。

簡單來說,我們可以將 Sentinel 配置看作是一個帶有版本號的狀態。一個狀態會以最後寫入者勝出(last-write-wins)的方式(也即是,最新的配置總是勝出)傳播至所有其他 Sentinel 。

4.1.5.2 選擇一個要晉升的從節點

sentinelFailoverSelectSlave()函數用來選擇一個要晉升的從節點。該函數調用sentinelSelectSlave()函數來選則一個晉升的從節點。

sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master) {
    // 從節點數組
    sentinelRedisInstance **instance =
        zmalloc(sizeof(instance[0])*dictSize(master->slaves));
    sentinelRedisInstance *selected = NULL;
    int instances = 0;
    dictIterator *di;
    dictEntry *de;
    mstime_t max_master_down_time = 0;
    // 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;

    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;
        // 跳過回覆PING命令過於久遠的從節點
        if (mstime() - slave->link->last_avail_time > SENTINEL_PING_PERIOD*5) continue;
        // 跳過優先級爲0的從節點
        if (slave->slave_priority == 0) continue;

        // 如果主節點處於主觀下線狀態,Sentinel每秒發送INFO命令給從節點,否則以默認的頻率發送。
        // 爲了檢查命令是否合法,因此計算一個延遲值
        if (master->flags & SRI_S_DOWN)
            info_validity_time = SENTINEL_PING_PERIOD*5;
        else
            info_validity_time = SENTINEL_INFO_PERIOD*3;
        // 如果從節點接受到INFO命令的回覆已經過期,跳過該從節點
        if (mstime() - slave->info_refresh > info_validity_time) continue;
        // 跳過下線時間過長的從節點
        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;
}

總結一下就是這些條件:

1. 不選有以下狀態的從節點: S_DOWN, O_DOWN, DISCONNECTED.
2. 最近一次回覆PING命令超過5s的從節點
3. 最近一次獲取INFO命令回覆的時間不超過`info_refresh`的三倍時間長度
4. 主從節點之間斷開操作的時間不超過:從當前的Sentinel節點來看,主節點處於下線狀態,從節點和主節點斷開連接的時間不能超過down-after-period的10倍,這看起來非常魔幻(black magic),但是實際上,當主節點不可達時,主從連接會斷開,但是必然不超過一定時間。意思是,主從斷開,一定是主節點造成的,而不是從節點。無論如何,我們將根據複製偏移量選擇最佳的從節點。
5. 從節點的優先級不能爲0,優先級爲0的從節點被拋棄。

如果以上條件都滿足,那麼按照一下順序排序,compareSlavesForPromotion()函數指定排序方法:

  • 最低的優先級的優先。
  • 複製偏移量較大的優先。
  • 運行runid字典序小的優先。
  • 如果runid相同,那麼選擇執行命令更多的從節點。

因此,當選擇出一個適合晉升的從節點後,sentinelFailoverSelectSlave()會打開該從節點的SRI_PROMOTED晉升標識,並且保存起來,最後更新故障轉移到下一步狀態。

4.1.5.3 使從節點變爲主節點

函數sentinelFailoverSendSlaveOfNoOne()會調用sentinelSendSlaveOf()函數發送一個slaveof no one命令,使從晉升的節點和原來的主節點斷絕主從關係,成爲新的主節點。

void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) {
    int retval;

    // 如果要晉升的從節點處於斷開連接的狀態,那麼不能發送命令。在當前狀態,在規定的故障轉移超時時間內可以重試。
    if (ri->promoted_slave->link->disconnected) {
        // 如果超出 配置的故障轉移超時時間,那麼中斷本次故障轉移後返回
        if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
            sentinelEvent(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
            sentinelAbortFailover(ri);
        }
        return;
    }
    retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0);
    if (retval != C_OK) return;
    // 命令發送成功,發送事件通知
    sentinelEvent(LL_NOTICE, "+failover-state-wait-promotion",
        ri->promoted_slave,"%@");
    // 設置故障轉移狀態爲等待從節點晉升爲主節點
    ri->failover_state = SENTINEL_FAILOVER_STATE_WAIT_PROMOTION;
    // 更新故障轉移操作狀態改變時間
    ri->failover_state_change_time = mstime();
}

發送成功後,會更新故障轉移狀態到下一步狀態。

4.1.5.4 等待從節點晉升爲主節點

調用sentinelFailoverWaitPromotion()來等待從節點晉升爲主節點,但是該函數只是處理故障轉移操作超時的情況。

void sentinelFailoverWaitPromotion(sentinelRedisInstance *ri) {
    // 所以,在這裏只是處理故障轉移超時的情況
    if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
        // 如果超出配置的故障轉移超時時間,那麼中斷本次故障轉移後返回
        sentinelEvent(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
        sentinelAbortFailover(ri);
    }
}

這個函數並沒有更改故障轉移操作的狀態,因爲,當從節點晉升爲主節點時,故障轉移狀態的改變在處理INFO命令的回覆時發生。

標題4.1.2中的場景:連接的節點實例是從節點,但是INFO命令顯示連接的是主節點。

所以,當下個時間事件發生(在故障轉移設置的超時時間內),就會處理下一個故障轉移的狀態。如果等待從節點晉升爲主節點超時,那麼會調用sentinelAbortFailover()函數中止當前的故障轉移操作,清空所有故障轉移的狀態,下個時間事件發生時重新執行。

4.1.5.5 從節點同步新的主節點

調用sentinelFailoverReconfNextSlave()函數,給所有沒有同步新主節點的從節點發送SLAVE OF <new master address>命令。

void sentinelFailoverReconfNextSlave(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    int in_progress = 0;

    di = dictGetIterator(master->slaves);
    // 遍歷所有的從節點
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *slave = dictGetVal(de);
        // 計算處於已經發送同步命令或者已經正在同步的從節點
        if (slave->flags & (SRI_RECONF_SENT|SRI_RECONF_INPROG))
            in_progress++;
    }
    dictReleaseIterator(di);

    di = dictGetIterator(master->slaves);
    // 如果已經發送同步命令或者已經正在同步的從節點個數小於設置的同步個數限制,那麼遍歷所有的從節點
    while(in_progress < master->parallel_syncs &&
          (de = dictNext(di)) != NULL)
    {
        sentinelRedisInstance *slave = dictGetVal(de);
        int retval;

        // 跳過被晉升的從節點和已經完成同步的從節點
        if (slave->flags & (SRI_PROMOTED|SRI_RECONF_DONE)) continue;

        // 如果從節點設置了發送slaveof命令,但是故障轉移更新到下一個狀態超時
        if ((slave->flags & SRI_RECONF_SENT) &&
            (mstime() - slave->slave_reconf_sent_time) >
            SENTINEL_SLAVE_RECONF_TIMEOUT)
        {
            sentinelEvent(LL_NOTICE,"-slave-reconf-sent-timeout",slave,"%@");
            // 清除已發送slaveof命令的標識
            slave->flags &= ~SRI_RECONF_SENT;
            // 設置爲完成同步的標識,隨後重新發送SLAVEOF命令,進行同步
            slave->flags |= SRI_RECONF_DONE;
        }

        // 跳過已經發送了命令或者已經正在同步的從節點
        if (slave->flags & (SRI_RECONF_SENT|SRI_RECONF_INPROG)) continue;
        // 跳過連接斷開的從節點
        if (slave->link->disconnected) continue;

        /* Send SLAVEOF <new master>. */
        // 發送 SLAVEOF <new master> 命令給從節點,包括剛纔超時的從節點
        retval = sentinelSendSlaveOf(slave,
                master->promoted_slave->addr->ip,
                master->promoted_slave->addr->port);
        // 如果發送成功
        if (retval == C_OK) {
            // 設置已經發送了SLAVEOF命令標識
            slave->flags |= SRI_RECONF_SENT;
            // 設置發送slaveof命令的時間
            slave->slave_reconf_sent_time = mstime();
            sentinelEvent(LL_NOTICE,"+slave-reconf-sent",slave,"%@");
            in_progress++;
        }
    }
    dictReleaseIterator(di);

    // 判斷故障轉移是否結束
    sentinelFailoverDetectEnd(master);
}

主要是給沒有被髮送同步新主節點命令的從節點雖然發送但是同步超時的從節點重新發送SLAVEOF <new master>命令。

函數最後調用了sentinelFailoverDetectEnd()函數來判斷故障轉移是否結束,但是結束的情況有兩種:

  • 故障轉移超時被動結束
  • 從節點已經完成同步新晉升的主節點結束
void sentinelFailoverDetectEnd(sentinelRedisInstance *master) {
    int not_reconfigured = 0, timeout = 0;
    dictIterator *di;
    dictEntry *de;
    // 自從上次更新故障轉移狀態的時間差
    mstime_t elapsed = mstime() - master->failover_state_change_time;

    /* We can't consider failover finished if the promoted slave is
     * not reachable. */
    // 如果被晉升的從節點不可達,直接返回
    if (master->promoted_slave == NULL ||
        master->promoted_slave->flags & SRI_S_DOWN) return;

    /* The failover terminates once all the reachable slaves are properly
     * configured. */
    // 遍歷所有的從節點,找出還沒有完成同步從節點
    di = dictGetIterator(master->slaves);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *slave = dictGetVal(de);
        // 如果是被晉升爲主節點的從節點或者是完成同步的從節點,則跳過
        if (slave->flags & (SRI_PROMOTED|SRI_RECONF_DONE)) continue;
        // 如果從節點處於客觀下線,則跳過
        if (slave->flags & SRI_S_DOWN) continue;
        // 沒有完成同步的節點數加1
        not_reconfigured++;
    }
    dictReleaseIterator(di);

    // 強制結束故障轉移超時的節點
    if (elapsed > master->failover_timeout) {
        // 忽略未完成同步的從節點
        not_reconfigured = 0;
        // 設置超時標識
        timeout = 1;
        sentinelEvent(LL_WARNING,"+failover-end-for-timeout",master,"%@");
    }

    // 如果所有的從節點完成了同步,那麼表示故障轉移結束
    if (not_reconfigured == 0) {
        sentinelEvent(LL_WARNING,"+failover-end",master,"%@");
        // 監控晉升的主節點,更新配置
        master->failover_state = SENTINEL_FAILOVER_STATE_UPDATE_CONFIG;
        // 更新故障轉移操作狀態改變時間
        master->failover_state_change_time = mstime();
    }

    // 如果是因爲超時導致故障轉移結束
    if (timeout) {
        dictIterator *di;
        dictEntry *de;

        di = dictGetIterator(master->slaves);
        // 遍歷所有的從節點
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *slave = dictGetVal(de);
            int retval;
            // 跳過完成同步和發送同步slaveof命令的從節點
            if (slave->flags & (SRI_RECONF_DONE|SRI_RECONF_SENT)) continue;
            // 跳過連接斷開的從節點
            if (slave->link->disconnected) continue;
            // 給沒有被髮送同步命令的從節點發送同步新晉升主節點的slaveof IP port 命令
            retval = sentinelSendSlaveOf(slave,
                    master->promoted_slave->addr->ip,
                    master->promoted_slave->addr->port);
            // 如果發送成功,將這些從節點設置爲已經發送slaveof命令的標識
            if (retval == C_OK) {
                sentinelEvent(LL_NOTICE,"+slave-reconf-sent-be",slave,"%@");
                slave->flags |= SRI_RECONF_SENT;
            }
        }
        dictReleaseIterator(di);
    }
}

該函數先會尋找沒有完成同步的從節點,如果存在,則會強制將主節點的故障狀態更新,如下:

master->failover_state = SENTINEL_FAILOVER_STATE_UPDATE_CONFIG;

這麼做是爲了讓主進程繼續向下執行,不要總是在此等待故障狀態的變化。

雖然強制將主節點的故障狀態更新,但是還是要將沒有完成同步的從節點發送slaveof IP port讓他們重新同步。

執行完這五步故障轉移操作後,回到sentinelHandleRedisInstance()函數,該函數就剩最後一步操作了。

4.1.6 更新主節點的狀態

執行該函數,嘗試發送SENTINEL IS-MASTER-DOWN-BY-ADDR給所有的哨兵節點獲取回覆,更新一些主節點狀態。

sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);

上次在標題4.1.4時也執行過該函數,不過當時是指定強制發送命令的標識SENTINEL_ASK_FORCED

這次則是有時間限制的發送SENTINEL命令,來更新其他哨兵節點對主節點是否下線的判斷。有可能發生主節點在故障轉移時重新上線的情況。

標題 4.1.5時討論過該情況。

當執行完這一步,sentinelHandleRedisInstance()的所有操作全部剖析完成。


以上就是標題4.1週期性操作的全部內容,還記得sentinelHandleDictOfRedisInstances函數遞歸的對當前哨兵所監控的所有主節點sentinel.masters,和所有主節點的所有從節點ri->slaves,和所有監控該主節點的其他所有哨兵節點ri->sentinels執行週期性操作。因此,執行完所有類型的節點的週期性任務之後,會接下來處理主從切換的情況。

4.2 處理主從切換

sentinel.c文件詳細註釋:Redis Sentinel詳細註釋

當執行完所有類型的節點的週期性任務之後,會執行sentinelHandleDictOfRedisInstances()下面的代碼:

    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
            // 遞歸對所有節點執行週期性操作
            ......
            // 如果ri實例處於完成故障轉移操作的狀態,所有從節點已經完成對新主節點的同步
            if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
                // 設置主從轉換的標識
                switch_to_promoted = ri;
            }

    }
    // 如果主從節點發生了轉換
    if (switch_to_promoted)
        // 將原來的主節點從主節點表中刪除,並用晉升的主節點替代
        // 意味着已經用新晉升的主節點代替舊的主節點,包括所有從節點和舊的主節點從屬當前新的主節點
        sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
    dictReleaseIterator(di);

還記得當前強制將主節點的故障狀態更新的狀態嗎?對,就是SENTINEL_FAILOVER_STATE_UPDATE_CONFIG狀態。這個狀態表示已經完成在故障轉移狀態下,所有從節點對新主節點的同步操作。因此需要調用sentinelFailoverSwitchToPromotedSlave()函數特殊處理髮送主從切換的情況。

該函數會發送事件通知然後調用sentinelResetMasterAndChangeAddress()來用新晉升的主節點代替舊的主節點,包括所有從節點和舊的主節點從屬當前新的主節點。

int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, int port) {
    sentinelAddr *oldaddr, *newaddr;
    sentinelAddr **slaves = NULL;
    int numslaves = 0, j;
    dictIterator *di;
    dictEntry *de;

    // 創建ip:port地址字符串
    newaddr = createSentinelAddr(ip,port);
    if (newaddr == NULL) return C_ERR;

    // 創建一個從節點表,將重置後的主節點添加到該表中
    // 不包含有我們要轉換地址的那一個從節點
    di = dictGetIterator(master->slaves);
    // 遍歷所有的從節點
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *slave = dictGetVal(de);
        // 如果當前從節點的地址和指定的地址相同,說明該從節點是要晉升爲主節點的,因此跳過該從節點
        if (sentinelAddrIsEqual(slave->addr,newaddr)) continue;
        // 否則將該從節點加入到一個數組中
        slaves = zrealloc(slaves,sizeof(sentinelAddr*)*(numslaves+1));
        slaves[numslaves++] = createSentinelAddr(slave->addr->ip,
                                                 slave->addr->port);
    }
    dictReleaseIterator(di);

    // 如果指定的地址和主節點地址不相同,說明,該主節點是要被替換的,那麼將該主節點地址加入到從節點數組中
    if (!sentinelAddrIsEqual(newaddr,master->addr)) {
        slaves = zrealloc(slaves,sizeof(sentinelAddr*)*(numslaves+1));
        slaves[numslaves++] = createSentinelAddr(master->addr->ip,
                                                 master->addr->port);
    }

    // 重置主節點,但不刪除所有監控自己的Sentinel節點
    sentinelResetMaster(master,SENTINEL_RESET_NO_SENTINELS);
    // 備份舊地址
    oldaddr = master->addr;
    // 設置新地址
    master->addr = newaddr;
    // 下線時間清零
    master->o_down_since_time = 0;
    master->s_down_since_time = 0;

    /* Add slaves back. */
    // 爲新的主節點加入從節點
    for (j = 0; j < numslaves; j++) {
        sentinelRedisInstance *slave;
        // 遍歷所有的從節點表,創建從節點實例,並將該實例從屬到當前新的主節點中
        slave = createSentinelRedisInstance(NULL,SRI_SLAVE,slaves[j]->ip,
                    slaves[j]->port, master->quorum, master);
        // 釋放原有的表中的從節點
        releaseSentinelAddr(slaves[j]);
        // 事件通知
        if (slave) sentinelEvent(LL_NOTICE,"+slave",slave,"%@");
    }
    // 釋放從節點表
    zfree(slaves);

    // 將原主節點地址釋放
    releaseSentinelAddr(oldaddr);
    // 刷新配置文件
    sentinelFlushConfig();
    return C_OK;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章