likes
comments
collection
share

详解 Redis 哨兵模式(一)

作者站长头像
站长
· 阅读数 47

哨兵模式是提高 Redis 可用性的一种方式,本文是哨兵模式系列的第一篇文章,主要介绍哨兵模式的启动过程。

哨兵其实是 Redis Sever 的一种特殊模式。哨兵实例的初始化入口也是 mian。下面我们来分析哨兵模式下的启动过程。

哨兵实例初始化

Redis 首先会调用 checkForSentinelMode 函数来判断当前是否以哨兵模式来运行,并把标识赋值到 server.sentinel_mode

server.sentinel_mode = checkForSentinelMode(argc,argv);

我们来看一下 checkForSentinelMode 是如何判断当前是否以哨兵模式来运行:

int checkForSentinelMode(int argc, char **argv) {
    int j;
    // 判断第一个参数是不是 redis-sentinel
    if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
    // 判断其他的参数是不是 --sentinel
    for (j = 1; j < argc; j++)
        if (!strcmp(argv[j],"--sentinel")) return 1;
    return 0;
}

可以看出它是通过两个条件来判断的:

  • 执行的命令是否为 redis-sentinel
  • 命令参数中是否含有 --sentinel

这就对应了我们在命令行中启动哨兵实例的两种方式,一是直接运行 redis-sentinel 命令,另一种是运行 redis-server 命令并且参数中含有 --sentinel 参数。如下所示:

redis-sentinel sentinel.conf⽂件路径 
redis-server sentinel.conf⽂件路径  --sentinel

当这两个条件满足一个时就会被认为是以哨兵模式运行当前实例,这时会将 server.sentinel_mode 的值设置为 1,这个值会被用来判断当前实例是否以是否以哨兵模式运行。

初始化配置项

根据上一步中 server.sentinel_mode 的值判断当前是否以哨兵模式初始化。

    // 是否是哨兵模式
    if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }

我们看到当为哨兵模式时会调用 initSentinelConfiginitSentinel 函数来完成哨兵的初始化。

initSentinelConfig 函数主要把将 server 的端口号改为 REDIS_SENTINEL_PORT,这个宏定义的值为 26379;然后把 server.protected_mode 改为 0,表示允许外部连接哨兵实例。

void initSentinelConfig(void) {
    server.port = REDIS_SENTINEL_PORT;
    server.protected_mode = 0; /* Sentinel must be exposed. */
}

initSentinel 函数主要做了两部分工作。

首先是替换 server 执⾏的命令表。因为哨兵是 Redis 的一种特殊模式,它所能执行的命令和 Redis 实例是有所区别的,哨兵实例执行的⼀些命令,其名称虽然和 Redis 实例命令表中的命令名称⼀样,但它们的实现函数是针对哨兵实例专门实现的。

void initSentinel(void) {
    ...
    // 清空当前命令表
    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);
        serverAssert(retval == DICT_OK);
    }
   ...
}
// 哨兵模式命令表
struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0}
};

其次 initSentinel 函数会初始化哨兵实例的各种属性信息。主要是初始化 sentinelState 结构体里面的数据。我们来看一下 sentinelState 有哪些数据:


struct sentinelState {
    // 哨兵实例 ID
    char myid[CONFIG_RUN_ID_SIZE+1]; /* This sentinel ID. */
    // 当前纪元
    uint64_t current_epoch;         /* Current epoch. */
    // 监听的所有主节点的哈希表
    dict *masters;      /* Dictionary of master sentinelRedisInstances.
                           Key is the instance name, value is the
                           sentinelRedisInstance structure pointer. */
    // 是否处于 TILT 模式
    int tilt;           /* Are we in TILT mode? */
    // 正在执行的脚本数量
    int running_scripts;    /* Number of scripts in execution right now. */
    // TILT 模式的开始时间
    mstime_t tilt_start_time;       /* When TITL started. */
    // 上⼀次执⾏时间处理函数的时间
    mstime_t previous_time;         /* Last time we ran the time handler. */
    // 保存脚本的队列
    list *scripts_queue;            /* Queue of user scripts to execute. */
    // 向其他哨兵实例发送的 IP 地址
    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;

启动哨兵实例

哨兵初始化之后就会调用 sentinelIsRunning 函数来启动哨兵实例。

if (!server.sentinel_mode) {
    ...
} else {
    InitServerLast();
    
    sentinelIsRunning();
}

sentinelIsRunning 函数主要的逻辑是首先校验哨兵的配置文件是否存在且可以正常写入;其次会校验哨兵实例是否设置了 ID,如果没有设置则会随机生成一个 ID;最后会调用 sentinelGenerateInitialMonitorEvents 函数给每个监听的主节点发送事件消息。

void sentinelIsRunning(void) {
    int j;
    // 校验配置文件
    if (server.configfile == NULL) {
        serverLog(LL_WARNING,
            "Sentinel started without a config file. Exiting...");
        exit(1);
    } else if (access(server.configfile,W_OK) == -1) {
        serverLog(LL_WARNING,
            "Sentinel config file %s is not writable: %s. Exiting...",
            server.configfile,strerror(errno));
        exit(1);
    }

    // 如果没有 ID 则会随机生成一个
    for (j = 0; j < CONFIG_RUN_ID_SIZE; j++)
        if (sentinel.myid[j] != 0) break;

    if (j == CONFIG_RUN_ID_SIZE) {
        /* Pick ID and persist the config. */
        getRandomHexChars(sentinel.myid,CONFIG_RUN_ID_SIZE);
        sentinelFlushConfig();
    }

    ...

    // 向 +monitor 频道发送事件
    sentinelGenerateInitialMonitorEvents();
}

我们来看一下 sentinelGenerateInitialMonitorEvents 是怎样发送事件的:

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

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

通过上面的代码,我们看到 sentinelGenerateInitialMonitorEvents 会获取获取主节点的列表,然后调用 sentinelEvent 对每个主节点发送 +monitor 事件。我们来看一下 sentinelEvent 函数的定义:

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

level 表示当前的日志级别,type 表示发送事件信息所用的订阅频道,ri 表示交互的主节点,fmt 表示发送的消息内容。我们来看一下 sentinelEvent 的逻辑:

void sentinelEvent(int level, char *type, sentinelRedisInstance *ri,
                   const char *fmt, ...) {
    // 判断是否以 %@ 开头
    if (fmt[0] == '%' && fmt[1] == '@') {
        sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ?
                                         NULL : ri->master;
        // 如果是和主节点交互则根据实例的名称、IP地址、端⼝号等信息生成传递的消息
        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);
        } else {
            snprintf(msg, sizeof(msg), "%s %s %s %d",
                sentinelRedisInstanceTypeStr(ri),
                ri->name, ri->addr->ip, ri->addr->port);
        }
        fmt += 2;
    } else {
        msg[0] = '\0';
    }
    ...
    
    if (level != LL_DEBUG) {
        channel = createStringObject(type,strlen(type));
        payload = createStringObject(msg,strlen(msg));
        // 发送消息
        pubsubPublishMessage(channel,payload);
        decrRefCount(channel);
        decrRefCount(payload);
    }
    ...
}

sentinelEvent 函数会先判断传⼊的消息内容开头是否为 %@,如果是的话它就会判断监听实例的类型是否为主节点。如果是主节点 sentinelEvent 函数会把监听实例的名称、IP 和端⼝号加⼊到待发送的消息中。如果当前的日志级别不是 LL_DEBUG 则会调用 pubsubPublishMessage 函数来真正的发送消息。

加载配置项

大家可能有个疑问,sentinel.masters 即哨兵的主节点是什么时候赋值的呢,其实就是在加载配置文件的时候。哨兵的加载配置文件也是通过 loadServerConfig 函数,loadServerConfig 调用的 loadServerConfigFromString 函数中有一个分支,它会判断当前是否为哨兵的配置项,如果是则会调用 sentinelHandleConfiguration 函数来解析当前配置。

    if (!strcasecmp(argv[0],"sentinel")) {
        /* argc == 1 is handled by main() as we need to enter the sentinel
         * mode ASAP. */
        if (argc != 1) {
            if (!server.sentinel_mode) {
                err = "sentinel directive while not in sentinel mode";
                goto loaderr;
            }
            // 解析哨兵配置
            err = sentinelHandleConfiguration(argv+1,argc-1);
            if (err) goto loaderr;
        }
    }

sentinelHandleConfiguration 解析 sentinel monitor <name> <host> <port> <quorum> 配置时,会根据这些配置信息调用 createSentinelRedisInstance 函数来创建主节点的实例信息。

char *sentinelHandleConfiguration(char **argv, int argc) {
    ...
     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.";
        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";
            }
        }
    }
    ...
}
转载自:https://juejin.cn/post/7176470596975345723
评论
请登录