本系列终于更新到哨兵模块的介绍,由于哨兵模块涉及节点通信和选举等流程,所以笔者将其分为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事件。
- 自此当前哨兵实例节点就开始监听主节点。