Redis 哨兵是如何完成初始化的
本系列終于更新到哨兵模塊的介紹,由于哨兵模塊涉及節(jié)點通信和選舉等流程,所以筆者將其分為3個篇章進行剖析,而本文筆者將從源碼分析的角度介紹一下redis哨兵是如何完成初始化的。
詳解哨兵初始化流程
1. 哨兵基本數(shù)據(jù)結(jié)構(gòu)
哨兵通過raft協(xié)議實現(xiàn)leader選舉和故障轉(zhuǎn)移線,針對這樣一個場景,我們的哨兵一般會使用單數(shù)個,為了保證選舉的正常進行哨兵還需要記錄節(jié)一次每次進行選舉的信息維護:
- 通過current_epoch記錄當(dāng)前選舉的紀(jì)元。
- 用masters指針?biāo)赶虻淖值渚S護當(dāng)前哨兵監(jiān)聽的master節(jié)點信息,每個master都會以sentinelRedisInstance結(jié)構(gòu)體進行信息維護各自的name、slave等信息。
- 通過announce_ip和announce_port用于和其他哨兵聯(lián)系時提供自身的地址信息。
對此我們給出sentinel 的結(jié)構(gòu)體代碼,讀者可參考上述的介紹了解一下每一個核心字段:
struct sentinelState {
//當(dāng)前紀(jì)元
uint64_t current_epoch; /* Current epoch. */
//維護主節(jié)點的哈希表指針
dict *masters; /* Dictionary of master sentinelRedisInstances.
//......
//向其他哨兵發(fā)送當(dāng)前實例的地址信息
char *announce_ip; /* IP addr that is gossiped to other sentinels if
not NULL. */
int announce_port; /* Port that is gossiped to other sentinels if
non zero. */
} sentinel;
2. 初始化哨兵基本配置
redis在啟動會檢查本次啟動是否是通過redis-sentinel指令或者--sentinel參數(shù)啟動哨兵,如果是則按照哨兵模式進行初始化,默認(rèn)給該節(jié)點端口號為26379并初始化哨兵sentinel:
對應(yīng)的我們給出核心代碼段,可以看到main方法啟動后會檢查是否是通過redis-sentinel或者參數(shù)--sentinel啟動,如果是則將sentinel_mode 設(shè)置為1,完成后續(xù)的配置和結(jié)構(gòu)體初始化:
int main(int argc, char **argv) {
//......
//檢查使用通過
server.sentinel_mode = checkForSentinelMode(argc,argv);
//......
if (server.sentinel_mode) {
initSentinelConfig();//初始化哨兵配置
initSentinel();//初始化哨兵結(jié)構(gòu)體
}
//......
}
我們步入initSentinelConfig方法可以看到配置初始化只做了一件事,即將端口號設(shè)置為26379:
void initSentinelConfig(void) {
//將端口號設(shè)置為26379
server.port = REDIS_SENTINEL_PORT;
}
我們再查看initSentinel這個初始化哨兵結(jié)構(gòu)體的函數(shù),可以看到其內(nèi)部會將當(dāng)前server執(zhí)行的命令表改為哨兵的命令,以及將所有IP、端口、masters指針進行初始化:
/* Perform the Sentinel mode initialization. */
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);
redisAssert(retval == DICT_OK);
}
//紀(jì)元初始化
sentinel.current_epoch = 0;
//masters指針初始化
sentinel.masters = dictCreate(&instancesDictType,NULL);
//......
//ip和端口號初始化
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
}
3. 初始化masters字典表
經(jīng)歷了上一步的初始化之后,redis就會開始解析redis.conf文件中解析出所有的master信息并存入masters中,假設(shè)我們在conf文件中鍵入如下配置:
# sentinel monitor <name> <host> <port> <quorum>
sentinel monitor masters-1 192.168.0.128 6379 1
redis就會從配置文件中匹配到sentinel 這個代碼段,然后解析出<name> <host> <port> <quorum>這幾個參數(shù),生成一個master即可sentinelRedisInstance對象,存入masters這個字典中:
我們給出讀取redis配置的核心代碼段
void loadServerConfigFromString(char *config) {
//......
for (i = 0; i < totlines; i++) {
sds *argv;
int argc;
linenum = i+1;
lines[i] = sdstrim(lines[i]," \t\r\n");
/* Skip comments and blank lines */
if (lines[i][0] == '#' || lines[i][0] == '\0') continue;
/* Split into arguments */
argv = sdssplitargs(lines[i],&argc);
if (argv == NULL) {
err = "Unbalanced quotes in configuration line";
goto loaderr;
}
/* Skip this line if the resulting command vector is empty. */
if (argc == 0) {
sdsfreesplitres(argv,argc);
continue;
}
sdstolower(argv[0]);
/* Execute config directives */
if (!strcasecmp(argv[0],"timeout") && argc == 2) {
//......
} else if (!strcasecmp(argv[0],"sentinel")) {//如果匹配到sentinel
//......
//解析參數(shù)生成master信息存入哨兵的masters字典表中
err = sentinelHandleConfiguration(argv+1,argc-1);
if (err) goto loaderr;
}
} //......
}
//......
}
我們再次步入sentinelHandleConfiguration可以看到大量配置參數(shù)解析的邏輯,流程比較簡單就是字符串處理,我們就以本次的監(jiān)聽主節(jié)點的命令monitor為例,當(dāng)redis解析到這個關(guān)鍵字則調(diào)用createSentinelRedisInstance解析出conf文件配置的master信息存入字典中:
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]);
if (quorum <= 0) return "Quorum must be 1 or greater.";
//解析出master信息存入字典中,可以看到傳入的標(biāo)識為SRI_MASTER,即當(dāng)前解析并監(jiān)視的對象是master節(jié)點
if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2],
atoi(argv[3]),quorum,NULL) == NULL)
{
//......
}
}
//......
}
最終我們步入createSentinelRedisInstance即可看到該方法通過與運算匹配出當(dāng)前傳入的信息是master的,于是拿到哨兵的masters字典表,完成master信息解析后將其存入字典中:
sentinelRedisInstance *createSentinelRedisInstance(char *name, int flags, char *hostname, int port, int quorum, sentinelRedisInstance *master) {
//......
//基于與運算獲得哨兵的masters表
if (flags & SRI_MASTER) table = sentinel.masters;
else if (flags & SRI_SLAVE) table = master->slaves;
else if (flags & SRI_SENTINEL) table = master->sentinels;
//......
//創(chuàng)建master實例
ri = zmalloc(sizeof(*ri));
//......
ri->name = sdsname;
//......
//存入哨兵的字典表masters中
dictAdd(table, ri->name, ri);
return ri;
}
4. 啟動并監(jiān)聽master
完成上述步驟后,redis得知當(dāng)前節(jié)點是以哨兵模式啟動,于是調(diào)用sentinelIsRunning方法,內(nèi)部遍歷masters節(jié)點的信息,發(fā)送到monitor頻道告知其他當(dāng)前哨兵監(jiān)聽的所有monitor信息
我們從入口看起,可以看到main方法后續(xù)會判斷如果是哨兵模式則執(zhí)行sentinelIsRunning:
if (!server.sentinel_mode) {
//......
} else {//如果是哨兵模式則如此啟動哨兵
sentinelIsRunning();
}
其內(nèi)部調(diào)用sentinelGenerateInitialMonitorEvents遍歷masters表的信息將master發(fā)布到monitor頻道上:
void sentinelIsRunning(void) {
//......
//獲取masters迭代器對所有主節(jié)點設(shè)置monitor
sentinelGenerateInitialMonitorEvents();
}
查看sentinelGenerateInitialMonitorEvents邏輯就是遍歷masters表獲取master信息調(diào)用sentinelEvent向主節(jié)點master的monitor頻道上發(fā)布消息告知當(dāng)前哨兵開始監(jiān)控:
void sentinelGenerateInitialMonitorEvents(void) {
dictIterator *di;
dictEntry *de;
di = dictGetIterator(sentinel.masters);
//遍歷master節(jié)點
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
//發(fā)布監(jiān)聽事件
sentinelEvent(REDIS_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
}
dictReleaseIterator(di);
}
小結(jié)
我們簡單小結(jié)一下redis哨兵的啟動步驟:
- redis-server感知到啟動模式為哨兵模式,則按照哨兵模式進行實例初始化。
- 加載哨兵模式支持的操作指令。
- 解析redis.conf配置中所有master信息存儲到哨兵實例結(jié)構(gòu)體的masters字典中。
- 遍歷所有需要監(jiān)控的master,向這些master的monitor頻道發(fā)布monitor事件。
- 自此當(dāng)前哨兵實例節(jié)點就開始監(jiān)聽主節(jié)點。