哨兵初始化

96 阅读5分钟

哨兵实例的初始化

哨兵实例的初始化入口函数也是 main(在 server.c 文件中)。

main 函数在调用 initServerConfig 函数初始化各种配置项之前,会调用 checkForSentinelMode 函数,来判断当前运行的是否为哨兵实例,如下所示:

server.sentinel_mode = checkForSentinelMode(argc,argv);

它会根据以下两个条件判断当前是否运行了哨兵实例。条件一:执行的命令本身,也就是 argv[0],是否为“redis-sentinel”。条件二:执行的命令参数中,是否有“–sentinel”。

int checkForSentinelMode(int argc, char **argv) {
    int j
    //第一个判断条件,判断执行命令本身是否为redis-sentinel
    if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
    for (j = 1; j < argc; j++)
        //第二个判断条件,判断命令参数是否有"--sentienl"
        if (!strcmp(argv[j],"--sentinel")) return 1;
    return 0;
}

初始化配置项

main 函数会专门调用 initSentinelConfig 和 initSentinel 两个函数,来完成哨兵实例专门的配置项初始化。

initSentinelConfig 函数主要是将当前 server 的端口号,改为哨兵实例专用的端口号 REDIS_SENTINEL_PORT。这是个宏定义,它对应的默认值是 26379。另外,这个函数还会把 server 的 protected_mode 设置为 0,即允许外部连接哨兵实例,而不是只能通过 127.0.0.1 本地连接 server。

而 initSentinel 函数则是在 initSentinelConfig 函数的基础上,进一步完成哨兵实例的初始化,这其中主要包括两部分工作。

  • 首先,initSentinel 函数会替换 server 能执行的命令表。
  • 其次,initSentinel 函数在替换了命令表后,紧接着它会开始初始化哨兵实例用到的各种属性信息。

为了保存这些属性信息,哨兵实例定义了 sentinelState 结构体(在 sentinel.c 文件中),这其中包括了哨兵实例的 ID、用于故障切换的当前纪元、监听的主节点、正在执行的脚本数量,以及与其他哨兵实例发送的 IP 和端口号等信息。

struct sentinelState {
    char myid[CONFIG_RUN_ID_SIZE+1];  //哨兵实例ID
    uint64_t current_epoch;         //当前纪元
    dict *masters;      //监听的主节点的哈希表
    int tilt;           //是否处于TILT模式
    int running_scripts;    //运行的脚本个数
    mstime_t tilt_start_time;  //tilt模式的起始时间
    mstime_t previous_time;     //上一次执行时间处理函数的时间
    list *scripts_queue;         //用于保存脚本的队列
    char *announce_ip;  //向其他哨兵实例发送的IP信息
    int announce_port;  //向其他哨兵实例发送的端口号
    …
} sentinel;

接下来,main 函数还会调用 initServer 函数完成 server 本身的初始化操作,这部分哨兵实例也是会执行的。然后,main 函数就会调用 sentinelIsRunning 函数(在 sentinel.c 文件中)启动哨兵实例。

启动哨兵实例

sentinelIsRunning 函数的执行逻辑比较简单,它首先会确认哨兵实例的配置文件存在并且可以正常写入。

然后,它会检查哨兵实例是否设置了 ID。如果没有设置 ID 的话,sentinelIsRunning 函数就会为哨兵实例随机生成一个 ID。

最后,sentinelIsRunning 函数会调用 sentinelGenerateInitialMonitorEvents 函数(在 sentinel.c 文件中),给每个被监听的主节点发送事件信息。

initSentinel 函数,它会初始化哨兵实例的数据结构 sentinel.masters。这个结构是使用了一个哈希表记录监听的主节点,每个主节点会使用 sentinelRedisInstance 结构来保存。

sentinelRedisInstance 是一个通用的结构体,它不仅可以表示主节点,也可以表示从节点或者其他的哨兵实例。这个结构体的成员变量有一个 flags,它可以设置为不同的值,SRI_MASTER、SRI_SLAVE 或 SRI_SENTINEL 这三种宏定义(在 sentinel.c 文件中)时,就分别表示当前实例是主节点、从节点或其他哨兵。

void sentinelGenerateInitialMonitorEvents(void) {
    dictIterator *di;
    dictEntry *de;

    di = dictGetIterator(sentinel.masters);//获取masters的迭代器
    while((de = dictNext(di)) != NULL) { //获取被监听的主节点
        sentinelRedisInstance *ri = dictGetVal(de);
        sentinelEvent(LL_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);   //发送+monitor事件
    }
    dictReleaseIterator(di);
}

sentinelGenerateInitialMonitorEvents 函数是调用 sentinelEvent 函数(在 sentinel.c 文件中)来实际发送事件信息的。

sentinelEvent 函数的原型定义如下,它的参数 level 表示当前的日志级别,type 表示发送事件信息所用的订阅频道,ri 表示对应交互的主节点,fmt 则表示发送的消息内容。

void sentinelEvent(int level, char *type, sentinelRedisInstance *ri, const char *fmt, ...)

sentinelEvent 函数会先判断传入的消息内容开头的两个字符,是否为“%”和“@”,如果是的话,它就会判断监听实例的类型是否为主节点。然后如果是主节点,sentinelEvent 函数会把监听实例的名称、IP 和端口号加入到待发送的消息中,如下所示:

...
//如果传递消息以"%"和"@"开头,就判断实例是否为主节点
if (fmt[0] == '%' && fmt[1] == '@') {
   //判断实例的flags标签是否为SRI_MASTER,如果是,就表明实例是主节点
   sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ?
                                         NULL : ri->master;
   //如果当前实例是主节点,根据实例的名称、IP地址、端口号等信息调用snprintf生成传递的消息msg
   if (master) {
      snprintf(msg, sizeof(msg), "%s %s %s %d @ %s %s %d", sentinelRedisInstanceTypeStr(ri), ri->name, ri->addr->ip, ri->addr->port,
                master->name, master->addr->ip, master->addr->port);
  }
        ...
}
...

然后,sentinelEvent 函数会把传入的消息中,除了开头两个字符以外的剩余内容加入到待发送的消息中。最后,sentinelEvent 函数会调用 pubsubPublishMessage 函数(在 pubsub.c 文件中),将消息发送到对应的频道中,如下所示:

if (level != LL_DEBUG) {
        channel = createStringObject(type,strlen(type));
        payload = createStringObject(msg,strlen(msg));
        pubsubPublishMessage(channel,payload);
        ...
  }

此文章为10月Day22学习笔记,内容来源于极客时间《Redis 源码剖析与实战》