1. 程式人生 > >Redis Sentinel原始碼分析(一)

Redis Sentinel原始碼分析(一)

Base 2.8.7

在程式碼分析前,先總體介紹下sentinel 的機制。
1. 下線定義
sentinel對下線有兩種定義:
a.主觀下線(sdown):sentinel例項本身對服務例項的判斷
b.客觀下線(odown):多個sentinel例項對同一個服務SDOWN的狀態做出協商後的判斷,只有master才可能在odown狀態
簡單的說,一個sentinel單獨做出的判斷只能是sdown,是沒有任何官方效力的,只有多個sentinel大家商量好,得到一致,才能將某個master狀態置為odown,只有確定master odown狀態後,才能做後續fail over的操作
2. 通訊
sentinel與maste/slave的互動主要包括:
a.PING:sentinel向其傳送PING以瞭解其狀態(是否下線)
b.INFO:sentinel向其傳送INFO以獲取replication相關的資訊
c.PUBLISH:sentinel向其監控的master/slave釋出本身的資訊及master相關的配置
d.SUBSCRIBE:sentinel通過訂閱master/slave的”__sentinel__:hello“頻道以獲取其它正在監控相同服務的sentinel
sentinel與sentinel的互動主要包括:
a.PING:sentinel向slave傳送PING以瞭解其狀態(是否下線)
b.SENTINEL is-master-down-by-addr:和其他sentinel協商master狀態,如果master odown,則投票選出leader做fail over
3. fail over


一次完整的fail over包括以下步驟:
a. sentinel發現master下線,則標記master sdown
b. 和其他sentinel協商以確定master狀態是否odown
c. 如果master odown,則選出leader
d. 當選為leader的sentinel選出一個slave做為master,並向該slave傳送slaveof no one命令以轉變slave角色為master
e. 向已下線的master及其他slave傳送slaveof xxxx命令使其作為新當選master的slave

本篇介紹Redis Sentinel初始化及定時事件註冊相關的程式碼,監控&fail over相關的程式碼在下一篇中介紹
redis sentinle可以執行./redis-sentinel sentinel.conf或./redis-server --sentinel sentinel.con執行sentinel程式
其初始化流程和redis-server執行一致,程式初始化會判斷是否處於sentinel模式,如果處於sentinel模式,則會完成一些sentinel相關的初始化工作,主要包括:
1)讀取sentinel相關配置
2)初始化sentinel狀態,並新增master、sentinel及slave到相應的字典
3)註冊sentinel相關的time event(週期性執行)

redis-sentinel和redis-server是同一個程式,檢視Makefile檔案可知:
$(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME)
$(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) 

main函式
int main(int argc, char **argv) {
    ......
    //checkForSentinelMode判斷是否以sentinel模式啟動
    //執行程式名為redis-sentinel,或者帶引數--sentinel執行則認為以sentinel模式執行
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    initServerConfig();

    //sentinel模式下需要完成的初始化工作
    if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }

    if (argc >= 2) {
        ......
        //匯入配置
        loadServerConfig(configfile,options);
        sdsfree(options);
    } 
    ......
    //註冊定時器
    initServer();
    ......
    //判斷config檔案是否存在及是否可寫(sentinel模式需要寫config檔案)
    if (!server.sentinel_mode) {
        ......
    } else {
        sentinelIsRunning();
    }
    //以下開始進入事件處理迴圈
    aeSetBeforeSleepProc(server.el,beforeSleep);
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
    return 0;
}

main函式中的loadServerConfig呼叫了loadServerConfigFromString函式,而loadServerConfigFromString在sentinel模式下會呼叫sentinelHandleConfiguration函式
此處先說明sentinel用到的幾個核心資料結構:
sentinelState為“頂級”資料結構,描述了當前執行的sentinel例項所包含的所有狀態,其master欄位指向包含了該sentinel結點監控的所有master(sentinelRedisInstance )例項狀態的字典
sentinelRedisInstance 描述了sentinel監控的“master”的狀態 
struct sentinelState {
    uint64_t current_epoch;   //當前處在第幾個世紀(每次fail over,current_epoch+1)
    dict *masters;      /* master例項字典(一個sentinle可監控多個master)*/
    int tilt;           /*是否在TITL模式中,後面詳細介紹TITL模式*/
    int running_scripts;    /* 當前正在執行的指令碼 */
    mstime_t tilt_start_time;   /* TITL模式開始的時間 */
    mstime_t previous_time;     /* 上次執行sentinel週期性執行任務的時間,用以判斷是否進入TITL模式*/
    list *scripts_queue;    /* 待執行指令碼佇列 */
} sentinel;
typedef struct sentinelRedisInstance {
    ......
    /* Master specific. */
    dict *sentinels;    /* 監控該master例項的其他sentinel結點字典*/
    dict *slaves;       /* 該master例項說包含的slave結點字典 */
    ......
} sentinelRedisInstance;

sentinelHandleConfiguration函式會handle sentinel相關的配置,建立master、slave、sentinel等例項,並根據配置初始化一些引數,包括髮現redis instance多長時間後判斷其為down
其中建立slave和sentinel的配置可以在sentinel執行過程中生成,也可以使用者配置,like:
sentinel known-slave mymaster 10.2.1.53 6379
sentinel known-sentinel mymaster 10.2.60.50 36379 33b297cfba3a7aece69fc999bb7150fa227e4fe7
char *sentinelHandleConfiguration(char **argv, int argc) {
    sentinelRedisInstance *ri;
    //Handle 類似“sentinel monitor mymaster 10.2.60.50 6379 2”的配置
    //呼叫createSentinelRedisInstance建立master例項(SRI_MASTER)
    if (!strcasecmp(argv[0],"monitor") && argc == 5) {
        /* monitor <name> <host> <port> <quorum> */
        int quorum = atoi(argv[4]);
        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";
            }
        }
    .....
    //呼叫createSentinelRedisInstance建立slave例項(SRI_SLAVE)
    } else if (!strcasecmp(argv[0],"known-slave") && argc == 4) {
        sentinelRedisInstance *slave;
        /* known-slave <name> <ip> <port> */
        //根據master name獲取對應的master例項
        ri = sentinelGetMasterByName(argv[1]);
        if (!ri) return "No such master with specified name.";
        if ((slave = createSentinelRedisInstance(NULL,SRI_SLAVE,argv[2],
                    atoi(argv[3]), ri->quorum, ri)) == NULL)
        {
            return "Wrong hostname or port for slave.";
        }
    //呼叫createSentinelRedisInstance建立sentinel例項(SRI_SENTINEL)
    } else if (!strcasecmp(argv[0],"known-sentinel") &&
               (argc == 4 || argc == 5)) {
        sentinelRedisInstance *si;
        // known-sentinel <name> <ip> <port> [runid] 
        //根據master name獲取對應的master例項
        ri = sentinelGetMasterByName(argv[1]); 

    //對於slave和sentinel例項,定義其hostname為ip:port
    if (flags & (SRI_SLAVE|SRI_SENTINEL)) {
        snprintf(slavename,sizeof(slavename),
            strchr(hostname,':') ? "[%s]:%d" : "%s:%d",
            hostname,port);
        name = slavename;
    }
    //根據例項型別不同,新增到不同的table
    if (flags & SRI_MASTER) table = sentinel.masters;
    else if (flags & SRI_SLAVE) table = master->slaves;
    else if (flags & SRI_SENTINEL) table = master->sentinels;
    sdsname = sdsnew(name);
    if (dictFind(table,sdsname)) {
        sdsfree(sdsname);
        errno = EBUSY;
        return NULL;
    }
    ......
    dictAdd(table, ri->name, ri);
    return ri;
}

main函式呼叫的initSentinel函式初始化了sentinel能夠支援的客戶端命令:
void initSentinel(void) {
    int j;
    //空command字典
    dictEmpty(server.commands,NULL);

    //新增sentinal模式下支援的命令,sentinelcmds包括:ping、sentinel、subscribe、unsubscribe、psubscribe、info、shutdown
    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);
        redisAssert(retval == DICT_OK);
    }
    ......
}

main函式呼叫的initServer會完成一些初始化工作,並註冊定時器
void initServer() {
    ......
    //註冊定時器,定時時間1ms
    if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can't create the serverCron time event.");
        exit(1);
    }
    ......
}

serverCron服務啟動後1ms第一次執行,以後每隔100ms執行一次,serverCron會執行sentinel相關任務
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
......
    /* 如果在sentinel模式下,則執行sentinel相關的週期性任務 */
    run_with_period(100) { //100ms執行一次
        if (server.sentinel_mode) sentinelTimer();
    }
    server.cronloops++;
    return 1000/server.hz;  //hz預設值為10(在sentinelTimer會被修改),此處返回100ms會被其它函式撲捉到,並重新註冊為定時函式
}

sentinelTimer內部包含sentinel模式需要定期執行的操作,包括check master、slave、sentinel的狀態,並根據配置的條件判斷是否需要fail over。