【Redis 源碼】集羣之哨兵 sentinel 初識

前言:

說到哨兵之前,可以思考一個問題。爲什麼有哨兵機制?這一點其實可以從主從複製的優缺點作爲考慮。主從複製其實有一個致命的弱點就是它是非高可用的,比如說當主節點出現故障時,那麼此時我的寫入就會出現問題,切換需要重新配,就無法做到高可用。此時其實就可以用到哨兵進制,哨兵自動發現故障,並通過選舉算法重新選舉新主完成轉移和通知實現高可用。
參考資料:
https://redis.io/topics/sentinel
一篇看着太長分爲兩篇:
《集羣之哨兵 sentinel 初識》和《集羣之哨兵 sentinel 故障轉移》。

(一)理解哨兵模式

基礎結構

  1. 哨兵本身是監聽者身份,沒有存儲功能。在整體體系當中,一個 sentinel 或者一個羣 sentinels 與 redis 主從體系是有監聽和被監聽者關係。

  2. 哨兵有如下一些功能:監聽、通知、自動故障轉移、配置提供程序。
    1)監聽:哨兵會不斷檢查主服務和從服務是否正常運行。
    2)通知:哨兵可以通過 API 通知到系統管理員或者其他程序,其中一個受監聽 redis 實例出問題。
    3)自動轉移故障:如果主服務不正常,則哨兵可以啓動故障轉移,在該從服務升級爲主服務。將其該主服務下其他從服務重新配置主服務,並通知使用 Redis 服務器的應用要使用新地址。

  1. 配置提供程序: Sentinel 充當客戶端服務發現的授權來源, 客戶端連接到 Sentinels,以詢問負責給定服務的當前 Redis 主服務器的地址。如果發生故障轉移,Sentinels 將報告新地址。
  1. 哨兵存在三種交互模式:哨兵與主服務、哨兵與從服務、哨兵與其他哨兵。

  2. 哨兵體現中有三個角色:master、slave、sentinel。

(二)配置哨兵模

1.1 sentinel.conf 配置說明

# 哨兵sentinel監控的redis主節點的 ip port 
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2

# 當在Redis實例中開啓了requirepass foobared 授權密碼 這樣所有連接Redis實例的客戶端都要提供密碼  
# 設置哨兵sentinel 連接主從的密碼 注意必須爲主從設置一樣的驗證密碼  
# sentinel auth-pass <master-name> <password>  
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd  

# 指定多少毫秒之後 主節點沒有應答哨兵sentinel 此時 哨兵主觀上認爲主節點下線 默認30秒  
# sentinel down-after-milliseconds <master-name> <milliseconds>  
sentinel down-after-milliseconds mymaster 30000 


# 這個配置項指定了在發生failover主備切換時最多可以有多少個slave同時對新的master進行 同步,  
# 這個數字越小,完成failover所需的時間就越長,  
# 但是如果這個數字越大,就意味着越 多的slave因爲replication而不可用。
# 可以通過將這個值設爲 1 來保證每次只有一個slave 處於不能處理命令請求的狀態。
# sentinel parallel-syncs <master-name> <numslaves>  
sentinel parallel-syncs mymaster 1 


# 故障轉移的超時時間 failover-timeout 可以用在以下這些方面:
# 1. 同一個sentinel對同一個master兩次failover之間的間隔時間。
# 2. 當一個slave從一個錯誤的master那裏同步數據開始計算時間。直到slave被糾正爲向正確的master那裏同步數據時。
# 3.當想要取消一個正在進行的failover所需要的時間。
# 4.當進行failover時,配置所有slaves指向新的master所需的最大時間。不過,即使過了這個超時,slaves依然會被正確配置爲指向master,
# 但是就不按parallel-syncs所配置的規則來了  
# 默認三分鐘  
# sentinel failover-timeout <master-name> <milliseconds>  
sentinel failover-timeout mymaster 180000  


# 哨兵sentinel實例運行的端口 默認26379  
port 26379  


# SCRIPTS EXECUTION  
  
# 配置當某一事件發生時所需要執行的腳本,可以通過腳本來通知管理員,例如當系統運行不正常時發郵件通知相關人員。
# 對於腳本的運行結果有以下規則:
# 若腳本執行後返回1,那麼該腳本稍後將會被再次執行,重複次數目前默認爲10  
# 若腳本執行後返回2,或者比2更高的一個返回值,腳本將不會重複執行。
# 如果腳本在執行過程中由於收到系統中斷信號被終止了,則同返回值爲1時的行爲相同。
# 一個腳本的最大執行時間爲60s,如果超過這個時間,腳本將會被一個SIGKILL信號終止,之後重新執行。
  
# 通知型腳本:當sentinel有任何警告級別的事件發生時(比如說redis實例的主觀失效和客觀失效等等),將會去調用這個腳本,  
# 這時這個腳本應該通過郵件,SMS等方式去通知系統管理員關於系統不正常運行的信息。調用該腳本時,將傳給腳本兩個參數,  
# 一個是事件的類型,一個是事件的描述。如果sentinel.conf配置文件中配置了這個腳本路徑,那麼必須保證這個腳本存在於這個
# 路徑,並且是可執行的,否則sentinel無法正常啓動成功。
# 通知腳本  
# sentinel notification-script <master-name> <script-path>  
sentinel notification-script mymaster /var/redis/notify.sh  
  
# 客戶端重新配置主節點參數腳本  
# 當一個master由於failover而發生改變時,這個腳本將會被調用,通知相關的客戶端關於master地址已經發生改變的信息。
# 以下參數將會在調用腳本時傳給腳本:  
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>  
# 目前<state>總是“failover”,  
# <role>是“leader”或者“observer”中的一個。
# 參數 from-ip, from-port, to-ip, to-port是用來和舊的master和新的master(即舊的slave)通信的  
# 這個腳本應該是通用的,能被多次調用,不是針對性的。
# sentinel client-reconfig-script <master-name> <script-path>  
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

master-name 節點名稱,名稱只能是 [A-z 0-9] 以及.-_內的字符。
quorum 當這些 quorum 個數 sentinel 哨兵認爲 master 主節點失聯, 那麼這時客觀上認爲主節點失聯了 。

1.2 啓動哨兵模式

如果使用的是 redis-sentinel 可執行文件(或者您具有指向該 redis-server 可執行文件的名稱的符號鏈接),則可以使用以下命令行運行 Sentinel:

redis-sentinel /path/to/sentinel.conf

否則,您可以直接使用 redis-server 在 Sentinel 模式下啓動的可執行文件:

redis-server /path/to/sentinel.conf --sentinel

server.c 源碼中可以體現兩種加載模式:

int checkForSentinelMode(int argc, char **argv) {
    int j;

    if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
    for (j = 1; j < argc; j++)
        if (!strcmp(argv[j],"--sentinel")) return 1;
    return 0;
}

(三)哨兵源碼解析

2.1 初始化哨兵

1) server.c 哨兵初始化

int main(int argc, char **argv) {
    //...省略
    server.sentinel_mode = checkForSentinelMode(argc,argv);  //判斷是否爲哨兵模式
    
    //...省略
    
    if (server.sentinel_mode) {
        initSentinelConfig(); //加載端口
        initSentinel();       //初始化哨兵模式,包括命令調用和各種數據結構。
    }
    //...省略
    if (!server.sentinel_mode) { //普通模式
       //...省略
    } else {
        /*
        檢測哨兵模式是否正常配置
        */
        sentinelIsRunning();  
    }
}

initSentinelConfig 函數中爲加載哨兵端口,默認端口爲 26379。initSentinel 函數中初始化哨兵模式,包括命令調用和各種數據結構。

initSentinel 函數如下:

struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};

void initSentinel(void) {
    unsigned int j;

    /* 初始化哨兵命令 */
    dictEmpty(server.commands,NULL);
    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;
    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;
    memset(sentinel.myid,0,sizeof(sentinel.myid));
}

redisCommand 結構體數組爲哨兵模式支持命令集。哨兵有單獨的命令集,只支持 ping、sentinel、subscribe、unsubscribe、psubscribe、publish、info、role、client、shutdown 命令。

2) server.c 哨兵任務

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
     //...省略
     run_with_period(100) {
        if (server.sentinel_mode) sentinelTimer();  //哨兵相關模式下運行任務
    }
    //...省略
}

serverCron 爲 initServer 函數中的註冊時間回調。serverCron 中調用 sentinelTimer 方法中執行哨兵模式中的任務。
包括執行定期操作比如 PING、分析主服務和從服務的 INFO 命令、故障轉移等等。

2.2 定期操作

週期執行命令函數 sentinelSendPeriodicCommands

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

    /* 如果這是一個處於O_DOWN狀態的主服務器的從服務器,我們開始發送
        它每秒鐘都有信息,而不是通常的哨兵信息期週期 */
    if ((ri->flags & SRI_SLAVE) &&
        ((ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS)) ||
         (ri->master_link_down_time != 0)))
    {
        info_period = 1000;   //1s
    } else {
        info_period = SENTINEL_INFO_PERIOD; //10s
    }

    /* ping通過 down-after-milliseconds 參數可以配置,默認1秒*/
    ping_period = ri->down_after_period;
    if (ping_period > SENTINEL_PING_PERIOD) ping_period = SENTINEL_PING_PERIOD;

    /* Send INFO to masters and slaves, not sentinels. */
    if ((ri->flags & SRI_SENTINEL) == 0 &&
        (ri->info_refresh == 0 ||
        (now - ri->info_refresh) > info_period))
    {
        retval = redisAsyncCommand(ri->link->cc,
            sentinelInfoReplyCallback, ri, "%s",
            sentinelInstanceMapCommand(ri,"INFO"));
        if (retval == C_OK) ri->link->pending_commands++;
    }

    /* 發送ping */
    if ((now - ri->link->last_pong_time) > ping_period &&
               (now - ri->link->last_ping_time) > ping_period/2) {
        sentinelSendPing(ri);
    }

    /* PUBLISH 發送hello,SENTINEL_PUBLISH_PERIOD爲2000則爲2s */
    if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) { 
        sentinelSendHello(ri);
    }
}

正常情況下每 10 秒會獲取一次 info 主和從到。每 1 秒發送 ping 命令,ping 命令 down-after-milliseconds 可以配置,每 2 秒廣播一次 hello msg。

總結:

(1)哨兵沒有存儲功能。
(2)哨兵服務支持命令:ping、sentinel、subscribe、unsubscribe、psubscribe、publish、info、role、client、shutdown
(3)哨兵提供功能:監聽、通知、自動故障轉移、配置提供程序。
(4)哨兵有兩種啓動模式:配置或者命令行參數。
(5)哨兵體現中有三個角色:master、slave、sentinel。
(6)週期函數主從非 O_DOWN 狀態下每 10s 發送一次 info 命令到主從。
(7)ping 命令默認是 1s 發送一次,可以通過 down-after-milliseconds 配置。
(8)每 2 秒廣播一次 hello msg。

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