哨兵也是 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ù)開始,來看看哨兵服務(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());
// 通過命令行參數(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;
}
在上面,通過判斷命令行參數(shù)來判斷 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 模式開始的時(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í)程序是哨兵服務(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ù)量沒有超出限定
sentinelRunPendingScripts();
// 清理已經(jīng)執(zhí)行完腳本的進(jìn)程,如果執(zhí)行成功從腳本隊(duì)列中刪除腳本
sentinelCollectTerminatedScripts();
// 停止執(zhí)行時(shí)間超時(shí)的腳本進(jìn)程
sentinelKillTimedoutScripts();
// 為了防止多個(gè)哨兵同時(shí)選舉,故意錯(cuò)開定時(shí)程序執(zhí)行的時(shí)間。通過調(diào)整周期可以
// 調(diào)整哨兵定時(shí)程序執(zhí)行的時(shí)間,即默認(rèn)值REDIS_DEFAULT_HZ 加上一個(gè)任意值
server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}
每個(gè)哨兵都有一個(gè) struct sentinel 結(jié)構(gòu)體,里面維護(hù)了多個(gè)主機(jī)的連接,與每個(gè)主機(jī)連接的相關(guān)信息都存儲(chǔ)在 struct sentinelRedisInstance。透過這兩個(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="" />
可見,哨兵服務(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 是哨兵用來判斷某個(gè) Redis 服務(wù)器是否下線的參數(shù),之后會(huì)講到。sentinelHandleConfiguration() 函數(shù)中,完成了對(duì)配置文件的解析和處理過程。
// 哨兵配置文件解析和處理
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)體。在這里,哨兵并沒有選擇立即去連接這指定的 Redis 服務(wù)器,而是將 sentinelRedisInstance.flag 標(biāo)記 SRI_DISCONNECT,而將連接的工作丟到定時(shí)程序中去,可以聯(lián)想到,定時(shí)程序中肯定有一個(gè)檢測(cè) sentinelRedisInstance.flag 的函數(shù),如果發(fā)現(xiàn)連接是斷開的,會(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ù)器的過程:
sentinelTimer()->sentinelHandleRedisInstance()->sentinelReconnectInstance()
。
sentinelReconnectInstance() 負(fù)責(zé)連接被標(biāo)記為 SRI_DISCONNECT 的 Redis 服務(wù)器。它對(duì)一個(gè) Redis 服務(wù)器發(fā)起了兩個(gè)連接:
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è)疑問,之前有提到從機(jī)(slave),哨兵又是在什么時(shí)候連接了從機(jī)和哨兵呢?
我們從上面 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="" />
從源碼來看,哨兵 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ì)有疑問,為什么不直接發(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ù)器收到來自哨兵的數(shù)據(jù)時(shí)候,會(huì)向所有訂閱 hello 頻道的哨兵發(fā)布數(shù)據(jù),由此剛才注冊(cè)的回調(diào)函數(shù)sentinelReceiveHelloMessages() 就被調(diào)用了?;卣{(diào)函數(shù) sentinelReceiveHelloMessages() 做了兩件事情:
總結(jié)一下這里的工作原理,哨兵會(huì)向 hello 頻道發(fā)送包括:哨兵自己的IP 地址和端口,runid,當(dāng)前的配置版本;其所監(jiān)視主機(jī)的 IP 地址,端口,當(dāng)前的配置版本?!具@里要說清楚,什么是 runid 和配置版本】雖然未知的信息很多,但我們可以得知,當(dāng)一個(gè)哨兵新加入到一個(gè) Redis 集群中時(shí),就能通過 hello 頻道,發(fā)現(xiàn)其他更多的哨兵,而它自己也能夠被其他的哨兵發(fā)現(xiàn)。這是 Redis 所謂 auto discover 的一部分。
同樣,在定時(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ù)器收集了這些信息回傳給了哨兵,剛才所說哨兵的回調(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è)到心跳包過久未收到(即超時(shí)),這證明網(wǎng)絡(luò)環(huán)境不好,或者對(duì)方很忙,也為接收方接下來的行動(dòng)提供指導(dǎo):接收方可以等待心跳正常的時(shí)候再發(fā)送數(shù)據(jù)。在哨兵的定時(shí)程序中,哨兵會(huì)向所有的服務(wù)器,包括哨兵服務(wù)器,發(fā)送 PING 心跳,而哨兵收到來自 Redis 服務(wù)器的回應(yīng)后,也會(huì)更新相應(yīng)的時(shí)間點(diǎn)或者執(zhí)行其他操作。
http://wiki.jikexueyuan.com/project/redis/images/a3.png" alt="" />
哨兵有兩種判斷用戶在線的方法,主觀和客觀方法,即 Check Subjectively Down 和 Check Objective Down。主觀是說,Redis 服務(wù)器的在線判斷依據(jù)是某個(gè)哨兵自己的信息;客觀是說,Redis 服務(wù)器的在線判斷依據(jù)是由其他監(jiān)視此 Redis 服務(wù)器的哨兵的信息。
http://wiki.jikexueyuan.com/project/redis/images/a4.png" alt="" />
哨兵憑借的自己的信息判斷 Redis 服務(wù)器是否下線的方法,稱為主觀方法,即通過判斷前面有提到的 PING 心跳等其他通信時(shí)間是否超時(shí)來判斷主機(jī)是否下線。主觀的信息有可能是錯(cuò)的。
http://wiki.jikexueyuan.com/project/redis/images/a5.png" alt="" />
哨兵不僅僅憑借自己的信息,還依據(jù)其他哨兵提供的信息判斷 Redis 服務(wù)器是否下線的方法稱為客觀方法,即通過所有其他哨兵報(bào)告的主機(jī)在線狀態(tài)來判定某主機(jī)是否下線。前面提到,INFO 命令可以從其他哨兵服務(wù)器上獲取信息,而這里面的信息就包含了他們共同關(guān)注主機(jī)的在線狀態(tài)。客觀判斷方法是基于主觀判斷方法的,即如果一個(gè) Redis 服務(wù)器被客觀判定為下線,那么其早已被主觀判斷為下線了。因此客觀判斷的在線狀態(tài)較有說服力,譬如在故障修復(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;
}
}
}
一個(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è)條件:
三個(gè)條件都得到滿足,故障修復(fù)就開始了。
繼續(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;
}
}
在哨兵服務(wù)器群中,有首領(lǐng)(leader)的概念,這個(gè)首領(lǐng)可以是系統(tǒng)管理員根據(jù)具體情況指定的,也可以是眾多的哨兵中按一定的條件選出的。在 WAIT_STATE 中執(zhí)行故障修復(fù)的哨兵首先確定自己是不是首領(lǐng),如果不是故障修復(fù)會(huì)被拖延,到下一個(gè)定時(shí)程序再次檢測(cè)自己是否為首領(lǐng),超過一定時(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ì)通過 is-master-down 命令交換。is-master-down 命令在故障修復(fù)的時(shí)候會(huì)被強(qiáng)制觸發(fā),收到它的哨兵將會(huì)進(jìn)行投票并返回自己的投票結(jié)果,哨兵會(huì)將它保存在對(duì)應(yīng)的 sentinelRedisInstance 中。如此一來,執(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ì)的投票過程。
總結(jié)了一下選舉首領(lǐng)的過程:
http://wiki.jikexueyuan.com/project/redis/images/b1.png" alt="" />
是一個(gè)比較曲折的過程。最終,如果確定當(dāng)前執(zhí)行故障修復(fù)的哨兵是首領(lǐng),它則可以進(jìn)入下一個(gè)狀態(tài):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ì)的選舉過程。
當(dāng)前執(zhí)行故障修復(fù)的哨兵會(huì)遍歷主機(jī)的所有從機(jī),只有足夠健康的從機(jī)才能被成為候選主機(jī)。足夠健康的條件包括:
滿足以上條件就有機(jī)會(huì)成為候選主機(jī),如果經(jīng)過上面的篩選之后有多臺(tái)從機(jī),那么這些從機(jī)會(huì)按下面的條件排序:
所選用的排序算法是常用的快排。這是一個(gè)比較曲折的過程。如果沒有從機(jī)符合要求,譬如最極端的情況,所有從機(jī)都跟著掛了,那么故障修復(fù)會(huì)失?。环駝t最終會(huì)確定一個(gè)從機(jī)成為候選主機(jī)。從機(jī)可以進(jìn)入下一個(gè)狀態(tài):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ù)沒有超時(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="" />
這一狀態(tài)純粹是為了等待上一個(gè)狀態(tài)的執(zhí)行結(jié)果(如候選主機(jī)的一些狀態(tài))被傳播到此哨兵上,至于是如何傳播的,之前我們有提到過 INFO 數(shù)據(jù)傳輸?shù)倪^程。這一狀態(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。
這是故障修復(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í)間),配置完成。同樣,哨兵是通過 INFO 數(shù)據(jù)傳輸中獲知這些狀態(tài)變更的。
http://wiki.jikexueyuan.com/project/redis/images/b3.png" alt="" />
詳細(xì)重新配置過程可以在
sentinelTimer()-?sentinelHandleDictOfRedisInstances()-
?sentinelHandleRedisInstance()-?sentinelFailoverStateMachine()-
?sentinelFailoverReconfNextSlave()-?sentinelSelectSlave()
最后會(huì)做從機(jī)配置狀況的檢測(cè),如果所有從機(jī)都重新配置完成或者超時(shí)了,會(huì)進(jìn)入最后一個(gè)狀態(tài) 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è)問題:故障修復(fù)過程中,一直沒有發(fā)送 SLAVEOF promoted_slave 給舊的主機(jī),因?yàn)橐呀?jīng)和舊的主機(jī)斷開連接,哨兵沒有選擇在故障修復(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ù)過來,只是這時(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ù)。