【Redis 源碼】集羣之哨兵 sentinel 故障轉移

前言:

各位看官大家好,這個主題內容比較長然後接着上一章就拆成了兩個部分。那麼我們接着上一章內容開始說。上一章中我們說到哨兵定時器 sentinelTimer 它們作用。sentinelTimer 方法中執行哨兵模式中的任務。包括執行定期操作比如 PING、分析主服務和從服務的 INFO 命令、故障轉移等等。那麼這一章我們就先從 sentinelTimer 開始說起。

(一) 基本結構

1.1 sentinelTimer 定時程序


圖中爲 sentinelTimer 調用鏈路,須線部分爲調用 aeCreateTimeEvent 註冊 serverCron 事件。
sentinel.c 中 sentinelTimer 方法:

void sentinelTimer(void) {
    //檢測是否需要開啓sentinel TILT模式
    sentinelCheckTiltCondition();
    //對哈希表中的每個服務器實例執行調度任務
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    //執行腳本命令,
    sentinelRunPendingScripts();
    //清理已經執行完腳本的進程,
    sentinelCollectTerminatedScripts();
    //kill執行時間超時的腳本
    sentinelKillTimedoutScripts();
    /*
    * 爲了防止多個哨兵同時選舉時故意錯開定時程序執行的時間。
    */
    server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}

1.2 哨兵結構介紹

基本數據結構

struct sentinelState {
    char myid[CONFIG_RUN_ID_SIZE+1]; /* 哨兵ID */
    uint64_t current_epoch;         /* Current epoch. */
    dict *masters;                  /* 存儲哨兵監聽服務器的信息 對應一個
                                    sentinelRedisInstance 結構體指針 */
    int tilt;                       /* 判斷 TILT 模式 */
    int running_scripts;            /* 當前正在執行的腳本數。*/
    mstime_t tilt_start_time;       /*  TITL 開始時間. */
    mstime_t previous_time;         /* 上次處理程序運行時間. */
    list *scripts_queue;           /* 要執行的用戶腳本隊列. */
    char *announce_ip;             /* IP 地址(gossip協議握手地址) */
    int announce_port;              /* 端口 (gossip協議端口) */
    unsigned long simfailure_flags; /* 故障模擬狀態. */
} sentinel;

每一個哨兵都有一個 sentinel 結構,裏面維護着多個主機連接。每個主機連接信息都維護着一個 sentinelRedisInstance,通過這個結構維護着所有主機連接的關係。

sentinelRedisInstance結構信息:
typedef struct sentinelRedisInstance {
    int flags;      /* 記錄哨兵類型以及實力當前狀態 */
    char *name;     /* 哨兵名稱格式爲ip:port ,例如"127.0.0.1:26379" */
    char *runid;    /* 哨兵運行ID.*/
    uint64_t config_epoch;  /* 配置紀元,用於故障轉移. */
    sentinelAddr *addr; /* 實例地址. */
     
     //...省略
     
    mstime_t s_down_since_time; /* 主觀下線標記時間. */
    mstime_t o_down_since_time; /* 客觀下線標記時間. */
    mstime_t down_after_period; /* 實例無響應多少毫秒之後纔會被判斷爲主觀下線(subjectively down ),SENTINEL down-after-milliseconds 選項設定的值 */
  
    //。。。省略
    /* Master specific. */
    dict *sentinels;    /* 其他sentinels. */
    dict *slaves;       /* 這個master的slave */
    unsigned int quorum;/* 判斷這個實例客觀下線(objectively down )所需的支持投票數量,SENTINEL monitor <master-name> <IP> <port> <quorum> 選項中的quorum 參數 */
    int parallel_syncs; /* 在執行故障轉移操作時,可以同時對新的主服務器進行同步的從服務器數量,SENTINEL parallel-syncs <master-name> <number> 選項的值. */
    char *auth_pass;    /* Password to use for AUTH against master & slaves. */

    //。。。省略
    mstime_t failover_start_time;   /* 上次故障轉移嘗試開始時間. */
    mstime_t failover_timeout;      /* 刷新故障遷移狀態的最大時間限制. SENTINEL failover-timeout <master-name> <ms> 選項的值*/
    
    //。。。省略
} sentinelRedisInstance;

1.3 sentinel 建立網絡連接

創建與被監聽的 master 網絡連接後,sentinel 會成功 master 的客戶端,它會向 master 發送命令。
並從 master 的響應中獲取 master 的信息。對於每個被監聽者的 master,sentinel 會向創建兩個異步的網絡連接。
該連接通過 sentinelReconnectInstance 函數創建,一個鏈接爲 commands 鏈接。另外一個鏈接爲 Pub / Sub 連接。訂閱發佈會創建一個__sentinel__:hello 的通道。

1.4 sentinel 命令集

# 重置名字匹配正則表達式的所有master狀態信息,清除之前存儲的狀態信息和slaves信息。PS:節點只要加入過sentinel,信息就會保存而不會自動清除  
sentinel reset <pattern>

# 用於改變關於master的配置,例如 sentinel set mymaster down-after-milliseconds 1000 ,此命令修改了當節點第一次失去連接到判定其下線所經過的時間
sentinel set <name> <option> <value>

# 告訴sentinel去監聽新的master 
sentinel monitor <name> <ip> <port> <quorum>  

# 命令sentinel放棄對某個master的監聽
sentinel remove <name>  

# 這個參數設置集羣從判斷節點掛掉,到執行故障轉移操作(即重新選舉master節點)的時間
sentinel failover-timeout mymaster 10000

# 獲取哨兵監視某個sentinel的信息
sentinel sentinels <master-name>

# 獲取sentinel監視的某個master的slaves信息
sentinel slaves <master-name>

# 獲取sentinel 監視的某個 master信息
sentinel master <name>

# 獲取sentinel監視所有的master信息
sentinel masters

# 詢問該sentinel,該 ip,port的master是否爲down狀態,
# 如果該sentinel爲tilt模式,會不理會這個詢問,不去判斷
# 該master是否爲主觀下線狀態,直接回復正常狀態。
sentinel is-master-down-by-addr <ip> <port> <current-epoch> <runid>

# 根據master名字獲取到master的ip和port
sentinel get-master-addr-by-name <master-name>

# 將sentinel 狀態信息寫入到配置文件當中
setinel flushconfig

# 檢查可投票同意master on failure的sentinel+1的個數以及相關狀態
# (可用的投票個數是否大於master 的quorum,需要quorum個同意master on failure)
setinel ckquorum <name>

(二) 發現故障

2.1 如何確定故障

提及到確認故障,哨兵中確認故障有兩種形式分爲對應兩種狀態 SRI_S_DOWN(主觀下線)和 SRI_O_DOWN(客觀下線)。

1) 主觀下線

主觀下線會涉及到一個方法 sentinelCheckSubjectivelyDown,圖中會主觀下線鏈路

void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;

    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;

    /* 檢測command 連接是否被關閉 */
    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))
    {
        instanceLinkCloseConnection(ri->link,ri->link->cc);
    }

    /* 檢測pubsub連接是否需要被關閉 
     */
    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))
    {
        instanceLinkCloseConnection(ri->link,ri->link->pc);
    }

    /* 更新SRI_S_DOWN狀態
     */
    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) {
            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);
        }
    }
}
  1. 檢測 command 連接是否被關閉;
    2. 檢測 pubsub 連接是否需要被關閉 ;
    3. 更新 SRI_S_DOWN 狀態狀態,更新狀態有如下兩個規則:
    3.1 超過 ri->down_after_period,代表超過響應時間,及 ping 無響應請求。該時間默認走的時 SENTINEL_DEFAULT_DOWN_AFTER 宏爲 30s。
    3.2 SLAVE 上報連續時間間隔要大於 ri->down_after_period+SENTINEL_INFO_PERIOD_2,及(30s + 10s_2),如果超過這個時間代表 slave 長時間連續不到 master,所以視爲主觀下線。

2) 客觀下線

說到客觀下線是,我們要思考一個問題。當一臺 master 服務已經掉線,並且已經維護自己的狀態爲 SRI_S_DOWN。由於在哨兵集羣中,ri->down_after_period 值可能不一樣。判斷 master 下線的時間間隔可能不一樣。所以必須去詢問 sentinel 節點這臺 master 服務是否下線。

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    //。。。省略
    /* Only masters */
    if (ri->flags & SRI_MASTER) {
        sentinelCheckObjectivelyDown(ri);
        if (sentinelStartFailoverIfNeeded(ri))
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        sentinelFailoverStateMachine(ri);
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);  
    }
}

在上面代碼中可以看到客觀下線只能 master 中使用。然後看一下 sentinelAskMasterStateToOtherSentinels 方法,該方法檢測 master 主觀下線後去詢問其他 sentinel。

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

    di = dictGetIterator(master->sentinels);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        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. */
        if (elapsed > SENTINEL_ASK_PERIOD*5) {
            ri->flags &= ~SRI_MASTER_DOWN;
            sdsfree(ri->leader);
            ri->leader = NULL;
        }

        /* 滿足下列情況纔可以詢問其他哨兵:
         *
         * 1) 主觀下線是否在進行
         * 2) Sentinel是否連接
         * 3) 我們沒有在哨兵詢問期內收到信息,1秒內. */
        if ((master->flags & SRI_S_DOWN) == 0) continue;
        if (ri->link->disconnected) continue;
        if (!(flags & SENTINEL_ASK_FORCED) &&
            mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
            continue;

        /* Ask */
        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,
                    (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
                    sentinel.myid : "*");
        if (retval == C_OK) ri->link->pending_commands++;
    }
    dictReleaseIterator(di);
}

通過遍歷維護的 master->sentinels 結構向其他 sentinel 節點發送命令:SENTINEL is-master-down-by-addr
命令格式如下:
SENTINEL is-master-down-by-addr <current_epoch> <leader_id>

命令詢問其他 sentinel 是否同意主服務器已下線。

接受 SENTINEL is-master-down-by-addr 命令返回狀態:
<down_state> <leader_runid> <leader_epoch>

down_state:爲 1 代表主服務器已下線,0 表示主服務器未下線。
leader_runid:領頭 sentinal id。
leader_epoch:領頭 sentinel 當前投票紀元。

(三) 故障轉移

3.1 故障狀態

當某個主節點進行故障轉移時,該主節點的的故障轉移狀態,master->failover_state, 依次會經歷 6 個狀態:
狀態宏:

SENTINEL_FAILOVER_STATE_NONE 0                 /*沒有故障轉移在進行*/
//以下爲經歷的6個狀態
SENTINEL_FAILOVER_STATE_WAIT_START 1           /* sentinel接手故障轉移*/
SENTINEL_FAILOVER_STATE_SELECT_SLAVE 2         /* 選擇slave成爲master*/
SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE 3   /* 發送slaveof no one給新master */
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 4       /* 等待新master升級完成,超時終止故障轉移*/
SENTINEL_FAILOVER_STATE_RECONF_SLAVES 5        /* 新master升級完成後,讓slaves複製新master */
SENTINEL_FAILOVER_STATE_UPDATE_CONFIG 6        /* 監視新master */

3.2 狀態機

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);         //sentinel接手故障轉移
            break;
        case SENTINEL_FAILOVER_STATE_SELECT_SLAVE:
            sentinelFailoverSelectSlave(ri);      //選擇slave成爲master
            break;
        case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
            sentinelFailoverSendSlaveOfNoOne(ri); //發送slaveof no one給新master
            break;
        case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
            sentinelFailoverWaitPromotion(ri);    //等待新master升級完成,超時終止故障轉移
            break;
        case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
            sentinelFailoverReconfNextSlave(ri); //新master升級完成後,讓slaves複製新master
            break;
    }
}

狀態機變化過程:

總結

1)一個哨兵結構中可以維護多個主機,包括 master,slave,sentinel。
2)確定故障分爲:主觀下線和客觀下線兩種。
3)主觀下線:爲一段時間內 ping 返回無效,探測所有節點都是一致的,則爲主觀下線。主觀下線的時間是可以配置的,以 master 配置維度爲準。
4)客觀下線:客觀下線只針對於 master 節點,且需要 master 爲主觀下線,並通過其他 sentinel 節點發送 SENTINEL is-master-down-by-addr 詢問其他節點 master 下線問題,達成共識的一個狀態。
5) 哨兵會創建兩個鏈接一個 commands 鏈接,一個訂閱發佈鏈接。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Dr87prK8LmU85dZANir6zw