Redis 哨兵是如何完成初始化的
本系列終于更新到哨兵模塊的介紹,由于哨兵模塊涉及節點通信和選舉等流程,所以筆者將其分為3個篇章進行剖析,而本文筆者將從源碼分析的角度介紹一下redis哨兵是如何完成初始化的。
詳解哨兵初始化流程
1. 哨兵基本數據結構
哨兵通過raft協議實現leader選舉和故障轉移線,針對這樣一個場景,我們的哨兵一般會使用單數個,為了保證選舉的正常進行哨兵還需要記錄節一次每次進行選舉的信息維護:
- 通過current_epoch記錄當前選舉的紀元。
- 用masters指針所指向的字典維護當前哨兵監聽的master節點信息,每個master都會以sentinelRedisInstance結構體進行信息維護各自的name、slave等信息。
- 通過announce_ip和announce_port用于和其他哨兵聯系時提供自身的地址信息。
對此我們給出sentinel 的結構體代碼,讀者可參考上述的介紹了解一下每一個核心字段:
struct sentinelState {
//當前紀元
uint64_t current_epoch; /* Current epoch. */
//維護主節點的哈希表指針
dict *masters; /* Dictionary of master sentinelRedisInstances.
//......
//向其他哨兵發送當前實例的地址信息
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參數啟動哨兵,如果是則按照哨兵模式進行初始化,默認給該節點端口號為26379并初始化哨兵sentinel:
對應的我們給出核心代碼段,可以看到main方法啟動后會檢查是否是通過redis-sentinel或者參數--sentinel啟動,如果是則將sentinel_mode 設置為1,完成后續的配置和結構體初始化:
int main(int argc, char **argv) {
//......
//檢查使用通過
server.sentinel_mode = checkForSentinelMode(argc,argv);
//......
if (server.sentinel_mode) {
initSentinelConfig();//初始化哨兵配置
initSentinel();//初始化哨兵結構體
}
//......
}
我們步入initSentinelConfig方法可以看到配置初始化只做了一件事,即將端口號設置為26379:
void initSentinelConfig(void) {
//將端口號設置為26379
server.port = REDIS_SENTINEL_PORT;
}
我們再查看initSentinel這個初始化哨兵結構體的函數,可以看到其內部會將當前server執行的命令表改為哨兵的命令,以及將所有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);
}
//紀元初始化
sentinel.current_epoch = 0;
//masters指針初始化
sentinel.masters = dictCreate(&instancesDictType,NULL);
//......
//ip和端口號初始化
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
}
3. 初始化masters字典表
經歷了上一步的初始化之后,redis就會開始解析redis.conf文件中解析出所有的master信息并存入masters中,假設我們在conf文件中鍵入如下配置:
# sentinel monitor <name> <host> <port> <quorum>
sentinel monitor masters-1 192.168.0.128 6379 1
redis就會從配置文件中匹配到sentinel 這個代碼段,然后解析出<name> <host> <port> <quorum>這幾個參數,生成一個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
//......
//解析參數生成master信息存入哨兵的masters字典表中
err = sentinelHandleConfiguration(argv+1,argc-1);
if (err) goto loaderr;
}
} //......
}
//......
}
我們再次步入sentinelHandleConfiguration可以看到大量配置參數解析的邏輯,流程比較簡單就是字符串處理,我們就以本次的監聽主節點的命令monitor為例,當redis解析到這個關鍵字則調用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信息存入字典中,可以看到傳入的標識為SRI_MASTER,即當前解析并監視的對象是master節點
if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2],
atoi(argv[3]),quorum,NULL) == NULL)
{
//......
}
}
//......
}
最終我們步入createSentinelRedisInstance即可看到該方法通過與運算匹配出當前傳入的信息是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;
//......
//創建master實例
ri = zmalloc(sizeof(*ri));
//......
ri->name = sdsname;
//......
//存入哨兵的字典表masters中
dictAdd(table, ri->name, ri);
return ri;
}
4. 啟動并監聽master
完成上述步驟后,redis得知當前節點是以哨兵模式啟動,于是調用sentinelIsRunning方法,內部遍歷masters節點的信息,發送到monitor頻道告知其他當前哨兵監聽的所有monitor信息
我們從入口看起,可以看到main方法后續會判斷如果是哨兵模式則執行sentinelIsRunning:
if (!server.sentinel_mode) {
//......
} else {//如果是哨兵模式則如此啟動哨兵
sentinelIsRunning();
}
其內部調用sentinelGenerateInitialMonitorEvents遍歷masters表的信息將master發布到monitor頻道上:
void sentinelIsRunning(void) {
//......
//獲取masters迭代器對所有主節點設置monitor
sentinelGenerateInitialMonitorEvents();
}
查看sentinelGenerateInitialMonitorEvents邏輯就是遍歷masters表獲取master信息調用sentinelEvent向主節點master的monitor頻道上發布消息告知當前哨兵開始監控:
void sentinelGenerateInitialMonitorEvents(void) {
dictIterator *di;
dictEntry *de;
di = dictGetIterator(sentinel.masters);
//遍歷master節點
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
//發布監聽事件
sentinelEvent(REDIS_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
}
dictReleaseIterator(di);
}
小結
我們簡單小結一下redis哨兵的啟動步驟:
- redis-server感知到啟動模式為哨兵模式,則按照哨兵模式進行實例初始化。
- 加載哨兵模式支持的操作指令。
- 解析redis.conf配置中所有master信息存儲到哨兵實例結構體的masters字典中。
- 遍歷所有需要監控的master,向這些master的monitor頻道發布monitor事件。
- 自此當前哨兵實例節點就開始監聽主節點。