Redis 哨兵模式(Sentinel)

上一篇咱們介紹了 redis 主從節點之間的數據同步複製技術,經過一次全量複製和不間斷的命令傳播,能夠達到主從節點數據同步備份的效果,一旦主節點宕機,咱們能夠選擇一個工做正常的 slave 成爲新的主節點,並讓其餘 slave 去同步它。java

這是處理 redis 故障轉移的一個方式,但卻不具有生產實用性,由於畢竟是手動處理故障,而 redis 發生故障時間節點不可預知,咱們須要一個自動監控組件幫咱們自動處理故障轉移。git

Redis 哨兵模式(Sentinel)就是一個自動地監控處理 redis 間故障節點轉移工做的一個「東西」,準確來講,Sentinel 實際上是一個 redis 服務端程序,只不過運行在特殊的模式下,不提供數據存儲服務,只進行普通 redis 節點監控管理。程序員

1、什麼是哨兵(Sentinel)

Sentinel 其實也是一個 redis 的服務端程序,它也會定時執行 serverCron 函數,只是裏面其餘的程序用不到,用到的是對普通 redis 節點的監控以及故障轉移模塊。github

Sentinel 初始化的時候會清空原來的命令表,寫入本身獨有的命令進去,因此普通 redis 節點支持的數據讀寫命令,對 Sentinel 來講都是找不到命令,由於它根本就沒有初始化這些命令的執行器。redis

Sentinel 會定時的對本身監控的 master 執行 info 命令,獲取最新的主從關係,還會定時的給全部的 redis 節點發送 ping 心跳檢測命令,若是檢測到某個 master 沒法響應了,就會在給其餘 Sentinel 發送消息,主觀認爲該 master 宕機,若是 Sentinel 集羣認同該 master 下線的人數達到一個值,那麼你們統一意見,下線該 master。bash

下線以前須要作的是找 Sentinel 集羣中的某一個來執行下線操做,這個步驟叫領導者選舉,選出來之後會從該 master 全部的 slave 節點中挑一個合適的做爲新的 master,並讓其餘 slave 從新同步新的 master。微信

其實以上咱們就簡單的介紹了 Sentinel 是什麼,本質上作了哪些事情,等下咱們會結合源碼細說其中的細節實現。這裏咱們再看下,如何配置並啓動一個 Sentinel 監控。(生產環境建議配置大於三個)markdown

第一步,啓動一個普通的 redis server 節點:函數

image

這一步沒什麼好說的,咱們啓動在一個默認的 6379 端口上。oop

第二步,啓動三個不一樣的 slave 節點:

image

第三步,編寫 sentinel 配置文件:

image

咱們解釋一下這幾條配置的含義,咱們說過 Sentinel 實際上是運行在特殊模式下的 redis server,因此它須要運行端口。緊接着咱們經過命令 sentinel monitor mymaster 配置當前 sentinel 須要監控的主節點 redis 以及觸發客觀下線參數,sentinel down-after-milliseconds 配置了一個參數,master 最長響應時間,超過這個時間就主觀判斷它下線。

sentinel parallel-syncs 配置用於限制主從切換以後,最多的並行同步數據的從節點數量,由於咱們知道,主從進行全量同步階段,從節點加載數據時是不提供服務的,若是這個參數越大,那麼主從切換完成的時間就越短,固然也會致使大量從節點不可提供讀服務,反之。

sentinel failover-timeout 配置了執行故障轉移的最大等待時間。

第四步,啓動 Sentinel:

使用命令,redis-sentinel [config],啓動三個 sentinel。

image

這樣的話,其實咱們就完成了一個簡單的 sentinel 集羣配置,下面咱們手動的讓 master 宕機,看看整個 sentinel 有沒有爲咱們作故障轉移。

image

image

從結果上看來,sentinel 自動爲咱們把原先的從節點 7003 設置爲新的 master,具體過程咱們不細說,等下結合源碼詳細介紹,這裏咱們應該大體對 sentinel 的實際應用有了大概的認識。

2、Sentinel 如何工做的

當咱們使用命令 redis-sentinel 啓動 sentinel 的時候,

int main(int argc, char **argv) {
    。。。。。
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    。。。。。
    if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }
    。。。。。
}
複製代碼

checkForSentinelMode 函數中會根據你的命令以及參數,檢查判斷是不是以 sentinel 模式啓動,若是是則返回 1,反之。若是是以 sentinel 啓動,則會進行一個 sentinel 的初始化操做。

void initSentinelConfig(void) {
    server.port = REDIS_SENTINEL_PORT; //26379
}
複製代碼

initSentinelConfig 實際上就是初始化當前 sentinel 運行端口,默認是 26379。

void initSentinel(void) {
    unsigned int j;
    //清空普通redis-server下可用的命令表
    dictEmpty(server.commands,NULL);
    //加載sentinel須要的命令
    for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
        int retval;
        struct redisCommand *cmd = sentinelcmds+j;

        retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
        serverAssert(retval == DICT_OK);
    }

    sentinel.current_epoch = 0;
    //根據配置文件,初始化本身須要監控的master(一個sentinel是可能監控多個 master的)
    sentinel.masters = dictCreate(&instancesDictType,NULL);
    sentinel.tilt = 0;
    sentinel.tilt_start_time = 0;
    sentinel.previous_time = mstime();
    sentinel.running_scripts = 0;
    sentinel.scripts_queue = listCreate();
    sentinel.announce_ip = NULL;
    sentinel.announce_port = 0;
    sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE;
    sentinel.deny_scripts_reconfig = SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG;
    memset(sentinel.myid,0,sizeof(sentinel.myid));
}
複製代碼

initSentinel 主要的做用仍是清空普通模式的 redis 命令表,加載獨屬於 sentinel 使用的命令,並初始化本身監控的 master 集合。

至此,sentinel 的初始化就算完成了,剩下的自動監控則在定時函數 serverCron 中,咱們一塊兒來看看。

//間隔 100 毫秒執行一次 sentinelTimer
    run_with_period(100) {
        if (server.sentinel_mode) sentinelTimer();
    }
複製代碼

也就是說,sentinel 啓動以後,會間隔 100 毫秒在 serverCron 調用一次 sentinelTimer 函數處理一些重要事件(其實,sentinelTimer 中會修改執行間隔)。

void sentinelTimer(void) {
    sentinelCheckTiltCondition();
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    sentinelRunPendingScripts();
    sentinelCollectTerminatedScripts();
    sentinelKillTimedoutScripts();
    server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}
複製代碼

sentinelTimer 函數體很是簡短,但不要高興太早。sentinelCheckTiltCondition 函數咱們不去多說,redis 高度依賴系統時間,若是屢次檢測到系統時鐘紀元不許確,它會斷定當前系統不穩定,進入 TITL,相似一個休眠的狀態,不會爲咱們作故障轉移,僅僅收集數據,等待系統恢復穩定。

void sentinelHandleDictOfRedisInstances(dict *instances) {
    dictIterator *di;
    dictEntry *de;
    sentinelRedisInstance *switch_to_promoted = NULL;
    di = dictGetIterator(instances);
    //遞歸遍歷監控的全部 master,執行監控操做
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        //這是監控的核心邏輯,下文細說
        sentinelHandleRedisInstance(ri);
        if (ri->flags & SRI_MASTER) {
            //不管是 slave 仍是其餘 sentinel,都視做一個redisInstance
            sentinelHandleDictOfRedisInstances(ri->slaves);
            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);
}
複製代碼

sentinelHandleRedisInstance 主要兩個部分組成,監控和故障轉移。

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    sentinelReconnectInstance(ri);
    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. */
    }

    if (ri->flags & SRI_MASTER) {
        sentinelCheckObjectivelyDown(ri);
        if (sentinelStartFailoverIfNeeded(ri))
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        sentinelFailoverStateMachine(ri);
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}
複製代碼

sentinelReconnectInstance 函數作兩件事,由於當前是一個 sentinel 實例,因此第一件事就是與當前遍歷的 instance 創建鏈接,不論它是 master、slave 或是 sentinel,並在成功創建鏈接後發送 ping 命令。第二,若是當前遍歷的是一個 master 或 slave,則會訂閱它的 sentinel_hello 頻道,當這個頻道上有消息更新,則會廣播全部訂閱的該頻道的客戶端。(訂閱這個頻道的主要做用仍是用於發現其餘 sentinel 以及與其餘 sentinel 交流本身對監控的節點的見解)

sentinelSendPeriodicCommands 函數默認每間隔十秒給 master 和 slave 發送 info 命令,瞭解他們的主從關係,若是此 instance 被本身主觀下線了,那麼會加快發送 info 命令的頻率,以保證本身最快知道主從關係變化,還會每間隔一秒 ping 全部類型的實例。

以上實際上是 sentinelHandleRedisInstance 中監控節點的部分,下面咱們繼續看其故障轉移怎麼作的。

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    sentinelReconnectInstance(ri);
    sentinelSendPeriodicCommands(ri);

    //判斷是否須要進入 tilt 模式
    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. */
    }

    if (ri->flags & SRI_MASTER) {
        //判斷是否須要客戶下線該節點
        sentinelCheckObjectivelyDown(ri);
        //若是肯定該節點客觀下線,進行領導者選舉
        if (sentinelStartFailoverIfNeeded(ri))
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
            //故障轉移
        sentinelFailoverStateMachine(ri);
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}
複製代碼

sentinelCheckSubjectivelyDown 檢測當前節點是否須要主觀下線,判斷條件是此節點對於本身的配置,若是當前這個實例超過配置的時間段沒有回覆本身的 ping,那麼判斷它下線,設置主觀下線標誌位。

sentinelCheckObjectivelyDown 檢測當前是否達到客觀下線的條件,檢測邏輯是這樣的,遍歷全部的兄弟 sentinel 結構,看看他們有沒有把當前節點主觀下線,統計數量,若是達到 quorum,則斷定該 master 客觀下線,設置標誌位並經過頻道通知到其餘 兄弟 sentinel。

sentinelStartFailoverIfNeeded 判斷當前是否已有 sentinel 在進行故障轉移(經過 master 的一個標誌位,若是有 sentinel 正在進行故障轉移,這個標誌位會被設置),若是有,則本身不參與,什麼都不作。

sentinelAskMasterStateToOtherSentinels 會去給其餘 sentinel 發送消息,要求它贊成本身做爲領導者對 master 進行故障轉移。具體怎麼作的呢,首先會拿到本身這邊關於全部兄弟 sentinel 的信息進行一個遍歷,並給他們發送命令 is-master-down-by-addr 要求他們贊成本身成爲領導者,並設置回調函數 sentinelReceiveIsMasterDownReply 處理回覆。

若是某個 sentinel 收到別人發來的領導者投票,且本身沒有給其餘人投過票的話就會贊成,反之不予理睬。

當某個 sentinel 收到足夠的票數,則它認爲本身就是 leader,標誌 master 爲故障轉移中,並進行真正的故障轉移操做。

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;
        //發送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;
    }
}
複製代碼

sentinelFailoverStateMachine 故障轉移包括五個步驟,分五個 sentinelTimer 執行週期處理。當新 master 選舉完成,會給其餘兄弟 sentinel 廣播,告知他們新的 master 已經出現,他們收到後,會撤銷對原 master 的主觀下線,並從新開始監控新的 master。

至此,咱們對 Sentinel 的介紹與源碼分析就結束了,它本質上就是一個運行在特殊模式下的 redis-server,經過不斷 ping 主從節點,在感知他們可能出現故障以後,集體進行一個投票認定並選舉出一我的去執行 master 的客觀下線。

下一篇,咱們看 redis 中更牛逼的 cluster。


關注公衆不迷路,一個愛分享的程序員。 公衆號回覆「1024」加做者微信一塊兒探討學習! 每篇文章用到的全部案例代碼素材都會上傳我我的 github github.com/SingleYam/o… 歡迎來踩!

YangAM 公衆號
相關文章
相關標籤/搜索