鍍金池/ 教程/ 大數(shù)據(jù)/ Redis 哨兵機(jī)制
Redis 數(shù)據(jù)淘汰機(jī)制
積分排行榜
小剖 Memcache
Redis 數(shù)據(jù)結(jié)構(gòu) intset
分布式鎖
從哪里開(kāi)始讀起,怎么讀
Redis 數(shù)據(jù)結(jié)構(gòu) dict
不在浮沙筑高臺(tái)
Redis 集群(上)
Redis 監(jiān)視器
源碼閱讀工具
Redis 日志和斷言
內(nèi)存數(shù)據(jù)管理
Redis 數(shù)據(jù)結(jié)構(gòu)綜述
源碼日志
Web 服務(wù)器存儲(chǔ) session
消息中間件
Redis 與 Lua 腳本
什么樣的源代碼適合閱讀
Redis 數(shù)據(jù)結(jié)構(gòu) sds
Memcached slab 分配策略
訂閱發(fā)布機(jī)制
Redis 是如何提供服務(wù)的
Redis 事務(wù)機(jī)制
Redis 集群(下)
主從復(fù)制
Redis 應(yīng)用
RDB 持久化策略
Redis 數(shù)據(jù)遷移
Redis 事件驅(qū)動(dòng)詳解
初探 Redis
Redis 與 Memcache
AOF 持久化策略
Redis 數(shù)據(jù)結(jié)構(gòu) redisOb
作者簡(jiǎn)介
Redis 數(shù)據(jù)結(jié)構(gòu) ziplist
Redis 數(shù)據(jù)結(jié)構(gòu) skiplist
Redis 哨兵機(jī)制

Redis 哨兵機(jī)制

Redis 哨兵的服務(wù)框架

哨兵也是 Redis 服務(wù)器,只是它與我們平時(shí)提到的 Redis 服務(wù)器職能不同,哨兵負(fù)責(zé)監(jiān)視普通的 Redis 服務(wù)器,提高一個(gè)服務(wù)器集群的健壯和可靠性。哨兵和普通的 Redis 服務(wù)器所用的是同一套服務(wù)器框架,這包括:網(wǎng)絡(luò)框架,底層數(shù)據(jù)結(jié)構(gòu),訂閱發(fā)布機(jī)制等。

從主函數(shù)開(kāi)始,來(lái)看看哨兵服務(wù)器是怎么誕生,它在什么時(shí)候和普通的 Redis 服務(wù)器分道揚(yáng)鑣:

int main(int argc, char **argv) {
    // 隨機(jī)種子,一般rand() 產(chǎn)生隨機(jī)數(shù)的函數(shù)會(huì)用到
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);
    dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
    // 通過(guò)命令行參數(shù)確認(rèn)是否啟動(dòng)哨兵模式
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    // 初始化服務(wù)器配置,主要是填充redisServer 結(jié)構(gòu)體中的各種參數(shù)
    initServerConfig();
    // 將服務(wù)器配置為哨兵模式,與普通的redis 服務(wù)器不同
    /* We need to init sentinel right now as parsing the configuration file
    * in sentinel mode will have the effect of populating the sentinel
    * data structures with master nodes to monitor. */
    if (server.sentinel_mode) {
        // initSentinelConfig() 只指定哨兵服務(wù)器的端口
        initSentinelConfig();
        initSentinel();
    }
    ......
    // 普通redis 服務(wù)器模式
    if (!server.sentinel_mode) {
    ......
    // 哨兵服務(wù)器模式
    } else {
    // 檢測(cè)哨兵模式是否正常配置
    sentinelIsRunning();
    }
    ......
    // 進(jìn)入事件循環(huán)
    aeMain(server.el);
    // 去除事件循環(huán)系統(tǒng)
    aeDeleteEventLoop(server.el);
    return 0;
}

在上面,通過(guò)判斷命令行參數(shù)來(lái)判斷 Redis 服務(wù)器是否啟用哨兵模式,會(huì)設(shè)置服務(wù)器參數(shù)結(jié)構(gòu)體中的redisServer.sentinel_mode 的值。在上面的主函數(shù)調(diào)用了一個(gè)很關(guān)鍵的函數(shù):initSentinel(),它完成了哨兵服務(wù)器特有的初始化程序,包括填充哨兵服務(wù)器特有的命令表,struct sentinel 結(jié)構(gòu)體。

// 哨兵服務(wù)器特有的初始化程序
/* Perform the Sentinel mode initialization. */
void initSentinel(void) {
    int j;
    // 如果 redis 服務(wù)器是哨兵模式,則清空命令列表。哨兵會(huì)有一套專門的命令列表,
    // 這與普通的 redis 服務(wù)器不同
    /* Remove usual Redis commands from the command table, then just add
    * the SENTINEL command. */
    dictEmpty(server.commands,NULL);
    // 將sentinelcmds 命令列表中的命令填充到server.commands
    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);
    }
    /* Initialize various data structures. */
    // sentinel.current_epoch 用以指定版本
    sentinel.current_epoch = 0;
    // 哨兵監(jiān)視的 redis 服務(wù)器哈希表
    sentinel.masters = dictCreate(&instancesDictType,NULL);
    // sentinel.tilt 用以處理系統(tǒng)時(shí)間出錯(cuò)的情況
    sentinel.tilt = 0;
    // TILT 模式開(kāi)始的時(shí)間
    sentinel.tilt_start_time = 0;
    // sentinel.previous_time 是哨兵服務(wù)器上一次執(zhí)行定時(shí)程序的時(shí)間
    sentinel.previous_time = mstime();
    // 哨兵服務(wù)器當(dāng)前正在執(zhí)行的腳本數(shù)量
    sentinel.running_scripts = 0;
    // 腳本隊(duì)列
    sentinel.scripts_queue = listCreate();
}

我們查看 struct redisCommand sentinelcmds 這個(gè)全局變量就會(huì)發(fā)現(xiàn),它里面只有七個(gè)命令,難道哨兵僅僅提供了這種服務(wù)?為了能讓哨兵自動(dòng)管理普通的 Redis 服務(wù)器,哨兵還添加了一個(gè)定時(shí)程序,我們從 serverCron() 定時(shí)程序中就會(huì)發(fā)現(xiàn),哨兵的定時(shí)程序被調(diào)用執(zhí)行了,這里包含了哨兵的主要工作:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ......
    run_with_period(100) {
         if (server.sentinel_mode) sentinelTimer();
    }
}

定時(shí)程序

定時(shí)程序是哨兵服務(wù)器的重要角色,所做的工作主要包括:監(jiān)視普通的 Redis 服務(wù)器(包括主機(jī)和從機(jī)),執(zhí)行故障修復(fù),執(zhí)行腳本命令。

// 哨兵定時(shí)程序
void sentinelTimer(void) {
    // 檢測(cè)是否需要啟動(dòng)sentinel TILT 模式
    sentinelCheckTiltCondition();
    // 對(duì)哈希表中的每個(gè)服務(wù)器實(shí)例執(zhí)行調(diào)度任務(wù),這個(gè)函數(shù)很重要
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    // 執(zhí)行腳本命令,如果正在執(zhí)行腳本的數(shù)量沒(méi)有超出限定
    sentinelRunPendingScripts();
    // 清理已經(jīng)執(zhí)行完腳本的進(jìn)程,如果執(zhí)行成功從腳本隊(duì)列中刪除腳本
    sentinelCollectTerminatedScripts();
    // 停止執(zhí)行時(shí)間超時(shí)的腳本進(jìn)程
    sentinelKillTimedoutScripts();
    // 為了防止多個(gè)哨兵同時(shí)選舉,故意錯(cuò)開(kāi)定時(shí)程序執(zhí)行的時(shí)間。通過(guò)調(diào)整周期可以
    // 調(diào)整哨兵定時(shí)程序執(zhí)行的時(shí)間,即默認(rèn)值REDIS_DEFAULT_HZ 加上一個(gè)任意值
    server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}

哨兵與 Redis 服務(wù)器的互聯(lián)

每個(gè)哨兵都有一個(gè) struct sentinel 結(jié)構(gòu)體,里面維護(hù)了多個(gè)主機(jī)的連接,與每個(gè)主機(jī)連接的相關(guān)信息都存儲(chǔ)在 struct sentinelRedisInstance。透過(guò)這兩個(gè)結(jié)構(gòu)體,很快就可以描繪出,一個(gè)哨兵服務(wù)器所維護(hù)的機(jī)器的信息:

typedef struct sentinelRedisInstance {
    ......
    /* Master specific. */
    // 其他正在監(jiān)視此主機(jī)的哨兵
    dict *sentinels; /* Other sentinels monitoring the same master. */
    // 次主機(jī)的從機(jī)列表
    dict *slaves; /* Slaves for this master instance. */
    ......
    // 如果是從機(jī),master 則指向它的主機(jī)
    struct sentinelRedisInstance *master; /* Master instance if it's slave. */
    ......
} sentinelRedisInstance;

哨兵服務(wù)器所能描述的 Redis 信息:

http://wiki.jikexueyuan.com/project/redis/images/a.png" alt="" />

可見(jiàn),哨兵服務(wù)器連接(監(jiān)視)了多臺(tái)主機(jī),多臺(tái)從機(jī)和多臺(tái)哨兵服務(wù)器。有這樣大概的脈絡(luò),我們繼續(xù)往下看就會(huì)更有線索。

哨兵要監(jiān)視 Redis 服務(wù)器,就必須連接 Redis 服務(wù)器。啟動(dòng)哨兵的時(shí)候需要指定一個(gè)配置文件,程序初始化的時(shí)候會(huì)讀取這個(gè)配置文件,獲取被監(jiān)視 Redis 服務(wù)器的 IP 地址和端口等信息。

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

或者

redis-sentinel /path/to/sentinel.conf

如果想要監(jiān)視一個(gè) Redis 服務(wù)器,可以在配置文件中寫入:

sentinel monitor <master-name> <ip> <redis-port> <quorum>

其中,master-name 是主機(jī)名,ip redis-port 分別是 IP 地址和端口,quorum 是哨兵用來(lái)判斷某個(gè) Redis 服務(wù)器是否下線的參數(shù),之后會(huì)講到。sentinelHandleConfiguration() 函數(shù)中,完成了對(duì)配置文件的解析和處理過(guò)程。

// 哨兵配置文件解析和處理
char *sentinelHandleConfiguration(char **argv, int argc) {
    sentinelRedisInstance *ri;
    if (!strcasecmp(argv[0],"monitor") && argc == 5) {
        /* monitor <name> <host> <port> <quorum> */
        int quorum = atoi(argv[4]);
        // quorum >= 0
    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";
        }
    }
    ......
}

可以看到里面主要調(diào)用了 createSentinelRedisInstance() 函數(shù)。createSentinelRedisInstance() 函數(shù)的主要工作是初始化 sentinelRedisInstance 結(jié)構(gòu)體。在這里,哨兵并沒(méi)有選擇立即去連接這指定的 Redis 服務(wù)器,而是將 sentinelRedisInstance.flag 標(biāo)記 SRI_DISCONNECT,而將連接的工作丟到定時(shí)程序中去,可以聯(lián)想到,定時(shí)程序中肯定有一個(gè)檢測(cè) sentinelRedisInstance.flag 的函數(shù),如果發(fā)現(xiàn)連接是斷開(kāi)的,會(huì)發(fā)起連接。這個(gè)策略和我們之前的講到的主從連接時(shí)候的策略是一樣的,是 Redis 的慣用手法。因?yàn)樯诒?Redis 服務(wù)器保持連接,所以必然會(huì)定時(shí)檢測(cè)和 Redis 服務(wù)器的連接狀態(tài)。

在定時(shí)程序的調(diào)用鏈中,確實(shí)發(fā)現(xiàn)了哨兵主動(dòng)連接 Redis 服務(wù)器的過(guò)程:

sentinelTimer()->sentinelHandleRedisInstance()->sentinelReconnectInstance()。

sentinelReconnectInstance() 負(fù)責(zé)連接被標(biāo)記為 SRI_DISCONNECT 的 Redis 服務(wù)器。它對(duì)一個(gè) Redis 服務(wù)器發(fā)起了兩個(gè)連接:

  1. 普通連接(sentinelRedisInstance.cc,Commands connection)
  2. 訂閱發(fā)布專用連接(sentinelRedisInstance.pc,publish connection)。為什么需要分這兩個(gè)連接呢?因?yàn)閷?duì)于一個(gè)客戶端連接來(lái)說(shuō),redis 服務(wù)器要么專門處理普通的命令,要么專門處理訂閱發(fā)布命令,這在之前訂閱發(fā)布篇幅中專門有提及這個(gè)細(xì)節(jié)。
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
    if (!(ri->flags & SRI_DISCONNECTED)) return;
        /* Commands connection. */
    if (ri->cc == NULL) {
        ri->cc = redisAsyncConnect(ri->addr->ip,ri->addr->port);
        // 連接出錯(cuò)
    if (ri->cc->err) {
        // 錯(cuò)誤處理
    } else {
        // 此連接被綁定到redis 服務(wù)器的事件中心
        ......
    }
}
    // 此哨兵會(huì)訂閱所有主從機(jī)的hello 訂閱頻道,每個(gè)哨兵都會(huì)定期將自己監(jiān)視的
    // 服務(wù)器和自己的信息發(fā)送到主從服務(wù)器的hello 頻道,從而此哨兵就能發(fā)現(xiàn)其
    // 他服務(wù)器,并且也能將自己的監(jiān)測(cè)的數(shù)據(jù)散播到其他服務(wù)器。這就是redis 所
    // 謂的auto discover.
    /* Pub / Sub */
    if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && ri->pc == NULL) {
        ri->pc = redisAsyncConnect(ri->addr->ip,ri->addr->port);
        // 連接出錯(cuò)
    if (ri->pc->err) {
    // 錯(cuò)誤處理
    } else {
    // 此連接被綁定到redis 服務(wù)器的事件中心
    ......
    // 訂閱了ri 上的__sentinel__:hello 頻道
    /* Now we subscribe to the Sentinels "Hello" channel. */
    retval = redisAsyncCommand(ri->pc,
    sentinelReceiveHelloMessages, NULL, "SUBSCRIBE %s",
    SENTINEL_HELLO_CHANNEL);
    ......
    }
}

Redis 在定時(shí)程序中會(huì)嘗試對(duì)所有的 master 作重連接。這里會(huì)有一個(gè)疑問(wèn),之前有提到從機(jī)(slave),哨兵又是在什么時(shí)候連接了從機(jī)和哨兵呢?

HELLO 命令

我們從上面 sentinelReconnectInstance() 的源碼得知,哨兵對(duì)于一個(gè) Redis 服務(wù)器管理了兩個(gè)連接:普通命令連接和訂閱發(fā)布專用連接。其中,哨兵在初始化訂閱發(fā)布連接的時(shí)候,做了兩個(gè)工作:一是,向 Redis 服務(wù)器發(fā)送 SUBSCRIBE SENTINEL_HELLO_CHANNEL命令;二是,注冊(cè)了回調(diào)函數(shù) sentinelReceiveHelloMessages()。稍稍理解大概可以畫出下面的數(shù)據(jù)流向圖:

http://wiki.jikexueyuan.com/project/redis/images/a1.png" alt="" />

從源碼來(lái)看,哨兵 A 向 master 1 的 HELLO 頻道發(fā)布的數(shù)據(jù)有:哨兵 A 的 IP 地址,端口,runid,當(dāng)前配置版本,以及 master 1 的 IP,端口,當(dāng)前配置版本。從上圖可以看出,其他所有監(jiān)視同一 Redis 服務(wù)器的哨兵都能收到一份 HELLO 數(shù)據(jù),這是訂閱發(fā)布相關(guān)的內(nèi)容。

在定時(shí)程序的調(diào)用鏈:sentinelTimer()->sentinelHandleRedisInstance()->sentinelPingInstance() 中,哨兵會(huì)向 Redis 服務(wù)器的 hello 頻道發(fā)布數(shù)據(jù)。在 sentinel.c 文件中找到向 hello 頻道發(fā)布數(shù)據(jù)的函數(shù):

int sentinelSendHello(sentinelRedisInstance *ri) {
    // ri 可以是一個(gè)主機(jī),從機(jī)。
    // 只是用主機(jī)和從機(jī)作為一個(gè)中轉(zhuǎn),主從機(jī)收到publish 命令后會(huì)將數(shù)據(jù)傳輸給
    // 訂閱了hello 頻道的哨兵。這里可能會(huì)有疑問(wèn),為什么不直接發(fā)給哨兵???
    char ip[REDIS_IP_STR_LEN];
    char payload[REDIS_IP_STR_LEN+1024];
    int retval;
    sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master;
    sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master);
    /* Try to obtain our own IP address. */
    if (anetSockName(ri->cc->c.fd,ip,sizeof(ip),NULL) == -1) return REDIS_ERR;
    if (ri->flags & SRI_DISCONNECTED) return REDIS_ERR;
        // 格式化需要發(fā)送的數(shù)據(jù),包括:
        // 哨兵IP 地址,端口,runnid,當(dāng)前配置版本,
        // 主機(jī)IP 地址,端口,當(dāng)前配置的版本
        /* Format and send the Hello message. */
        snprintf(payload,sizeof(payload),
        "%s,%d,%s,%llu," /* Info about this sentinel. */
        "%s,%s,%d,%llu", /* Info about current master. */
        ip, server.port, server.runid,
        (unsigned long long) sentinel.current_epoch,
        /* --- */
        master->name,master_addr->ip,master_addr->port,
        (unsigned long long) master->config_epoch);
        retval = redisAsyncCommand(ri->cc,
        sentinelPublishReplyCallback, NULL, "PUBLISH %s %s",
            SENTINEL_HELLO_CHANNEL,payload);
    if (retval != REDIS_OK) return REDIS_ERR;
        ri->pending_commands++;
        return REDIS_OK;
}

redisAsync 系列的函數(shù)底層也是《Redis 事件驅(qū)動(dòng)詳解》中的內(nèi)容

當(dāng) Redis 服務(wù)器收到來(lái)自哨兵的數(shù)據(jù)時(shí)候,會(huì)向所有訂閱 hello 頻道的哨兵發(fā)布數(shù)據(jù),由此剛才注冊(cè)的回調(diào)函數(shù)sentinelReceiveHelloMessages() 就被調(diào)用了?;卣{(diào)函數(shù) sentinelReceiveHelloMessages() 做了兩件事情:

  1. 發(fā)現(xiàn)其他監(jiān)視同一 Redis 服務(wù)器的哨兵
  2. 更新配置版本,當(dāng)其他哨兵傳遞的配置版本更高的時(shí)候,會(huì)更新 Redis 主服務(wù)器配置(IP 地址和端口)

總結(jié)一下這里的工作原理,哨兵會(huì)向 hello 頻道發(fā)送包括:哨兵自己的IP 地址和端口,runid,當(dāng)前的配置版本;其所監(jiān)視主機(jī)的 IP 地址,端口,當(dāng)前的配置版本?!具@里要說(shuō)清楚,什么是 runid 和配置版本】雖然未知的信息很多,但我們可以得知,當(dāng)一個(gè)哨兵新加入到一個(gè) Redis 集群中時(shí),就能通過(guò) hello 頻道,發(fā)現(xiàn)其他更多的哨兵,而它自己也能夠被其他的哨兵發(fā)現(xiàn)。這是 Redis 所謂 auto discover 的一部分。

INFO 命令

同樣,在定時(shí)程序的調(diào)用鏈:sentinelTimer()->sentinelHandleRedisInstance()->sentinelPingInstance() 中,哨兵向與 Redis 服務(wù)器的命令連接通道上,發(fā)送了一個(gè)INFO 命令(字符串);并注冊(cè)了回調(diào)函數(shù)sentinelInfoReplyCallback()。Redis 服務(wù)器需要對(duì) INFO 命令作出相應(yīng),能在 redis.c 主文件中找到 INFO 命令的處理函數(shù):當(dāng) Redis 服務(wù)器收到INFO命令時(shí)候,會(huì)向該哨兵回傳數(shù)據(jù),包括:

關(guān)于該 Redis 服務(wù)器的細(xì)節(jié)信息,rRedis 軟件版本,與其所連接的客戶端信息,內(nèi)存占用情況,數(shù)據(jù)落地(持久化)情況,各種各樣的狀態(tài),主從復(fù)制信息,所有從機(jī)的信息,CPU 使用情況,存儲(chǔ)的鍵值對(duì)數(shù)量等。

由此得到最值得關(guān)注的信息,所有從機(jī)的信息都在這個(gè)時(shí)候曝光給了哨兵,哨兵由此就可以監(jiān)視此從機(jī)了。

Redis 服務(wù)器收集了這些信息回傳給了哨兵,剛才所說(shuō)哨兵的回調(diào)函數(shù) sentinelInfoReplyCallback()會(huì)被調(diào)用,它的主要工作就是著手監(jiān)視未被監(jiān)視的從機(jī);完成一些故障修復(fù)(failover)的工作。連同上面的一節(jié),就是Redis 的 auto discover 的全貌了。

http://wiki.jikexueyuan.com/project/redis/images/a2.png" alt="" />

心跳

心跳是一種判斷兩臺(tái)機(jī)器連接是否正常非常常用的手段,接收方在收到心跳包之后,會(huì)更新收到心跳的時(shí)間,在某個(gè)時(shí)間點(diǎn)如果檢測(cè)到心跳包過(guò)久未收到(即超時(shí)),這證明網(wǎng)絡(luò)環(huán)境不好,或者對(duì)方很忙,也為接收方接下來(lái)的行動(dòng)提供指導(dǎo):接收方可以等待心跳正常的時(shí)候再發(fā)送數(shù)據(jù)。在哨兵的定時(shí)程序中,哨兵會(huì)向所有的服務(wù)器,包括哨兵服務(wù)器,發(fā)送 PING 心跳,而哨兵收到來(lái)自 Redis 服務(wù)器的回應(yīng)后,也會(huì)更新相應(yīng)的時(shí)間點(diǎn)或者執(zhí)行其他操作。

http://wiki.jikexueyuan.com/project/redis/images/a3.png" alt="" />

在線狀態(tài)監(jiān)測(cè)

哨兵有兩種判斷用戶在線的方法,主觀和客觀方法,即 Check Subjectively Down 和 Check Objective Down。主觀是說(shuō),Redis 服務(wù)器的在線判斷依據(jù)是某個(gè)哨兵自己的信息;客觀是說(shuō),Redis 服務(wù)器的在線判斷依據(jù)是由其他監(jiān)視此 Redis 服務(wù)器的哨兵的信息。

http://wiki.jikexueyuan.com/project/redis/images/a4.png" alt="" />

哨兵憑借的自己的信息判斷 Redis 服務(wù)器是否下線的方法,稱為主觀方法,即通過(guò)判斷前面有提到的 PING 心跳等其他通信時(shí)間是否超時(shí)來(lái)判斷主機(jī)是否下線。主觀的信息有可能是錯(cuò)的。

http://wiki.jikexueyuan.com/project/redis/images/a5.png" alt="" />

哨兵不僅僅憑借自己的信息,還依據(jù)其他哨兵提供的信息判斷 Redis 服務(wù)器是否下線的方法稱為客觀方法,即通過(guò)所有其他哨兵報(bào)告的主機(jī)在線狀態(tài)來(lái)判定某主機(jī)是否下線。前面提到,INFO 命令可以從其他哨兵服務(wù)器上獲取信息,而這里面的信息就包含了他們共同關(guān)注主機(jī)的在線狀態(tài)??陀^判斷方法是基于主觀判斷方法的,即如果一個(gè) Redis 服務(wù)器被客觀判定為下線,那么其早已被主觀判斷為下線了。因此客觀判斷的在線狀態(tài)較有說(shuō)服力,譬如在故障修復(fù)中就用到客觀判斷的結(jié)果。

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    int quorum = 0, odown = 0;
    // 足夠多的哨兵報(bào)告主機(jī)下線了,則設(shè)置Objectively down 標(biāo)記
    if (master->flags & SRI_S_DOWN) { // 此哨兵本身認(rèn)為redis 服務(wù)器下線了
        /* Is down for enough sentinels? */
        quorum = 1; /* the current sentinel. */
        /* Count all the other sentinels. */
        // 查看其它哨兵報(bào)告的狀況
        di = dictGetIterator(master->sentinels);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
    if (ri->flags & SRI_MASTER_DOWN) quorum++;
    }
    dictReleaseIterator(di);
    // 足夠多的哨兵報(bào)告主機(jī)下線了,設(shè)置標(biāo)記
    if (quorum >= master->quorum) odown = 1;
    }
    /* Set the flag accordingly to the outcome. */
    if (odown) {
        // 寫日志,設(shè)置SRI_O_DOWN
    if ((master->flags & SRI_O_DOWN) == 0) {
        sentinelEvent(REDIS_WARNING,"+odown",master,"%@ #quorum %d/%d",
        quorum, master->quorum);
        master->flags |= SRI_O_DOWN;
        master->o_down_since_time = mstime();
    }
    } else {
    // 寫日志,取消SRI_O_DOWN
    if (master->flags & SRI_O_DOWN) {
        sentinelEvent(REDIS_WARNING,"-odown",master,"%@");
        master->flags &= ~SRI_O_DOWN;
        }
    }
}

故障修復(fù)

一個(gè) Redis 集群難免遇到主機(jī)宕機(jī)斷電的時(shí)候,哨兵如果檢測(cè)主機(jī)被大多數(shù)的哨兵判定為下線,就很可能會(huì)執(zhí)行故障修復(fù),重新選出一個(gè)主機(jī)。一般在 Redis 服務(wù)器集群中,只有主機(jī)同時(shí)肩負(fù)讀請(qǐng)求和寫請(qǐng)求的兩個(gè)功能,而從機(jī)只負(fù)責(zé)讀請(qǐng)求,從機(jī)的數(shù)據(jù)更新都是由之前所提到的主從復(fù)制上獲取的。因此,當(dāng)出現(xiàn)意外情況的時(shí)候,很有必要新選出一個(gè)新的主機(jī)。

http://wiki.jikexueyuan.com/project/redis/images/a6.png" alt="" />

一般在 Redis 服務(wù)器集群中,只有主機(jī)同時(shí)肩負(fù)讀請(qǐng)求和寫請(qǐng)求的兩個(gè)功能,而從機(jī)只負(fù)責(zé)讀請(qǐng)求依然是在定時(shí)程序的調(diào)用鏈中, 我們能找到故障修復(fù)(failover) 誕生的地方:

sentinelTimer()->sentinelHandleRedisInstance()->sentinelStartFailoverIfNeeded()。

sentinelStartFailoverIfNeeded() 函數(shù)判斷是否有必要進(jìn)行故障修復(fù),這里有三個(gè)條件:

  1. Redis 主機(jī)必須已經(jīng)被客觀判定為下線了
  2. 針對(duì) Redis 主機(jī)的故障修復(fù)尚未開(kāi)始
  3. 限定時(shí)間內(nèi),不能多次執(zhí)行故障修復(fù)

三個(gè)條件都得到滿足,故障修復(fù)就開(kāi)始了。

繼續(xù)往下走:sentinelTimer()->sentinelHandleRedisInstance()->sentinelStartFailoverIfNeeded()->sentinelStartFailover()。sentinelStartFailover() 設(shè)置了一些故障修復(fù)相關(guān)的標(biāo)記等數(shù)據(jù)。故障修復(fù)分成了幾個(gè)步驟完成,每個(gè)步驟對(duì)應(yīng)一個(gè)狀態(tài)。

http://wiki.jikexueyuan.com/project/redis/images/b.png" alt="" />

故障修復(fù)狀態(tài)圖

哨兵專門有一個(gè)故障修復(fù)狀態(tài)機(jī),

// 故障修復(fù)狀態(tài)機(jī),依據(jù)被標(biāo)記的狀態(tài)執(zhí)行相應(yīng)的動(dòng)作
void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
    redisAssert(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;
            case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
            sentinelFailoverSendSlaveOfNoOne(ri);
            break;
            case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
            sentinelFailoverWaitPromotion(ri);
            break;
            case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
            sentinelFailoverReconfNextSlave(ri);
            break;
    }
}

WAIT_START

在哨兵服務(wù)器群中,有首領(lǐng)(leader)的概念,這個(gè)首領(lǐng)可以是系統(tǒng)管理員根據(jù)具體情況指定的,也可以是眾多的哨兵中按一定的條件選出的。在 WAIT_STATE 中執(zhí)行故障修復(fù)的哨兵首先確定自己是不是首領(lǐng),如果不是故障修復(fù)會(huì)被拖延,到下一個(gè)定時(shí)程序再次檢測(cè)自己是否為首領(lǐng),超過(guò)一定時(shí)間會(huì)強(qiáng)制停止故障修復(fù)。

怎么樣才可以當(dāng)選一個(gè)首領(lǐng)呢?每一個(gè)哨兵都會(huì)有一個(gè)當(dāng)前的配置版本號(hào) current_-epoch,此版本號(hào)會(huì)經(jīng)由hello,is-master-down 命令交換,以便將自身的版本號(hào)告知其他所有監(jiān)視同一 Redis 服務(wù)器的哨兵。

每一個(gè)哨兵手里都會(huì)有一票投給其中一個(gè)配置版本最高的哨兵,它的投票信息將會(huì)通過(guò) is-master-down 命令交換。is-master-down 命令在故障修復(fù)的時(shí)候會(huì)被強(qiáng)制觸發(fā),收到它的哨兵將會(huì)進(jìn)行投票并返回自己的投票結(jié)果,哨兵會(huì)將它保存在對(duì)應(yīng)的 sentinelRedisInstance 中。如此一來(lái),執(zhí)行故障修復(fù)的哨兵就能得到其他哨兵的投票結(jié)果,它就能確定自己是不是哨兵了。

struct sentinelState {
    // 哨兵的配置版本
    uint64_t current_epoch;
    ......
    } sentinel;
typedef struct sentinelRedisInstance {
    ......
    // 故障修復(fù)相關(guān)的參數(shù)
    /* Failover */
    // 所選首領(lǐng)的runid。runid 其實(shí)就是一個(gè)redis 服務(wù)器唯一標(biāo)識(shí)
    char *leader; /* If this is a master instance, this is the runid of
    the Sentinel that should perform the failover. If
    this is a Sentinel, this is the runid of the Sentinel
    that this Sentinel voted as leader. */
    // 所選首領(lǐng)的配置版本
    uint64_t leader_epoch; /* Epoch of the 'leader' field. */
    ......
} sentinelRedisInstance;

因此, 只要某哨兵的配置版本足夠高, 它就有機(jī)會(huì)當(dāng)選為首領(lǐng)。在

sentinelTimer()-?sentinelHandleDictOfRedisInstances()-?sentinelHandleRedisInstance()-
?sentinelFailoverStateMachine()-?sentinelFailoverWaitStart()-?sentinelFailoverWaitStart()

你可以看到詳細(xì)的投票過(guò)程。

總結(jié)了一下選舉首領(lǐng)的過(guò)程:

  1. 遍歷哨兵表中的所有哨兵,統(tǒng)計(jì)每個(gè)哨兵的得票情況,注意,得票哨兵的版本號(hào)必須和執(zhí)行故障修復(fù)哨兵的配置版本號(hào)相同,這樣做是為了確認(rèn)執(zhí)行故障修復(fù)版本號(hào)已經(jīng)將自己的版本告訴了其他的哨兵。【這里在畫圖的時(shí)候可以說(shuō)明白,其實(shí)低版本號(hào)的哨兵是沒(méi)有機(jī)會(huì)進(jìn)行故障修復(fù)的】
  2. 計(jì)算得票最多的哨兵
  3. 執(zhí)行故障修復(fù)的哨兵自己給得票數(shù)最高的哨兵投一票,如果沒(méi)有投票結(jié)果,則給自己投一票。當(dāng)然投票的前提還是配置版本號(hào)要比自己的高。
  4. 再次計(jì)算得票最多的哨兵
  5. 滿足兩個(gè)條件:得票最多的哨兵的票數(shù)必須超過(guò)選舉數(shù)的一半以上;得票最多的哨兵的票數(shù)必須超過(guò)主機(jī)的法定人數(shù)(quorum)。

http://wiki.jikexueyuan.com/project/redis/images/b1.png" alt="" />

是一個(gè)比較曲折的過(guò)程。最終,如果確定當(dāng)前執(zhí)行故障修復(fù)的哨兵是首領(lǐng),它則可以進(jìn)入下一個(gè)狀態(tài):SELECT_SLAVE。

SELECT_SLAVE

SELECT_SLAVE 的意圖很明確,因?yàn)楫?dāng)前的主機(jī)(master)已經(jīng)掛了,需要重新指定一個(gè)主機(jī),候選的服務(wù)器就是當(dāng)前掛掉主機(jī)的所有從機(jī)(slave)。

sentinelTimer()-?sentinelHandleDictOfRedisInstances()-?sentinelHandleRedisInstance()-?sentinelFailoverStateMachine()-?sentinelFailoverSelectSlave()-?sentinelSelectSlave() 

你可以看到詳細(xì)的選舉過(guò)程。

當(dāng)前執(zhí)行故障修復(fù)的哨兵會(huì)遍歷主機(jī)的所有從機(jī),只有足夠健康的從機(jī)才能被成為候選主機(jī)。足夠健康的條件包括:

  1. 不能有下面三個(gè)標(biāo)記中的一個(gè):SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED
  2. ping 心跳正常
  3. 優(yōu)先級(jí)不能為 0(slave->slave_priority)
  4. INFO 數(shù)據(jù)不能超時(shí)
  5. 主從連接斷線會(huì)時(shí)間不能超時(shí)

滿足以上條件就有機(jī)會(huì)成為候選主機(jī),如果經(jīng)過(guò)上面的篩選之后有多臺(tái)從機(jī),那么這些從機(jī)會(huì)按下面的條件排序:

  1. 優(yōu)選選擇優(yōu)先級(jí)高的從機(jī)
  2. 優(yōu)先選擇主從復(fù)制偏移量高的從機(jī),即從機(jī)從主機(jī)復(fù)制的數(shù)據(jù)越多
  3. 優(yōu)先選擇有 runid 的從機(jī)
  4. 如果上面條件都一樣,那么將 runid 按字典順序排序

所選用的排序算法是常用的快排。這是一個(gè)比較曲折的過(guò)程。如果沒(méi)有從機(jī)符合要求,譬如最極端的情況,所有從機(jī)都跟著掛了,那么故障修復(fù)會(huì)失??;否則最終會(huì)確定一個(gè)從機(jī)成為候選主機(jī)。從機(jī)可以進(jìn)入下一個(gè)狀態(tài):SLAVEOF_NOONE。

SLAVEOF_NOONE

這一步中,哨兵主要做的是向候選主機(jī)發(fā)送slaveof noone 命令。我們知道,slaveof noone 命令可以讓一個(gè)從機(jī)轉(zhuǎn)變?yōu)橐粋€(gè)主機(jī),Redis 從機(jī)收到會(huì)做從從機(jī)到主機(jī)的轉(zhuǎn)換。發(fā)送 slaveof noone 命令之后,哨兵還會(huì)向候選主機(jī)發(fā)送 config rewrite 讓候選主機(jī)當(dāng)前配置信息寫入配置文件,以方便候選從機(jī)下次重啟的時(shí)候可以恢復(fù)。

void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) {
    int retval;
    // 與候選從機(jī)的連接必須正常,且故障修復(fù)沒(méi)有超時(shí)
    /* We can't send the command to the promoted slave if it is now
    * disconnected. Retry again and again with this state until the timeout
    * is reached, then abort the failover. */
    if (ri->promoted_slave->flags & SRI_DISCONNECTED) {
    if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
        sentinelEvent(REDIS_WARNING,"-failover-abort-slave-timeout",ri,"%@");
        sentinelAbortFailover(ri);
    }
    return;
}
/* Send SLAVEOF NO ONE command to turn the slave into a master.
* We actually register a generic callback for this command as we don't
* really care about the reply. We check if it worked indirectly observing
* if INFO returns a different role (master instead of slave). */
retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0);
......
}

http://wiki.jikexueyuan.com/project/redis/images/b2.png" alt="" />

WAIT_PROMOTION

這一狀態(tài)純粹是為了等待上一個(gè)狀態(tài)的執(zhí)行結(jié)果(如候選主機(jī)的一些狀態(tài))被傳播到此哨兵上,至于是如何傳播的,之前我們有提到過(guò) INFO 數(shù)據(jù)傳輸?shù)倪^(guò)程。這一狀態(tài)的執(zhí)行函數(shù) sentinelFailoverWaitPromotion() 只做了超時(shí)的判斷,如果超時(shí)就會(huì)停止故障修復(fù)。那狀態(tài)是如何轉(zhuǎn)變的呢?就在哨兵捕捉到候選主機(jī)狀態(tài)的時(shí)候。我們可以看到,在哨兵處理 Redis 服務(wù)器 INFO 輸出的回調(diào)函數(shù) sentinelInfoReplyCallback() 中,故障修復(fù)的狀態(tài)從 WAIT_PROMOTION 轉(zhuǎn)變到了下一個(gè)狀態(tài) RECONF_SLAVES。

RECONF_SLAVES

這是故障修復(fù)狀態(tài)機(jī)里面的最后一個(gè)狀態(tài),后面還會(huì)有一個(gè)狀態(tài)。這一狀態(tài)主要做的是向其他非候選從機(jī)發(fā)送 slaveof promote_slave,即讓候選主機(jī)成為他們的主機(jī)。其中會(huì)涉及幾個(gè) Redis 服務(wù)器狀態(tài)的標(biāo)記:SRI_RECONF_SENT,SRI_RECONFINPROG,SRI-RECONF_DONE,分別表示已經(jīng)向從機(jī)發(fā)送 slaveof 命令,從機(jī)正在重新配置(這里需要一些時(shí)間),配置完成。同樣,哨兵是通過(guò) INFO 數(shù)據(jù)傳輸中獲知這些狀態(tài)變更的。

http://wiki.jikexueyuan.com/project/redis/images/b3.png" alt="" />

詳細(xì)重新配置過(guò)程可以在

sentinelTimer()-?sentinelHandleDictOfRedisInstances()-
?sentinelHandleRedisInstance()-?sentinelFailoverStateMachine()-
?sentinelFailoverReconfNextSlave()-?sentinelSelectSlave()

最后會(huì)做從機(jī)配置狀況的檢測(cè),如果所有從機(jī)都重新配置完成或者超時(shí)了,會(huì)進(jìn)入最后一個(gè)狀態(tài) UPDATE_CONFIG。

UPDATE_CONFIG

這里還存在最后一個(gè)狀態(tài) UPDATE_CONFIG。在定時(shí)程序中如果發(fā)現(xiàn)進(jìn)入了這一狀態(tài),會(huì)調(diào)用sentinelFailoverSwitchToPromotedSlave()-?sentinelResetMasterAndChangeAddress()。因?yàn)橹鳈C(jī)和從機(jī)發(fā)生了修改,所以 sentinel.masters 肯定需要修改,譬如主機(jī)的IP 地址和端口,所以最后的工作是將修改并整理哨兵服務(wù)器保存的信息,而這正是 sentinelResetMasterAndChangeAddress()的主要工作。

http://wiki.jikexueyuan.com/project/redis/images/b4.png" alt="" />

int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, int port) {
    sentinelAddr *oldaddr, *newaddr;
    sentinelAddr **slaves = NULL;
    int numslaves = 0, j;
    dictIterator *di;
    dictEntry *de;
    newaddr = createSentinelAddr(ip,port);
    if (newaddr == NULL) return REDIS_ERR;
        // 保存從機(jī)實(shí)例
        /* Make a list of slaves to add back after the reset.
        * Don't include the one having the address we are switching to. */
        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);
    // 主機(jī)也被視為從機(jī)添加到從機(jī)數(shù)組
    /* If we are switching to a different address, include the old address
    * as a slave as well, so that we'll be able to sense / reconfigure
    * the old master. */
    if (!sentinelAddrIsEqual(newaddr,master->addr)) {
        slaves = zrealloc(slaves,sizeof(sentinelAddr*)*(numslaves+1));
        slaves[numslaves++] = createSentinelAddr(master->addr->ip,
        master->addr->port);
    }
    // 重置主機(jī)
    // sentinelResetMaster() 會(huì)將很多信息清空,也會(huì)設(shè)置很多信息
    /* Reset and switch address. */
    sentinelResetMaster(master,SENTINEL_RESET_NO_SENTINELS);
    oldaddr = master->addr;
    master->addr = newaddr;
    master->o_down_since_time = 0;
    master->s_down_since_time = 0;
    // 將從機(jī)恢復(fù)
    /* 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(REDIS_NOTICE,"+slave",slave,"%@");
        sentinelFlushConfig();
        }
    }
    zfree(slaves);
    // 銷毀舊的地址結(jié)構(gòu)體
    /* Release the old address at the end so we are safe even if the function
    * gets the master->addr->ip and master->addr->port as arguments. */
    releaseSentinelAddr(oldaddr);
    sentinelFlushConfig();
    return REDIS_OK;
}

還有一個(gè)問(wèn)題:故障修復(fù)過(guò)程中,一直沒(méi)有發(fā)送 SLAVEOF promoted_slave 給舊的主機(jī),因?yàn)橐呀?jīng)和舊的主機(jī)斷開(kāi)連接,哨兵沒(méi)有選擇在故障修復(fù)的時(shí)候向它發(fā)送任何的數(shù)據(jù)。但在故障修復(fù)的最后一個(gè)狀態(tài)中,哨兵依舊有將舊的主機(jī)塞到新主機(jī)的從機(jī)列表中,所以哨兵還是會(huì)超時(shí)發(fā)送 INFO HELLO 等數(shù)據(jù),對(duì)舊的主機(jī)抱有希望。如果因?yàn)榫W(wǎng)絡(luò)環(huán)境的不佳導(dǎo)致的故障修復(fù),那舊的主機(jī)很可能恢復(fù)過(guò)來(lái),只是這時(shí)它是一臺(tái)從機(jī)了。哨兵選擇在這個(gè)時(shí)候,發(fā)送 slaveof onone 重新配置舊的主機(jī)。

就此,故障修復(fù)結(jié)束。故障修復(fù)為 Redis 集群很好的自適應(yīng)和自修復(fù)性。當(dāng)某主機(jī)因?yàn)楫惓;蛘咤礄C(jī)而不能提供服務(wù)的時(shí)候,故障修復(fù)還能讓 Redis 集群繼續(xù)提供服務(wù)。