redis 源码系列(18):和单点说再见 --- sentinel

有了在主从节点之间同步数据的解决方案之后,我们已经有了运行多个 redis 服务的能力,但是我们仍然缺乏自动处理故障转移的能力。sentinel 是 redis 自带的高可用解决方案。在这个方案中有三种不同角色:

  • master
  • slave
  • sentinel

主从节点的作用不需赘述,他们是数据库服务的直接提供者。sentinel 则主要有以下作用:

  • monitoring,sentinel 会持续的监控整个集群的情况
  • notification,sentinel 在集群出现故障的时候,可以通过 API 通知集群管理员,在自动故障转移无法进行时,快速人工干预
  • automatic failover,sentinel 检测到主节点情况异常时,可以自动进行故障转移,将某个从节点提升为主节点,并且将集群的新状态下发到集群内的节点
  • configuration provider,启用了 sentinel 部署模式的 redis 服务器,在 client 端也应该做相应适配,将 sentinel 作为一个配置提供者,通过 sentinel 获取集群主节点的信息以后再去请求主节点,同时需要处理 sentinel 在故障转移后的通知

sentinel overview

sentinel 是为了解决 redis 分布式部署下的高可用问题,这个就要求 sentinel 本身是高可用的 — 它本身就需要集群部署。事实上考虑到 sentinel 处理故障转移的工作方式,应该最少部署 3 个独立 sentinel 实例。

启动 sentinel

使用如下命令,即可启动一个 sentinel 节点:

redis-server /path/to/sentinel.conf --sentinel
# 或者使用 redis-sentinel 可执行启动
redis-sentinel /path/to/sentinel.conf

注意,sentinel 启动时,必须要传入配置文件,因为 sentinel 还会将集群配置存储在配置文件内,防止 sentinel 重启时丢失集群信息。

sentinel 默认监听的端口是 26379,并且其支持的命令很少,这一点与从节点不同。

sentinel 配置

# sentinel monitor <master-group-name> <ip> <port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5

master、salve、sentinel 加起来共同组成一个 redis 高可用集群。sentinel 节点可以监控多个 master ,但是不需要指定监听某 master 的从节点,sentinel 会自动发现其从节点,并且在集群信息发生变化时,将配置更新到本地的配置文件当中。

在指定监控 master 的指令中,最后一个 quorum 的参数需要特别解释一下:

  • 必须当最少 quorum 个 sentinel 实例观察到主节点处于异常(不管是网络分区还是宕机),才可以认为这个主节点已经处于故障,进行故障转移
  • quorum 只是指定侦查到故障的阈值,要想真的发起一次故障转移,则需要最少半数以上的 sentinel 节点认为主节点已经处于异常

举例来说,假设有 5 个 sentinel 节点,对于指定的监控主节点,quorum 设置为 2,那么:

  • 如果两个 sentinel 节点在同一时刻发现主节点不可达,那么其中一个 sentinel 节点会尝试发起一次故障转移
  • 但是除非一共有 3 个或以上的 sentinel 认为主节点不可达,否则这个故障转移不会被同意进行

这个机制保证了,故障转移不可能出现在网络分区的小分区内(小仅针对 sentinel 节点而言)。

故障状态

sentinel 对集群内的主节点故障分为两种状态:

  • SDOWN(subjective down),这个代表本 sentinel 节点观察到节点处于故障状态,是一个局部状态
  • ODOWN(objective down),需要最少 quorum 个 sentinel 节点观察到主节点处于故障状态,主节点处于 ODOWN 状态会触发 failover

automatic discover

通过利用 redis 的 PUB/SUB 机制,我们 sentinel 可以自动发现集群中的从节点和 sentinel 节点。

实现

instance

sentinel 模式下,redis 定义了一个 sentinelRedisInstance 结构体来表示 master、salve 和 sentinel 三种角色,充分理解这个结构体个字段的含义是我们理解 sentinel 的前提:

typedef struct sentinelRedisInstance {
    // 标识值,记录了实例的类型,以及该实例的当前状态
    int flags;      /* See SRI_... defines */
    // 实例的名字
    // 主服务器的名字由用户在配置文件中设置
    // 从服务器以及 Sentinel 的名字由 Sentinel 自动设置
    // 格式为 ip:port ,例如 "127.0.0.1:26379"
    char *name;     /* Master name from the point of view of this sentinel. */=
    // 实例的运行 ID
    char *runid;    /* run ID of this instance. */
    // 配置纪元,用于实现故障转移
    uint64_t config_epoch;  /* Configuration epoch. */
    // 实例的地址
    sentinelAddr *addr; /* Master host. */
    // 用于发送命令的异步连接,因为代码中涉及到了大量发送命令的代码
    // redis sentinel 复用了 hiredis(redis c client) 中的代码
    redisAsyncContext *cc; /* Hiredis context for commands. */
    // 用于执行 SUBSCRIBE 命令、接收频道信息的异步连接
    redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */
    // 已发送但尚未回复的命令数量
    int pending_commands;   /* Number of commands sent waiting for a reply. */
    // cc 连接的创建时间
    mstime_t cc_conn_time; /* cc connection time. */
    // pc 连接的创建时间
    mstime_t pc_conn_time; /* pc connection time. */
    // 最后一次从这个实例接收信息的时间
    mstime_t pc_last_activity; /* Last time we received any message. */
    // 实例最后一次返回正确的 PING 命令回复的时间
    mstime_t last_avail_time; /* Last time the instance replied to ping with
                                 a reply we consider valid. */
    // 实例最后一次发送 PING 命令的时间
    mstime_t last_ping_time;  /* Last time a pending ping was sent in the
                                 context of the current command connection
                                 with the instance. 0 if still not sent or
                                 if pong already received. */
    // 实例最后一次返回 PING 命令的时间,无论内容正确与否
    mstime_t last_pong_time;  /* Last time the instance replied to ping,
                                 whatever the reply was. That's used to check
                                 if the link is idle and must be reconnected. */

    // 最后一次向频道发送问候信息的时间
    // 只在当前实例为 sentinel 时使用
    mstime_t last_pub_time;   /* Last time we sent hello via Pub/Sub. */

    // 最后一次接收到这个 sentinel 发来的问候信息的时间
    // 只在当前实例为 sentinel 时使用
    mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
                                 we received a hello from this Sentinel
                                 via Pub/Sub. */

    // 最后一次回复 SENTINEL is-master-down-by-addr 命令的时间
    // 只在当前实例为 sentinel 时使用
    mstime_t last_master_down_reply_time; /* Time of last reply to
                                             SENTINEL is-master-down command. */

    // 实例被判断为 SDOWN 状态的时间
    mstime_t s_down_since_time; /* Subjectively down since time. */

    // 实例被判断为 ODOWN 状态的时间
    mstime_t o_down_since_time; /* Objectively down since time. */

    // SENTINEL down-after-milliseconds 选项所设定的值
    // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
    mstime_t down_after_period; /* Consider it down after that period. */

    // 从实例获取 INFO 命令的回复的时间
    mstime_t info_refresh;  /* Time at which we received INFO output from it. */

    /* Role and the first time we observed it.
     * This is useful in order to delay replacing what the instance reports
     * with our own configuration. We need to always wait some time in order
     * to give a chance to the leader to report the new configuration before
     * we do silly things. */
    // 实例的角色
    int role_reported;
    // 角色的更新时间
    mstime_t role_reported_time;

    // 最后一次从服务器的主服务器地址变更的时间
    mstime_t slave_conf_change_time; /* Last time slave master addr changed. */

    /* Master specific. */
    /* 主服务器实例特有的属性 -------------------------------------------------------------*/

    // 其他同样监控这个主服务器的所有 sentinel
    dict *sentinels;    /* Other sentinels monitoring the same master. */

    // 如果这个实例代表的是一个主服务器
    // 那么这个字典保存着主服务器属下的从服务器
    // 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
    dict *slaves;       /* Slaves for this master instance. */

    // SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的 quorum 参数
    // 判断这个实例为客观下线(objectively down)所需的支持投票数量
    int quorum;         /* Number of sentinels that need to agree on failure. */

    // SENTINEL parallel-syncs <master-name> <number> 选项的值
    // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs; /* How many slaves to reconfigure at same time. */

    // 连接主服务器和从服务器所需的密码
    char *auth_pass;    /* Password to use for AUTH against master & slaves. */

    /* Slave specific. */
    /* 从服务器实例特有的属性 -------------------------------------------------------------*/

    // 主从服务器连接断开的时间
    mstime_t master_link_down_time; /* Slave replication link down time. */

    // 从服务器优先级
    int slave_priority; /* Slave priority according to its INFO output. */

    // 执行故障转移操作时,从服务器发送 SLAVEOF <new-master> 命令的时间
    mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */

    // 主服务器的实例(在本实例为从服务器时使用)
    struct sentinelRedisInstance *master; /* Master instance if it's slave. */

    // INFO 命令的回复中记录的主服务器 IP
    char *slave_master_host;    /* Master host as reported by INFO */
    
    // INFO 命令的回复中记录的主服务器端口号
    int slave_master_port;      /* Master port as reported by INFO */

    // INFO 命令的回复中记录的主从服务器连接状态
    int slave_master_link_status; /* Master link status as reported by INFO */

    // 从服务器的复制偏移量
    unsigned long long slave_repl_offset; /* Slave replication offset. */

    /* Failover */
    /* 故障转移相关属性 -------------------------------------------------------------------*/


    // 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。
    // 如果这是一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。
    // 这个域只在 Sentinel 实例的 flags 属性的 SRI_MASTER_DOWN 标志处于打开状态时才有效。
    char *leader;       /* If this is a master instance, this is the runid of
                           the Sentinel that should perform the failover. If
                           this is a Sentinel, this is the runid of the Sentinel
                           that this Sentinel voted as leader. */
    // 领头的纪元
    uint64_t leader_epoch; /* Epoch of the 'leader' field. */
    // 当前执行中的故障转移的纪元
    uint64_t failover_epoch; /* Epoch of the currently started failover. */
    // 故障转移操作的当前状态
    int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */

    // 状态改变的时间
    mstime_t failover_state_change_time;

    // 最后一次进行故障迁移的时间
    mstime_t failover_start_time;   /* Last failover attempt start time. */

    // SENTINEL failover-timeout <master-name> <ms> 选项的值
    // 刷新故障迁移状态的最大时限
    mstime_t failover_timeout;      /* Max time to refresh failover state. */

    mstime_t failover_delay_logged; /* For what failover_start_time value we
                                       logged the failover delay. */
    // 指向被提升为新主服务器的从服务器的指针
    struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */

    /* Scripts executed to notify admin or reconfigure clients: when they
     * are set to NULL no script is executed. */
    // 一个文件路径,保存着 WARNING 级别的事件发生时执行的,
    // 用于通知管理员的脚本的地址
    char *notification_script;

    // 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,
    // 需要执行的脚本的地址
    char *client_reconfig_script;

} sentinelRedisInstance;

如果大家一下无法理解上面各字段的作用,不需要担心,这里只需要有个感性认识,后面我们会一一讲解。

启动和初始化

在启动 redis 的时候,会进行检查是否是以 sentinel 模式启动,如果是的话进行相应的初始化:

int checkForSentinelMode(int argc, char **argv) {
    int j;

    if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
    for (j = 1; j < argc; j++)
        if (!strcmp(argv[j],"--sentinel")) return 1;
    return 0;
}

// in main function
if (server.sentinel_mode) {
  initSentinelConfig();
  initSentinel();
}

void initSentinelConfig(void) {
    // sentinel 监听端口默认为 26379
    server.port = REDIS_SENTINEL_PORT;
}

/* Perform the Sentinel mode initialization. */
void initSentinel(void) {
    int j;
		// sentinel 支持的命令不同于主从节点,需要重新构造命令表
    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);
    }
    // 初始化纪元,epoch 可以认为是对集群状态的描述,单调递增
    sentinel.current_epoch = 0;
    // 初始化保存主服务器信息的字典
    sentinel.masters = dictCreate(&instancesDictType,NULL);
    // 初始化 TILT 模式的相关选项
    sentinel.tilt = 0;
    sentinel.tilt_start_time = 0;
    sentinel.previous_time = mstime();
    // 初始化脚本相关选项
    sentinel.running_scripts = 0;
    sentinel.scripts_queue = listCreate();
}

再来看一下如何解析 config,我们仅分析一下 sentinel monitor 指令:

char *sentinelHandleConfiguration(char **argv, int argc) {
    sentinelRedisInstance *ri;
    // SENTINEL monitor 选项
    if (!strcasecmp(argv[0],"monitor") && argc == 5) {
        /* monitor <name> <host> <port> <quorum> */
        // 读入 quorum 参数
        int quorum = atoi(argv[4]);
        // 检查 quorum 参数必须大于 0
        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";
            }
        }
    } 
		// 略
}

sentinelRedisInstance *createSentinelRedisInstance(char *name, int flags, char *hostname, int port, int quorum, sentinelRedisInstance *master) {
    sentinelRedisInstance *ri;
    sentinelAddr *addr;
    dict *table = NULL;
    char slavename[128], *sdsname;

    redisAssert(flags & (SRI_MASTER|SRI_SLAVE|SRI_SENTINEL));
    redisAssert((flags & SRI_MASTER) || master != NULL);

    /* Check address validity. */
    // 保存 IP 地址和端口号到 addr
    addr = createSentinelAddr(hostname,port);
    if (addr == NULL) return NULL;

    // 如果实例是从服务器或者 sentinel ,那么使用 ip:port 格式为实例设置名字
    // master 的 name 是在配置文件中指定的
    if (flags & (SRI_SLAVE|SRI_SENTINEL)) {
        snprintf(slavename,sizeof(slavename),
            strchr(hostname,':') ? "[%s]:%d" : "%s:%d",
            hostname,port);
        name = slavename;
    }
		// 每次创建实例之前,我们都要检查是否重复
    if (flags & SRI_MASTER) table = sentinel.masters;
    else if (flags & SRI_SLAVE) table = master->slaves;
    else if (flags & SRI_SENTINEL) table = master->sentinels;
    sdsname = sdsnew(name);
    if (dictFind(table,sdsname)) {
        // 实例已存在,函数直接返回
        sdsfree(sdsname);
        errno = EBUSY;
        return NULL;
    }
    // 创建实例对象
    ri = zmalloc(sizeof(*ri));
    // 所有连接都已断线为起始状态,sentinel 会在需要时自动为它创建连接
    ri->flags = flags | SRI_DISCONNECTED;
    ri->name = sdsname;
    ri->runid = NULL;
    ri->config_epoch = 0;
    ri->addr = addr;
    ri->cc = NULL;
    ri->pc = NULL;
    ri->pending_commands = 0;
    ri->cc_conn_time = 0;
    ri->pc_conn_time = 0;
    ri->pc_last_activity = 0;
    /* We set the last_ping_time to "now" even if we actually don't have yet
     * a connection with the node, nor we sent a ping.
     * This is useful to detect a timeout in case we'll not be able to connect
     * with the node at all. */
    ri->last_ping_time = mstime();
    ri->last_avail_time = mstime();
    ri->last_pong_time = mstime();
    ri->last_pub_time = mstime();
    ri->last_hello_time = mstime();
    ri->last_master_down_reply_time = mstime();
    ri->s_down_since_time = 0;
    ri->o_down_since_time = 0;
    ri->down_after_period = master ? master->down_after_period :
                            SENTINEL_DEFAULT_DOWN_AFTER;
    ri->master_link_down_time = 0;
    ri->auth_pass = NULL;
    ri->slave_priority = SENTINEL_DEFAULT_SLAVE_PRIORITY;
    ri->slave_reconf_sent_time = 0;
    ri->slave_master_host = NULL;
    ri->slave_master_port = 0;
    ri->slave_master_link_status = SENTINEL_MASTER_LINK_STATUS_DOWN;
    ri->slave_repl_offset = 0;
    ri->sentinels = dictCreate(&instancesDictType,NULL);
    ri->quorum = quorum;
    ri->parallel_syncs = SENTINEL_DEFAULT_PARALLEL_SYNCS;
    ri->master = master;
    ri->slaves = dictCreate(&instancesDictType,NULL);
    ri->info_refresh = 0;

    /* Failover state. */
    ri->leader = NULL;
    ri->leader_epoch = 0;
    ri->failover_epoch = 0;
    ri->failover_state = SENTINEL_FAILOVER_STATE_NONE;
    ri->failover_state_change_time = 0;
    ri->failover_start_time = 0;
    ri->failover_timeout = SENTINEL_DEFAULT_FAILOVER_TIMEOUT;
    ri->failover_delay_logged = 0;
    ri->promoted_slave = NULL;
    ri->notification_script = NULL;
    ri->client_reconfig_script = NULL;

    /* Role */
    ri->role_reported = ri->flags & (SRI_MASTER|SRI_SLAVE);
    ri->role_reported_time = mstime();
    ri->slave_conf_change_time = mstime();
    // 将实例添加到适当的表中
    dictAdd(table, ri->name, ri);
    // 返回实例
    return ri;
}

当解析完配置文件后,sentinel 节点会执行 sentinelIsRunning:

void sentinelIsRunning(void) {
    redisLog(REDIS_WARNING,"Sentinel runid is %s", server.runid);
    // Sentinel 不能在没有配置文件的情况下执行
    if (server.configfile == NULL) {
        redisLog(REDIS_WARNING,
            "Sentinel started without a config file. Exiting...");
        exit(1);
    } else if (access(server.configfile,W_OK) == -1) {
        redisLog(REDIS_WARNING,
            "Sentinel config file %s is not writable: %s. Exiting...",
            server.configfile,strerror(errno));
        exit(1);
    }
		// 为每一个监控过的 master 产生 monitor 事件
    sentinelGenerateInitialMonitorEvents();
}

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

    di = dictGetIterator(sentinel.masters);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        // 向 +monitor 频道发送消息
        sentinelEvent(REDIS_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
    }
    dictReleaseIterator(di);
}
/*
	1. 记录日志
	2. 向指定 channel(type) 发送消息
	3. 执行用户指定的通知脚本
*/
void sentinelEvent(int level, char *type, sentinelRedisInstance *ri,
                   const char *fmt, ...) {
    va_list ap;
    // 日志字符串
    char msg[REDIS_MAX_LOGMSG_LEN];
    robj *channel, *payload;
    // 处理 %@,可以理解为 %@ 是内置变量代表的是节点内部的表示
    if (fmt[0] == '%' && fmt[1] == '@') {
        // master == NULL 则代表 ri 是主节点,根据节点是否为主节点,使用不同的表示方法
        sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ?
                                         NULL : ri->master;
        if (master) {      
            // ri 不是主服务器
            snprintf(msg, sizeof(msg), "%s %s %s %d @ %s %s %d",
                // 打印 ri 的类型
                sentinelRedisInstanceTypeStr(ri),
                // 打印 ri 的名字、IP 和端口号
                ri->name, ri->addr->ip, ri->addr->port,
                // 打印 ri 的主服务器的名字、 IP 和端口号
                master->name, master->addr->ip, master->addr->port);
        } else {
            // ri 是主服务器
            snprintf(msg, sizeof(msg), "%s %s %s %d",
                // 打印 ri 的类型
                sentinelRedisInstanceTypeStr(ri),
                // 打印 ri 的名字、IP 和端口号
                ri->name, ri->addr->ip, ri->addr->port);
        }
        // 跳过已处理的 "%@" 字符
        fmt += 2;
    } else {
        msg[0] = '\0';
    }
    // 打印之后的内容,格式和平常的 printf 一样
    if (fmt[0] != '\0') {
        va_start(ap, fmt);
        vsnprintf(msg+strlen(msg), sizeof(msg)-strlen(msg), fmt, ap);
        va_end(ap);
    }
    // 如果日志的级别足够高的话,那么记录到日志中
    if (level >= server.verbosity)
        redisLog(level,"%s %s",type,msg);
    // 如果日志不是 DEBUG 日志,那么将它发送到频道中,注意这里是发送到本 sentinel 节点的频道
    if (level != REDIS_DEBUG) {
        channel = createStringObject(type,strlen(type));
        payload = createStringObject(msg,strlen(msg));
        pubsubPublishMessage(channel,payload);
        decrRefCount(channel);
        decrRefCount(payload);
    }
    // 如果有需要的话,调用提醒脚本,脚本相关的我们不进行讲解
    if (level == REDIS_WARNING && ri != NULL) {
        sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ?
                                         ri : ri->master;
        if (master->notification_script) {
            sentinelScheduleScriptExecution(master->notification_script,
                type,msg,NULL);
        }
    }
}

到这里,sentinel 独有的一些初始化相关部分就介绍完了

cron

与 replication 一样,sentinel 的主要逻辑均在 sentinelTimer 内,这个函数在 serverCron 中每一秒调用一次:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    run_with_period(100) {
        if (server.sentinel_mode) sentinelTimer();
    }
}

void sentinelTimer(void) {
		// 检查是否需要进入 TITL 模式,这个后续会讲,现在只需要这是一个异常情况
    sentinelCheckTiltCondition();
 		// 处理所有监控的主节点
    sentinelHandleDictOfRedisInstances(sentinel.masters);

    // 运行等待执行的脚本
    sentinelRunPendingScripts();

    // 清理已执行完毕的脚本,并重试出错的脚本
    sentinelCollectTerminatedScripts();

    // 杀死运行超时的脚本
    sentinelKillTimedoutScripts();

    /* We continuously change the frequency of the Redis "timer interrupt"
     * in order to desynchronize every Sentinel from every other.
     * This non-determinism avoids that Sentinels started at the same time
     * exactly continue to stay synchronized asking to be voted at the
     * same time again and again (resulting in nobody likely winning the
     * election because of split brain voting). */
    server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}
/*
	这个函数会递归调用,最终处理 sentinel 链接的所有节点(包括 master、slave、sentinel)
*/
void sentinelHandleDictOfRedisInstances(dict *instances) {
    dictIterator *di;
    dictEntry *de;
    sentinelRedisInstance *switch_to_promoted = NULL;
    // 遍历列表
    di = dictGetIterator(instances);
    while((de = dictNext(di)) != NULL) {
        // 取出实例对应的实例结构
        sentinelRedisInstance *ri = dictGetVal(de);
        // 执行调度操作
        sentinelHandleRedisInstance(ri);
        // 如果被遍历的是主服务器,那么递归地遍历该主服务器的所有从服务器和 sentinel
        if (ri->flags & SRI_MASTER) {
            // 所有从服务器
            sentinelHandleDictOfRedisInstances(ri->slaves);
            // 所有 sentinel
            sentinelHandleDictOfRedisInstances(ri->sentinels);
            // 对已下线主服务器(ri)的故障迁移已经完成
            // ri 的所有从服务器都已经同步到新主服务器
            if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
                // 已选出新的主服务器
                switch_to_promoted = ri;
            }
        }
    }
    // 将原主服务器(已下线)从主服务器表格中移除,并使用新主服务器代替它
    if (switch_to_promoted)
        sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
    dictReleaseIterator(di);
}

因为 sentinel cron 逻辑比较复杂,下面我们不会按照代码调用逻辑来讲解,而是通过提出问题->解决问题的方式来讲解。

与集群节点建立连接

在创建 sentinelRedisInstance 的时候,我们提到所有刚创建的实例,其均处于未连接状态,那么如何与节点建立连接呢?

void sentinelReconnectInstance(sentinelRedisInstance *ri) {
    // 示例未断线(已连接),返回
    if (!(ri->flags & SRI_DISCONNECTED)) return;
		// 实例有两个连接,一个用于发送 PUB/SUB 命令,一个用于发送其他命令
  
  	// 对所有实例创建一个用于发送 Redis 命令的连接
    if (ri->cc == NULL) {
        // 连接实例,hiredis 接口,这里不详细展开具体实现了
        ri->cc = redisAsyncConnect(ri->addr->ip,ri->addr->port);
        if (ri->cc->err) {
             // 连接出错
            sentinelEvent(REDIS_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",
                ri->cc->errstr);
            sentinelKillLink(ri,ri->cc);
        } else {
            // 设置连接属性
            ri->cc_conn_time = mstime();
            ri->cc->data = ri;
            redisAeAttach(server.el,ri->cc);
            // 设置连线 callback, 当连接成功以后,会调用 callback
            redisAsyncSetConnectCallback(ri->cc,
                                            sentinelLinkEstablishedCallback);
            // 设置断线 callback,当连接断开以后,会调用 callback
            redisAsyncSetDisconnectCallback(ri->cc,
                                            sentinelDisconnectCallback);
            // 发送 AUTH 命令,验证身份
            sentinelSendAuthIfNeeded(ri,ri->cc);
            sentinelSetClientName(ri,ri->cc,"cmd");
            /* Send a PING ASAP when reconnecting. */
            sentinelSendPing(ri);
        }
    }
    // 对主服务器和从服务器,创建一个用于订阅频道的连接,sentinel 只会
    // 向主节点和从节点发送 PUB/SUB 命令
    if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && ri->pc == NULL) {
        // 相同的连接过程
        ri->pc = redisAsyncConnect(ri->addr->ip,ri->addr->port);
        if (ri->pc->err) {
            sentinelEvent(REDIS_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
                ri->pc->errstr);
            sentinelKillLink(ri,ri->pc);
        } else {
            int retval;
            // 设置连接属性
            ri->pc_conn_time = mstime();
            ri->pc->data = ri;
            redisAeAttach(server.el,ri->pc);
            // 设置连接 callback
            redisAsyncSetConnectCallback(ri->pc,
                                            sentinelLinkEstablishedCallback);
            // 设置断线 callback
            redisAsyncSetDisconnectCallback(ri->pc,
                                            sentinelDisconnectCallback);
            // 发送 AUTH 命令,验证身份
            sentinelSendAuthIfNeeded(ri,ri->pc);
            // 为客户但设置名字 "pubsub"
            sentinelSetClientName(ri,ri->pc,"pubsub");
            // 发送 SUBSCRIBE __sentinel__:hello 命令,订阅频道
            // 当收到消息以后调用 sentinelReceiveHelloMessages
            retval = redisAsyncCommand(ri->pc,
                sentinelReceiveHelloMessages, NULL, "SUBSCRIBE %s",
                    SENTINEL_HELLO_CHANNEL);
            if (retval != REDIS_OK) {
								// 订阅出错就直接重新建立连接
                sentinelKillLink(ri,ri->pc);
                return;
            }
        }
    }
		// 对于所有类型的节点,都需要建立发送命令的连接
    // 对于主、从节点,需要同时建立 PUB/SUB 专用连接才算与节点完全建立连接
    if (ri->cc && (ri->flags & SRI_SENTINEL || ri->pc))
        ri->flags &= ~SRI_DISCONNECTED;
}

sentinelReconnectInstance 会在每次的 sentinelTimer 中对每个节点实例进行调用。这里要说明一下为什么需要两个连接,因为在 SUBSCRIBE 模式下,redis client 会不再发送命令到服务器,而是指等待服务器的推送消息,所以我们需要一个连接专门进行订阅,一个则用来进行发送其他命令。

automatic discover

在文章开始就提过,我们不需要为 sentinel 指定监控的从节点、其他 sentinel 节点,它可以自己自动发现这些节点,这个是如何做到的呢?

从节点

通过与主节点建立连接,sentinel 已经可以与主节点通信,它会定期的当主节点发送 INFO 命令,并调用 sentinelInfoReplyCallback 处理主节点的响应:

void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
    sentinelRedisInstance *ri = c->data;
    redisReply *r;
    if (ri) ri->pending_commands--;
    if (!reply || !ri) return;
    r = reply;
    if (r->type == REDIS_REPLY_STRING) {
        // 根据主节点响应更新本地状态
        sentinelRefreshInstanceInfo(ri,r->str);
    }
}

void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) {
    sds *lines;
    int numlines, j;
    int role = 0;
    // 将该变量重置为 0 ,避免 INFO 回复中无该值的情况
    ri->master_link_down_time = 0;
    // 对 INFO 命令的回复进行逐行分析
    lines = sdssplitlen(info,strlen(info),"\r\n",2,&numlines);
    for (j = 0; j < numlines; j++) {
        sentinelRedisInstance *slave;
        sds l = lines[j];

        /* run_id:<40 hex chars>*/
        // 读取并分析 runid
        if (sdslen(l) >= 47 && !memcmp(l,"run_id:",7)) {

            // 新设置 runid
            if (ri->runid == NULL) {
                ri->runid = sdsnewlen(l+7,40);
            } else {
                // RUNID 不同,说明服务器已重启
                if (strncmp(ri->runid,l+7,40) != 0) {
                    sentinelEvent(REDIS_NOTICE,"+reboot",ri,"%@");

                    // 释放旧 ID ,设置新 ID
                    sdsfree(ri->runid);
                    ri->runid = sdsnewlen(l+7,40);
                }
            }
        }
        // 读取从服务器的 ip 和端口号
        /* old versions: slave0:<ip>,<port>,<state>
         * new versions: slave0:ip=127.0.0.1,port=9999,... */
        if ((ri->flags & SRI_MASTER) &&
            sdslen(l) >= 7 &&
            !memcmp(l,"slave",5) && isdigit(l[5]))
        {
            char *ip, *port, *end;
            if (strstr(l,"ip=") == NULL) {
                /* Old format. */
                ip = strchr(l,':'); if (!ip) continue;
                ip++; /* Now ip points to start of ip address. */
                port = strchr(ip,','); if (!port) continue;
                *port = '\0'; /* nul term for easy access. */
                port++; /* Now port points to start of port number. */
                end = strchr(port,','); if (!end) continue;
                *end = '\0'; /* nul term for easy access. */
            } else {
                /* New format. */
                ip = strstr(l,"ip="); if (!ip) continue;
                ip += 3; /* Now ip points to start of ip address. */
                port = strstr(l,"port="); if (!port) continue;
                port += 5; /* Now port points to start of port number. */
                /* Nul term both fields for easy access. */
                end = strchr(ip,','); if (end) *end = '\0';
                end = strchr(port,','); if (end) *end = '\0';
            }
            // 如果发现有新的从服务器出现,那么为它添加实例
            // 通过定期从主节点获取 INFO,sentinel 做到了自动发现集群中的从节点
            if (sentinelRedisInstanceLookupSlave(ri,ip,atoi(port)) == NULL) {
                if ((slave = createSentinelRedisInstance(NULL,SRI_SLAVE,ip,
                            atoi(port), ri->quorum, ri)) != NULL)
                {
                    sentinelEvent(REDIS_NOTICE,"+slave",slave,"%@");
                }
            }
        }
        /* master_link_down_since_seconds:<seconds> */
        // 读取主从服务器的断线时长
        // 这个只会在实例是从服务器,并且主从连接断开的情况下出现
        if (sdslen(l) >= 32 &&
            !memcmp(l,"master_link_down_since_seconds",30))
        {
            ri->master_link_down_time = strtoll(l+31,NULL,10)*1000;
        }

        /* role:<role> */
        // 读取实例的角色
        if (!memcmp(l,"role:master",11)) role = SRI_MASTER;
        else if (!memcmp(l,"role:slave",10)) role = SRI_SLAVE;
        // 处理从服务器
        if (role == SRI_SLAVE) {
            /* master_host:<host> */
            // 读入主服务器的 IP
            if (sdslen(l) >= 12 && !memcmp(l,"master_host:",12)) {
                if (ri->slave_master_host == NULL ||
                    strcasecmp(l+12,ri->slave_master_host))
                {
                    sdsfree(ri->slave_master_host);
                    ri->slave_master_host = sdsnew(l+12);
                    ri->slave_conf_change_time = mstime();
                }
            }

            /* master_port:<port> */
            // 读入主服务器的端口号
            if (sdslen(l) >= 12 && !memcmp(l,"master_port:",12)) {
                int slave_master_port = atoi(l+12);
                if (ri->slave_master_port != slave_master_port) {
                    ri->slave_master_port = slave_master_port;
                    ri->slave_conf_change_time = mstime();
                }
            }
            
            /* master_link_status:<status> */
            // 读入主服务器的状态
            if (sdslen(l) >= 19 && !memcmp(l,"master_link_status:",19)) {
                ri->slave_master_link_status =
                    (strcasecmp(l+19,"up") == 0) ?
                    SENTINEL_MASTER_LINK_STATUS_UP :
                    SENTINEL_MASTER_LINK_STATUS_DOWN;
            }

            /* slave_priority:<priority> */
            // 读入从服务器的优先级
            if (sdslen(l) >= 15 && !memcmp(l,"slave_priority:",15))
                ri->slave_priority = atoi(l+15);
            /* slave_repl_offset:<offset> */
            // 读入从服务器的复制偏移量
            if (sdslen(l) >= 18 && !memcmp(l,"slave_repl_offset:",18))
                ri->slave_repl_offset = strtoull(l+18,NULL,10);
        }
    }
    // 更新刷新 INFO 命令回复的时间
    ri->info_refresh = mstime();
    sdsfreesplitres(lines,numlines);
		// 故障转移相关代码先省略
}
sentinel

在建立连接时,我们看到了每个 sentinel 都会向每个主节点和从节点的 __sentinel__:hello 频道注册。而每个 sentinel 节点会间歇的向它所监控的所有节点 PUB hello 信息,让其他已经启动的 sentinel 节点知道自己的存在:

int sentinelSendHello(sentinelRedisInstance *ri) {
    char ip[REDIS_IP_STR_LEN];
    char payload[REDIS_IP_STR_LEN+1024];
    int retval;
    // 如果实例是主服务器,那么使用此实例的信息
    // 如果实例是从服务器,那么使用这个从服务器的主服务器的信息
    sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master;
    // 获取地址信息,如果这个主节点正在进行故障转移,则使用将要提升的从节点的地址
    sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master);
    // 获取实例自身的地址
    if (anetSockName(ri->cc->c.fd,ip,sizeof(ip),NULL) == -1) return REDIS_ERR;
    if (ri->flags & SRI_DISCONNECTED) return REDIS_ERR;
    // 格式化信息
    snprintf(payload,sizeof(payload),
        "%s,%d,%s,%llu," /* Info about this sentinel. */
        "%s,%s,%d,%llu", /* Info about current master. */
        ip, server.port, server.runid,
        (unsigned long long) sentinel.current_epoch,
        master->name,master_addr->ip,master_addr->port,
        (unsigned long long) master->config_epoch);
    // 发送信息
    retval = redisAsyncCommand(ri->cc,
        sentinelPublishReplyCallback, NULL, "PUBLISH %s %s",
            SENTINEL_HELLO_CHANNEL,payload);
    if (retval != REDIS_OK) return REDIS_ERR;
    ri->pending_commands++;
    return REDIS_OK;
}
/*
PUB hello 消息的回调函数
*/
void sentinelPublishReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
    sentinelRedisInstance *ri = c->data;
    redisReply *r;
    if (ri) ri->pending_commands--;
    if (!reply || !ri) return;
    r = reply;
    // 如果命令发送成功,那么更新 last_pub_time
    if (r->type != REDIS_REPLY_ERROR)
        ri->last_pub_time = mstime();
}
/*
hello 频道的读取回调函数
*/
void sentinelReceiveHelloMessages(redisAsyncContext *c, void *reply, void *privdata) {
    sentinelRedisInstance *ri = c->data;
    redisReply *r;
    if (!reply || !ri) return;
    r = reply;
    // 更新最后一次接收频道命令的时间
    ri->pc_last_activity = mstime();
    // 只处理频道发来的信息,不处理订阅时和退订时产生的信息
    if (r->type != REDIS_REPLY_ARRAY ||
        r->elements != 3 ||
        r->element[0]->type != REDIS_REPLY_STRING ||
        r->element[1]->type != REDIS_REPLY_STRING ||
        r->element[2]->type != REDIS_REPLY_STRING ||
        strcmp(r->element[0]->str,"message") != 0) return;
    // 只处理非自己发送的信息
    if (strstr(r->element[2]->str,server.runid) != NULL) return;
    sentinelProcessHelloMessage(r->element[2]->str, r->element[2]->len);
}

void sentinelProcessHelloMessage(char *hello, int hello_len) {
    /* Format is composed of 8 tokens:
     * 0=ip,1=port,2=runid,3=current_epoch,4=master_name,
     * 5=master_ip,6=master_port,7=master_config_epoch. */
    int numtokens, port, removed, master_port;
    uint64_t current_epoch, master_config_epoch;
    char **token = sdssplitlen(hello, hello_len, ",", 1, &numtokens);
    sentinelRedisInstance *si, *master;

    if (numtokens == 8) {
        // 获取主服务器的名字,并丢弃和未知主服务器相关的消息。
        master = sentinelGetMasterByName(token[4]);
        if (!master) goto cleanup; /* Unknown master, skip the message. */
        // 看这个 Sentinel 是否已经认识发送消息的 Sentinel
        port = atoi(token[1]);
        master_port = atoi(token[6]);
        si = getSentinelRedisInstanceByAddrAndRunID(
                        master->sentinels,token[0],port,token[2]);
        current_epoch = strtoull(token[3],NULL,10);
        master_config_epoch = strtoull(token[7],NULL,10);

        if (!si) {
						// 移除监控该 master 的 sentinels 中与这个 sentinel runid 相同
            // 或者 host:port 相同的 sentinel
            removed = removeMatchingSentinelsFromMaster(master,token[0],port,
                            token[2]);
            if (removed) {
                sentinelEvent(REDIS_NOTICE,"-dup-sentinel",master,
                    "%@ #duplicate of %s:%d or %s",
                    token[0],port,token[2]);
            }

            /* Add the new sentinel. */
            si = createSentinelRedisInstance(NULL,SRI_SENTINEL,
                            token[0],port,master->quorum,master);
            if (si) {
                sentinelEvent(REDIS_NOTICE,"+sentinel",si,"%@");
   							// 根据消息填充 sentinel runid,并且 dump、 配置到配置文件
                si->runid = sdsnew(token[2]);
                sentinelFlushConfig();
            }
        }
        // 如果消息中记录的纪元比 Sentinel 当前的纪元要高,那么更新纪元
        if (current_epoch > sentinel.current_epoch) {
            sentinel.current_epoch = current_epoch;
            sentinelFlushConfig();
            sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
                (unsigned long long) sentinel.current_epoch);
        }
        // 如果消息中记录的配置信息更新,那么对主服务器的信息进行更新
        if (master->config_epoch < master_config_epoch) {
            master->config_epoch = master_config_epoch;
            if (master_port != master->addr->port ||
                strcmp(master->addr->ip, token[5]))
            {
              	// 如果监控的 master 的 ip 或者 port 发生改变(主节点变化)
                sentinelAddr *old_addr;
                sentinelEvent(REDIS_WARNING,"+config-update-from",si,"%@");
                sentinelEvent(REDIS_WARNING,"+switch-master",
                    master,"%s %s %d %s %d",
                    master->name,
                    master->addr->ip, master->addr->port,
                    token[5], master_port);

                old_addr = dupSentinelAddr(master->addr);
                // 更新主节点地址
                sentinelResetMasterAndChangeAddress(master, token[5], master_port);
                sentinelCallClientReconfScript(master,
                    SENTINEL_OBSERVER,"start",
                    old_addr,master->addr);
                releaseSentinelAddr(old_addr);
            }
        }
        // 更新接收到 sentinel 节点 hello 信息的时间
        if (si) si->last_hello_time = mstime();
    }

cleanup:
    sdsfreesplitres(token,numtokens);
}

主节点本身知道其从节点的信息,所以我们发现从节点只需要通过主节点的 INFO 响应即可。因为 redis 主节点并没有对 sentinel 进行认为特殊处理标记,所以我们通过在指定频道注册、发送消息来通知、获取集群中新增的 sentinel 节点。

PING

心跳机制是分布式服务中必不可少的组成部分,sentinel 通过心跳来判断集群节点的状态:

int sentinelSendPing(sentinelRedisInstance *ri) {
    int retval = redisAsyncCommand(ri->cc,
        sentinelPingReplyCallback, NULL, "PING");
    if (retval == REDIS_OK) {
        ri->pending_commands++;
        // last_ping_time == 0 代表的是已经收到了对方对我们上一次 PING 的回应
        // 发送了新 PING 命令以后更新这个字段
        if (ri->last_ping_time == 0) ri->last_ping_time = mstime();
        return 1;
    } else {
        return 0;
    }
}
// 处理节点对 PING 命令的响应
void sentinelPingReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
    sentinelRedisInstance *ri = c->data;
    redisReply *r;

    if (ri) ri->pending_commands--;
    if (!reply || !ri) return;
    r = reply;

    if (r->type == REDIS_REPLY_STATUS ||
        r->type == REDIS_REPLY_ERROR) {
        // 认为下面三种响应时合法的响应,如果收到了更新对应字段
        if (strncmp(r->str,"PONG",4) == 0 ||
            strncmp(r->str,"LOADING",7) == 0 ||
            strncmp(r->str,"MASTERDOWN",10) == 0)
        {
            // 实例运作正常
            ri->last_avail_time = mstime();
            ri->last_ping_time = 0; /* Flag the pong as received. */
        } else {
            // 如果服务器因为执行脚本而进入 BUSY 状态,
            // 那么尝试通过发送 SCRIPT KILL 来恢复服务器
            if (strncmp(r->str,"BUSY",4) == 0 &&
                (ri->flags & SRI_S_DOWN) &&
                !(ri->flags & SRI_SCRIPT_KILL_SENT))
            {
                if (redisAsyncCommand(ri->cc,
                        sentinelDiscardReplyCallback, NULL,
                        "SCRIPT KILL") == REDIS_OK)
                    ri->pending_commands++;
                ri->flags |= SRI_SCRIPT_KILL_SENT;
            }
        }
    }
    // 更新实例最后一次回复 PING 命令的时间,不管回复内容是否正常
    // 注意,代表实例正常的字段时 last_avail_time,这个要求对方对 PING 响应正常
    ri->last_pong_time = mstime();
}

sentinel 在启动时,只知道需要监控的主节点信息,对于其他的 sentinel 和从节点一无所知。当与主节点建立连接后,通过定期向主节点发送 INFO 命令,可以获取到该主节点所有的从节点,后续该主节点的新增从节点也可以迅速在下次 INFO 命令的响应中反馈给 sentinel 节点。通过 INFO 命令,sentinel 可以和集群中的主从节点获取节点对于集群的状态(这些状态可能存在不一致)。

sentinel 会在每一个发现的主从节点注册监听 __sentinel__:hello 频道,sentinel 节点会定期向 __sentinel__:hello 频道发送消息,向其他 sentinel 节点广播该 sentinel 监听主节点的消息。

检查故障

现在来看一下 sentinel 节点如何检测节点是否 reachable:

void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;
    if (ri->last_ping_time)
        // last_ping_time 代表的是上次发送 PING 命令,并且该 PING 命令并未返回的时间
        elapsed = mstime() - ri->last_ping_time;
    // cmd 使用链接已经建立超过 SENTINEL_MIN_LINK_RECONNECT_PERIOD 时间
    // 而且 PING 延迟时间已经超过 ri->down_after_period/2,上次 PONG 响应距离
    // 现在也超过了 ri->down_after_period/2, 则认为这个链接需要重新建立
    if (ri->cc &&
        (mstime() - ri->cc_conn_time) > SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
        ri->last_ping_time != 0 && /* Ther is a pending ping... */
        /* The pending ping is delayed, and we did not received
         * error replies as well. */
        (mstime() - ri->last_ping_time) > (ri->down_after_period/2) &&
        (mstime() - ri->last_pong_time) > (ri->down_after_period/2))
    {
        sentinelKillLink(ri,ri->cc);
    }
		// PUB 使用的链接建立已经超过 SENTINEL_MIN_LINK_RECONNECT_PERIOD 时间
    // 并且上次频道活跃距离现在超过 SENTINEL_PUBLISH_PERIOD*3,也进行重连
    if (ri->pc &&
        (mstime() - ri->pc_conn_time) > SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
        (mstime() - ri->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
    {
        sentinelKillLink(ri,ri->pc);
    }
		/*
		sentinel 认为节点 S_DOWN 的条件:
		1. 这个节点超过 ri->down_after_period 没有响应 PING 命令
		2. 这个节点之前是 MASTER,但是在 INFO 中上报自己为 salve,并且上报时间距离现在超过 ri->down_after_period+SENTINEL_INFO_PERIOD*2
		注意,主节点、从节点、sentinel 都可以处于 S_DOWN 状态,但是 O_DOWN 是主节点才有的状态
		*/
    if (elapsed > ri->down_after_period ||
        (ri->flags & SRI_MASTER &&
         ri->role_reported == SRI_SLAVE &&
         mstime() - ri->role_reported_time >
          (ri->down_after_period+SENTINEL_INFO_PERIOD*2)))
    {
        if ((ri->flags & SRI_S_DOWN) == 0) {
            // 发送事件
            sentinelEvent(REDIS_WARNING,"+sdown",ri,"%@");
            // 记录进入 SDOWN 状态的时间
            ri->s_down_since_time = mstime();
            // 打开 SDOWN 标志
            ri->flags |= SRI_S_DOWN;
        }
    } else {
        // 移除(可能有的) SDOWN 状态
        if (ri->flags & SRI_S_DOWN) {
            // 发送事件
            sentinelEvent(REDIS_WARNING,"-sdown",ri,"%@");
            // 移除相关标志
            ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
        }
    }
}

对于主节点,我们还需要额外检查其是否处于 O_DOWN 状态:

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    int quorum = 0, odown = 0;
		// 如果处于 S_DOWN 状态,进一步检查是否满足 O_DOWN 状态要求
    if (master->flags & SRI_S_DOWN) {
        // 检查认为该主节点下线的 sentinel 总数
        quorum = 1; /* the current sentinel. */
        // 遍历监控该主节点的所有的 sentinel
        di = dictGetIterator(master->sentinels);
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);      
            // 该 SENTINEL 也认为 master 已下线
            if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
        dictReleaseIterator(di);
        // master->quorum 就是 sentinel monitor 指令的 quorum 参数所设定
        // 如果大于等于 quorum 个 sentinel 认为该主节点已经下线,节点处于 O_DOWN 状态
        if (quorum >= master->quorum) odown = 1;
    }
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            // 未设置 O_DOWN 则设置 O_DOWN 状态并且发送事件
            sentinelEvent(REDIS_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
            master->flags |= SRI_O_DOWN;
            // 记录进入 ODOWN 的时间
            master->o_down_since_time = mstime();
        }
    } else {
        if (master->flags & SRI_O_DOWN) {
						// 如果主节点之前被设置为 O_DOWN 状态,更新状态,并且发送事件
            sentinelEvent(REDIS_WARNING,"-odown",master,"%@");
            master->flags &= ~SRI_O_DOWN;
        }
    }
}

故障转移

一旦检测到主节点处于 O_DOWN 状态,就可以发起故障转移:

int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
    // 仅针对处于 O_DOWN 主节点才可以进行故障转移
    if (!(master->flags & SRI_O_DOWN)) return 0;
    // 如果已经正在进行状态转移,不能再转移
    if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;
		// 如果上次的故障转移开始时间距离现在时间还不够长(小于master->failover_timeout*2)
  	if (mstime() - master->failover_start_time <
        master->failover_timeout*2)
    {
      	// 推迟执行新的故障转移,并且设置 master->failover_delay_logged
        if (master->failover_delay_logged != master->failover_start_time) {
            time_t clock = (master->failover_start_time +
                            master->failover_timeout*2) / 1000;
            char ctimebuf[26];

            ctime_r(&clock,ctimebuf);
            ctimebuf[24] = '\0'; /* Remove newline. */
            master->failover_delay_logged = master->failover_start_time;
            redisLog(REDIS_WARNING,
                "Next failover delay: I will not start a failover before %s",
                ctimebuf);
        }
        return 0;
    }

    // 开始一次故障转移
    sentinelStartFailover(master);
    return 1;
}
// 本函数只是简单的更改一下状态(包括纪元、主节点 flags 等)
void sentinelStartFailover(sentinelRedisInstance *master) {
    redisAssert(master->flags & SRI_MASTER);
    // 更新故障转移状态
    master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
    // 更新主服务器状态
    master->flags |= SRI_FAILOVER_IN_PROGRESS;
    // 递增本 sentinel 当前纪元,并且记录主节点开始故障转移的纪元
    master->failover_epoch = ++sentinel.current_epoch;
    // 发送事件
    sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
        (unsigned long long) sentinel.current_epoch);
    sentinelEvent(REDIS_WARNING,"+try-failover",master,"%@");
    // 记录故障转移状态的变更时间
    master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
    master->failover_state_change_time = mstime();
}

当 sentinel 发现有主节点处于 O_DOWN 状态以后,就会通过向其他 sentinel 节点发送 SENTINEL is-master-down-by-addr 命令,尝试获取发起故障转移,获取其他 sentinel 的投票:

/*
	与 sentinel 节点同步某主节点状态的方法
*/
#define SENTINEL_ASK_FORCED (1<<0)
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
    dictIterator *di;
    dictEntry *de;
    // 遍历正在监视相同 master 的所有 sentinel
    // 向它们发送 SENTINEL is-master-down-by-addr 命令
    di = dictGetIterator(master->sentinels);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        // 如果距离上次接收到 ri(sentinel) 的 is-master-down-by-addr 命令已经太久了
        // 就清除 flags 的 SRI_MASTER_DOWN 位和 leader 字段
        mstime_t elapsed = mstime() - ri->last_master_down_reply_time;
        char port[32];
        int retval;
        if (elapsed > SENTINEL_ASK_PERIOD*5) {
            ri->flags &= ~SRI_MASTER_DOWN;
            sdsfree(ri->leader);
            ri->leader = NULL;
        }
        /*
        	满足下列条件才向该 sentinel 发送 SENTINEL is-master-down-by-addr 命令:
        	1. 监控主节点已经处于 S_DOWN 状态
        	2. 与该 sentinel 节点的链接正常
        	3. 参数 flags 要求强制发送或者距离上次节点响应 SENTINEL is-master-down-by-addr 命令
        		 事件已经超过 SENTINEL_ASK_PERIOD
        */
        if ((master->flags & SRI_S_DOWN) == 0) continue;
        if (ri->flags & SRI_DISCONNECTED) continue;
        if (!(flags & SENTINEL_ASK_FORCED) &&
            mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
            continue;
        // 发送 SENTINEL is-master-down-by-addr 命令
        ll2string(port,sizeof(port),master->addr->port);
        retval = redisAsyncCommand(ri->cc,
                    sentinelReceiveIsMasterDownReply, NULL,
                    "SENTINEL is-master-down-by-addr %s %s %llu %s",
                    master->addr->ip, port,
                    sentinel.current_epoch,
                    // 如果本 Sentinel 已经检测到 master 进入 ODOWN 
                    // 并且要开始一次故障转移,那么向其他 Sentinel 发送自己的运行 ID
                    // 让对方将给自己投一票(如果对方在这个纪元内还没有投票的话)
                    (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
                    server.runid : "*");
        if (retval == REDIS_OK) ri->pending_commands++;
    }
    dictReleaseIterator(di);
}

来看一下 sentinel 节点如何响应 SENTINEL is-master-down-by-addr 命令:

void sentinelCommand(redisClient *c) {
      // 略
      } else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
        /* SENTINEL IS-MASTER-DOWN-BY-ADDR <ip> <port> <current-epoch> <runid>*/
        sentinelRedisInstance *ri;
        long long req_epoch;
        uint64_t leader_epoch = 0;
        char *leader = NULL;
        long port;
        int isdown = 0;

        if (c->argc != 6) goto numargserr;
        if (getLongFromObjectOrReply(c,c->argv[3],&port,NULL) != REDIS_OK ||
            getLongLongFromObjectOrReply(c,c->argv[4],&req_epoch,NULL)
                                                              != REDIS_OK)
            return;
        ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,
            c->argv[2]->ptr,port,NULL);

        // 检查是否存在该节点,而且是一个处于 S_DOWN 的主节点(处于 O_DOWN 的必然也处于 S_DOWN)
        if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
                                    (ri->flags & SRI_MASTER))
            isdown = 1;
				// 计算 leader
        if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
            leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
                                            c->argv[5]->ptr,
                                            &leader_epoch);
        }
        // 回复格式:
        // 1) <down_state>    1 代表下线, 0 代表未下线,这个是本 sentinel 的局部状态
        // 2) <leader_runid>  Sentinel 选举作为领头 Sentinel 的运行 ID
        // 3) <leader_epoch>  领头 Sentinel 目前的配置纪元
        addReplyMultiBulkLen(c,3);
        addReply(c, isdown ? shared.cone : shared.czero);
        addReplyBulkCString(c, leader ? leader : "*");
        addReplyLongLong(c, (long long)leader_epoch);
        if (leader) sdsfree(leader);
    } //
}

char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
    // 如果请求投票的 sentinel 的 epoch 大于本节点 epoch,更新状态和配置文件,发送事件
    if (req_epoch > sentinel.current_epoch) {
        sentinel.current_epoch = req_epoch;
        sentinelFlushConfig();
        sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
            (unsigned long long) sentinel.current_epoch);
    }
    if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch)
    {
      	// 如果本节点在 req_eepoch 尚未投票(master->leader_epoch < req_epoch)
    		// 而且发送命令的 sentinel 纪元不落后于本节点,将对 master 进行故障转移的节点
        // 设置成请求节点,并且更新其 leader_epoch,代表已经为本 epoch 投票过
        sdsfree(master->leader);
        master->leader = sdsnew(req_runid);
        // 这里已经有 sentinel.current_epoch == req_epoch
        master->leader_epoch = sentinel.current_epoch;
        sentinelFlushConfig();
        sentinelEvent(REDIS_WARNING,"+vote-for-leader",master,"%s %llu",
            master->leader, (unsigned long long) master->leader_epoch);
        // 如果本 sentinel 投票给其他节点去执行故障转移,那么本节点就算后续要尝试在同一节点上进行
        // 故障转移,必须要间隔一定时间。即多个 sentinel 不会在同一时间对同一主节点进行故障转移
        if (strcasecmp(master->leader,server.runid))
            master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
    }
    *leader_epoch = master->leader_epoch;
    return master->leader ? sdsnew(master->leader) : NULL;
}

再来看看 sentinel 如何处理 is-master-down-by-addr 响应的:

void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
    sentinelRedisInstance *ri = c->data;
    redisReply *r;
    if (ri) ri->pending_commands--;
    if (!reply || !ri) return;
    r = reply;
    // 忽略错误的响应格式 
    if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 &&
        r->element[0]->type == REDIS_REPLY_INTEGER &&
        r->element[1]->type == REDIS_REPLY_STRING &&
        r->element[2]->type == REDIS_REPLY_INTEGER)
    {
        // 更新最后一次回复询问的时间
        ri->last_master_down_reply_time = mstime();
        // 设置 SENTINEL 认为主服务器的状态
        if (r->element[0]->integer == 1) {
            // 已下线
            ri->flags |= SRI_MASTER_DOWN;
        } else {
            // 未下线
            ri->flags &= ~SRI_MASTER_DOWN;
        }
        // 如果运行 ID 不是 "*" 的话,那么这是一个带投票的回复
        if (strcmp(r->element[1]->str,"*")) {
            sdsfree(ri->leader);
            // 打印日志
            if (ri->leader_epoch != r->element[2]->integer)
                redisLog(REDIS_WARNING,
                    "%s voted for %s %llu", ri->name,
                    r->element[1]->str,
                    (unsigned long long) r->element[2]->integer);
            // ri 实例投票给 leader 进行故障转移
            ri->leader = sdsnew(r->element[1]->str);
            ri->leader_epoch = r->element[2]->integer;
        }
    }
}

redis 共定义了 7 种故障转移相关的状态:

#define SENTINEL_FAILOVER_STATE_NONE 0  /* No failover in progress. */
#define SENTINEL_FAILOVER_STATE_WAIT_START 1  /* Wait for failover_start_time*/ 
#define SENTINEL_FAILOVER_STATE_SELECT_SLAVE 2 /* Select slave to promote */
#define SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE 3 /* Slave -> Master */
#define SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 4 /* Wait slave to change role */
#define SENTINEL_FAILOVER_STATE_RECONF_SLAVES 5 /* SLAVEOF newmaster */
#define SENTINEL_FAILOVER_STATE_UPDATE_CONFIG 6 /* Monitor promoted slave. */

故障转移过程就是在这 7 种状态转移的过程:

void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
    redisAssert(ri->flags & SRI_MASTER);
    // master 未进入故障转移状态,直接返回
    if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return;
    switch(ri->failover_state) {
        // 等待故障转移开始
        case SENTINEL_FAILOVER_STATE_WAIT_START:
            sentinelFailoverWaitStart(ri);
            break;
        // 选择新主服务器
        case SENTINEL_FAILOVER_STATE_SELECT_SLAVE:
            sentinelFailoverSelectSlave(ri);
            break;
        // 升级被选中的从服务器为新主服务器
        case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
            sentinelFailoverSendSlaveOfNoOne(ri);
            break;
        // 等待升级生效,如果升级超时,那么重新选择新主服务器
        // 具体情况请看 sentinelRefreshInstanceInfo 函数
        case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
            sentinelFailoverWaitPromotion(ri);
            break;
        // 向从服务器发送 SLAVEOF 命令,让它们同步新主服务器
        case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
            sentinelFailoverReconfNextSlave(ri);
            break;
    }
}
1. SENTINEL_FAILOVER_STATE_WAIT_START -> SENTINEL_FAILOVER_STATE_SELECT_SLAVE
void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
    char *leader;
    int isleader;
    // 获取给定 epoch 的领头 Sentinel
    leader = sentinelGetLeader(ri, ri->failover_epoch);
    // 本 Sentinel 是否为领头 Sentinel ?
    isleader = leader && strcasecmp(leader,server.runid) == 0;
    sdsfree(leader);
		// 如果不是本次故障转移的 leader,而且不是一次强制的故障转移,什么都不做
    if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
        // 取 SENTINEL_ELECTION_TIMEOUT 和 ri->failover_timeout 中的最大值作为故障迁移超时时间
        int election_timeout = SENTINEL_ELECTION_TIMEOUT;
        if (election_timeout > ri->failover_timeout)
            election_timeout = ri->failover_timeout;
        // 如果故障转移超时,取消故障转移
        if (mstime() - ri->failover_start_time > election_timeout) {
            sentinelEvent(REDIS_WARNING,"-failover-abort-not-elected",ri,"%@");
            sentinelAbortFailover(ri);
        }
        return;
    }
    // 本 Sentinel 作为领头,开始执行故障迁移操作
    sentinelEvent(REDIS_WARNING,"+elected-leader",ri,"%@");
    // 状态转移 SENTINEL_FAILOVER_STATE_SELECT_SLAVE 
    // 记录状态变更时间
    ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
    ri->failover_state_change_time = mstime();
    sentinelEvent(REDIS_WARNING,"+failover-state-select-slave",ri,"%@");
}

char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {
    dict *counters;
    dictIterator *di;
    dictEntry *de;
    unsigned int voters = 0, voters_quorum;
    char *myvote;
    char *winner = NULL;
    uint64_t leader_epoch;
    uint64_t max_votes = 0;

    redisAssert(master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS));

    // 计数器,实例 -> 实例获得投票的数量
    counters = dictCreate(&leaderVotesDictType,NULL);
    // 统计其他 sentinel 的主观 leader 投票
    di = dictGetIterator(master->sentinels);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        // 为目标 Sentinel 选出的领头 Sentinel 增加一票
        if (ri->leader != NULL && ri->leader_epoch == sentinel.current_epoch)
            sentinelLeaderIncr(counters,ri->leader);
        // 统计投票数量
        voters++;
    }
    dictReleaseIterator(di);

  	/*
  		检查赢得投票的实例:
  		1. 得票数超过 50%
  		2. 得票数大于 quorum
  	*/
    di = dictGetIterator(counters);
    while((de = dictNext(di)) != NULL) {
        // 取出票数
        uint64_t votes = dictGetUnsignedIntegerVal(de);
        // 选出票数最大的人,只有得票最多的实例才可能成为 leader
        if (votes > max_votes) {
            max_votes = votes;
            winner = dictGetKey(de);
        }
    }
    dictReleaseIterator(di);
    // 本 sentinel 也进行投票
    // 如果已经有 winner(获得最多的票的 sentinel)投给这个实例
    // 投给自己
    if (winner)
        myvote = sentinelVoteLeader(master,epoch,winner,&leader_epoch);
    else
        myvote = sentinelVoteLeader(master,epoch,server.runid,&leader_epoch);
		// 如果本 sentinel 也在 epoch 进行投票
    if (myvote && leader_epoch == epoch) {
        // 更新计数器以及 winner
        uint64_t votes = sentinelLeaderIncr(counters,myvote);
        if (votes > max_votes) {
            max_votes = votes;
            winner = myvote;
        }
    }
    // 无论是否投票,总票数都需要算上本 sentinel 节点
    voters++;
		// 必须满足之前提到的两个条件,才能成为 winener
    voters_quorum = voters/2+1;
    if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
        winner = NULL;
    // 返回领头 Sentinel ,或者 NULL
    winner = winner ? sdsnew(winner) : NULL;
    sdsfree(myvote);
    dictRelease(counters);
    return winner;
}
// 重置故障转移相关字段,取消故障转移
void sentinelAbortFailover(sentinelRedisInstance *ri) {
    redisAssert(ri->flags & SRI_FAILOVER_IN_PROGRESS);
    redisAssert(ri->failover_state <= SENTINEL_FAILOVER_STATE_WAIT_PROMOTION);
    // 移除相关标识
    ri->flags &= ~(SRI_FAILOVER_IN_PROGRESS|SRI_FORCE_FAILOVER);
    // 清除状态
    ri->failover_state = SENTINEL_FAILOVER_STATE_NONE;
    ri->failover_state_change_time = mstime();
    // 清除新主服务器的升级标识
    if (ri->promoted_slave) {
        ri->promoted_slave->flags &= ~SRI_PROMOTED;
        // 清空新服务器
        ri->promoted_slave = NULL;
    }
}
2 SENTINEL_FAILOVER_STATE_SELECT_SLAVE->SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
void sentinelFailoverSelectSlave(sentinelRedisInstance *ri) {
    // 在旧主服务器所属的从服务器中,选择新服务器,作为提升为新主节点的候选
    sentinelRedisInstance *slave = sentinelSelectSlave(ri);
    
    if (slave == NULL) {
        // 没有可用的从服务器可以提升为新主服务器,故障转移操作无法执行
        sentinelEvent(REDIS_WARNING,"-failover-abort-no-good-slave",ri,"%@");

        // 中止故障转移
        sentinelAbortFailover(ri);
    } else {
        // 有合适的从服务器,发送事件
        sentinelEvent(REDIS_WARNING,"+selected-slave",slave,"%@");
        // 打开实例的升级标记
        slave->flags |= SRI_PROMOTED;
        // 记录被选中的从服务器
        ri->promoted_slave = slave;
        // 更新故障转移状态
        ri->failover_state = SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE;
        // 更新状态改变时间
        ri->failover_state_change_time = mstime();
        // 发送事件
        sentinelEvent(REDIS_NOTICE,"+failover-state-send-slaveof-noone",
            slave, "%@");
    }
}

sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master) {
    sentinelRedisInstance **instance =
        zmalloc(sizeof(instance[0])*dictSize(master->slaves));
    sentinelRedisInstance *selected = NULL;
    int instances = 0;
    dictIterator *di;
    dictEntry *de;
    mstime_t max_master_down_time = 0;
    // 对于 S_DOWN 节点,计算到主节点处于 S_DOWN 的时间
    if (master->flags & SRI_S_DOWN)
        max_master_down_time += mstime() - master->s_down_since_time;
    max_master_down_time += master->down_after_period * 10;
    // 遍历所有从服务器
    di = dictGetIterator(master->slaves);
    while((de = dictNext(di)) != NULL) {
        // 从服务器实例
        sentinelRedisInstance *slave = dictGetVal(de);
        mstime_t info_validity_time;
        // 忽略所有 SDOWN 、ODOWN 或者已断线的从服务器
        if (slave->flags & (SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED)) continue;
        if (mstime() - slave->last_avail_time > SENTINEL_PING_PERIOD*5) continue;
        if (slave->slave_priority == 0) continue;
				// 对于处于 S_DOWN 的主节点,缩短 INFO 有效时间
        if (master->flags & SRI_S_DOWN)
            info_validity_time = SENTINEL_PING_PERIOD*5;
        else
            info_validity_time = SENTINEL_INFO_PERIOD*3;
        // INFO 回复已过期,不考虑
        if (mstime() - slave->info_refresh > info_validity_time) continue;
        // 从服务器下线的时间过长,不考虑
        if (slave->master_link_down_time > max_master_down_time) continue;
        // 将被选中的 slave 保存到数组中
        instance[instances++] = slave;
    }
    dictReleaseIterator(di);
    if (instances) {
        // 对被选中的从服务器进行排序
     qsort(instance,instances,sizeof(sentinelRedisInstance*),
            compareSlavesForPromotion);
        
        // 分值最低的从服务器为被选中服务器
        selected = instance[0];
    }
    zfree(instance);
    // 返回被选中的从服务区
    return selected;
}
3 SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE -> SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) {
    int retval;
		// 如果在状态转移之间被提升的从节点断线了,会不停的重试直到超时为止(重试是发生在下一次 cron 内)
    if (ri->promoted_slave->flags & SRI_DISCONNECTED) {
        // 如果超过时限,结束故障转移
        if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
            sentinelEvent(REDIS_WARNING,"-failover-abort-slave-timeout",ri,"%@");
            sentinelAbortFailover(ri);
        }
        return;
    }
		// 向从节点发送 slaveof no one 命令,将其提升为主节点
    retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0);
    if (retval != REDIS_OK) return;
    sentinelEvent(REDIS_NOTICE, "+failover-state-wait-promotion",
        ri->promoted_slave,"%@");
    // 更新故障转移状态
    ri->failover_state = SENTINEL_FAILOVER_STATE_WAIT_PROMOTION;
    // 更新状态改变的时间
    ri->failover_state_change_time = mstime();
}

int sentinelSendSlaveOf(sentinelRedisInstance *ri, char *host, int port) {
    char portstr[32];
    int retval;
    ll2string(portstr,sizeof(portstr),port);
    if (host == NULL) {
        host = "NO";
        memcpy(portstr,"ONE",4);
    }
    // 发送 SLAVEOF NO ONE,sentinel 不检查节点响应,而是根据 INFO 的内容来判断提升是否成功(该从节点是否上报自己是一个主节点)
    retval = redisAsyncCommand(ri->cc,
        sentinelDiscardReplyCallback, NULL, "SLAVEOF %s %s", host, portstr);
    if (retval == REDIS_ERR) return retval;
    ri->pending_commands++;
    // 发送 CONFIG REWRITE
    if (redisAsyncCommand(ri->cc,
        sentinelDiscardReplyCallback, NULL, "CONFIG REWRITE") == REDIS_OK)
    {
        ri->pending_commands++;
    }
    return REDIS_OK;
}

之前省略了在处理 INFO 响应中关于故障转移部分的代码,现在可以来看一下了:

    // role 是节点 INFO 响应中上报的节点是主节点还是从节点
    // ri->role_reported 是 sentinel 节点之前观测到的节点是主节点还是从节点
		if (role != ri->role_reported) {
        ri->role_reported_time = mstime();
        ri->role_reported = role;
        if (role == SRI_SLAVE) ri->slave_conf_change_time = mstime();
        // 如果这次上报内容与之前一致,发送 +role-change 事件,不一致发送 -role-change 事件
        sentinelEvent(REDIS_VERBOSE,
            ((ri->flags & (SRI_MASTER|SRI_SLAVE)) == role) ?
            "+role-change" : "-role-change",
            ri, "%@ new reported role is %s",
            role == SRI_MASTER ? "master" : "slave",
            ri->flags & SRI_MASTER ? "master" : "slave");
    }
		// 如果处于 tilt 模式,不做任何后续处理
    if (sentinel.tilt) return;
    if ((ri->flags & SRI_MASTER) && role == SRI_SLAVE) {
        // 该节点之前是主节点,现在上报是从节点,该节点后续后被判定为 S_DOWN,不需要进行额外处理
    }
    // 处理从服务器转变为主服务器的情况
    if ((ri->flags & SRI_SLAVE) && role == SRI_MASTER) {
				// 如果这个从节点的主节点正在进行故障转移,等待从节点提升为主节点
        if ((ri->master->flags & SRI_FAILOVER_IN_PROGRESS) &&
            (ri->master->failover_state ==
                SENTINEL_FAILOVER_STATE_WAIT_PROMOTION))
        {
            // 这是一个被 Sentinel 发送 SLAVEOF no one 之后由从服务器变为主服务器的实例
            // 将这个新主服务器的配置纪元设置为 Sentinel 赢得领头选举的纪元
            // 这一操作会强制其他 Sentinel 更新它们自己的配置
            // (假设没有一个更新的纪元存在的话)
            // 更新从服务器的主服务器(已下线)的配置纪元
            ri->master->config_epoch = ri->master->failover_epoch;
            // 设置从服务器的主服务器(已下线)的故障转移状态
            // 这个状态会让从服务器开始同步新的主服务器
            ri->master->failover_state = SENTINEL_FAILOVER_STATE_RECONF_SLAVES;
            // 更新从服务器的主服务器(已下线)的故障转移状态变更时间
            ri->master->failover_state_change_time = mstime();
            // 将当前 Sentinel 状态保存到配置文件里面
            sentinelFlushConfig();
            // 发送事件
            sentinelEvent(REDIS_WARNING,"+promoted-slave",ri,"%@");
            sentinelEvent(REDIS_WARNING,"+failover-state-reconf-slaves",
                ri->master,"%@");
            // 执行脚本
            sentinelCallClientReconfScript(ri->master,SENTINEL_LEADER,
                "start",ri->master->addr,ri->addr);
        } else {
            // 这个从节点变为主节点不是因为 slaveof no one 提升导致的
            mstime_t wait_time = SENTINEL_PUBLISH_PERIOD*4;
      			// 如果这个从节点的主节点还是正常工作的,而且这个从节点在 wait_time 之内没有处于 S_DOWN 状态
            if (sentinelMasterLooksSane(ri->master) &&
               sentinelRedisInstanceNoDownFor(ri,wait_time) &&
               mstime() - ri->role_reported_time > wait_time)
            {
                // 重新将实例设置为主节点的从服务器
                int retval = sentinelSendSlaveOf(ri,
                        ri->master->addr->ip,
                        ri->master->addr->port);
                
                // 发送事件
                if (retval == REDIS_OK)
                    sentinelEvent(REDIS_NOTICE,"+convert-to-slave",ri,"%@");
            }
        }
    }
		// 如果一个 slave 本次上报的主节点的 host 或者 port 有变化
    if ((ri->flags & SRI_SLAVE) &&
        role == SRI_SLAVE &&
        (ri->slave_master_port != ri->master->addr->port ||
         strcasecmp(ri->slave_master_host,ri->master->addr->ip)))
    {
        mstime_t wait_time = ri->master->failover_timeout;
        // 检查从节点之前的主节点状态是否还可用 
        if (sentinelMasterLooksSane(ri->master) &&
            sentinelRedisInstanceNoDownFor(ri,wait_time) &&
            mstime() - ri->slave_conf_change_time > wait_time)
        {
            // 重新将从节点的主节点设置为之前的主节点
            int retval = sentinelSendSlaveOf(ri,
                    ri->master->addr->ip,
                    ri->master->addr->port);

            if (retval == REDIS_OK)
                sentinelEvent(REDIS_NOTICE,"+fix-slave-config",ri,"%@");
        }
    }
		// 处理已经发送过 slaveof 命令的从节点
    if ((ri->flags & SRI_SLAVE) && role == SRI_SLAVE &&
        (ri->flags & (SRI_RECONF_SENT|SRI_RECONF_INPROG)))
    {
        /* SRI_RECONF_SENT -> SRI_RECONF_INPROG. */
        if ((ri->flags & SRI_RECONF_SENT) &&
            ri->slave_master_host &&
            strcmp(ri->slave_master_host,
                    ri->master->promoted_slave->addr->ip) == 0 &&
            ri->slave_master_port == ri->master->promoted_slave->addr->port)
        {
            ri->flags &= ~SRI_RECONF_SENT;
            ri->flags |= SRI_RECONF_INPROG;
            sentinelEvent(REDIS_NOTICE,"+slave-reconf-inprog",ri,"%@");
        }

        /* SRI_RECONF_INPROG -> SRI_RECONF_DONE */
        if ((ri->flags & SRI_RECONF_INPROG) &&
            ri->slave_master_link_status == SENTINEL_MASTER_LINK_STATUS_UP)
        {
            ri->flags &= ~SRI_RECONF_INPROG;
            ri->flags |= SRI_RECONF_DONE;
            sentinelEvent(REDIS_NOTICE,"+slave-reconf-done",ri,"%@");
        }
    }
4 SENTINEL_FAILOVER_STATE_WAIT_PROMOTION -> SENTINEL_FAILOVER_STATE_RECONF_SLAVES

这一步转换已经在上一节当处理从节点 INFO 响应时候进行了,状态机中,如果处于 SENTINEL_FAILOVER_STATE_WAIT_PROMOTION 只会检查是否超时:

void sentinelFailoverWaitPromotion(sentinelRedisInstance *ri) {
    /* Just handle the timeout. Switching to the next state is handled
     * by the function parsing the INFO command of the promoted slave. */
    if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
        sentinelEvent(REDIS_WARNING,"-failover-abort-slave-timeout",ri,"%@");
        sentinelAbortFailover(ri);
    }
}
5 SENTINEL_FAILOVER_STATE_RECONF_SLAVES -> SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
// 让主节点的从服务器与新主节点同步
void sentinelFailoverReconfNextSlave(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    int in_progress = 0;
    // 计算正在同步新主服务器的从服务器数量
    di = dictGetIterator(master->slaves);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *slave = dictGetVal(de);
        // SLAVEOF 命令已发送,或者同步正在进行
        if (slave->flags & (SRI_RECONF_SENT|SRI_RECONF_INPROG))
            in_progress++;
    }
    // 最多只能有 master->parallel_syncs 个在进行同步新节点的从节点
    di = dictGetIterator(master->slaves);
    while(in_progress < master->parallel_syncs &&
          (de = dictNext(di)) != NULL)
    {
        sentinelRedisInstance *slave = dictGetVal(de);
        int retval;
        // 跳过已经完成的重新同步的从节点和提升为主节点的从节点
        if (slave->flags & (SRI_PROMOTED|SRI_RECONF_DONE)) continue;
				// 如果某个节点执行重新同步太久
        if ((slave->flags & SRI_RECONF_SENT) &&
            (mstime() - slave->slave_reconf_sent_time) >
            SENTINEL_SLAVE_RECONF_TIMEOUT)
        {
            // 发送重试同步事件
            sentinelEvent(REDIS_NOTICE,"-slave-reconf-sent-timeout",slave,"%@");
            // 清除已发送 SLAVEOF 命令的标记
            slave->flags &= ~SRI_RECONF_SENT;
            slave->flags |= SRI_RECONF_DONE;
        }
        // 如果已向从服务器发送 SLAVEOF 命令,或者同步正在进行
        // 又或者从服务器已断线,那么略过该服务器
        if (slave->flags & (SRI_DISCONNECTED|SRI_RECONF_SENT|SRI_RECONF_INPROG))
            continue;
        // 向从服务器发送 SLAVEOF 命令,让它同步新主服务器
        retval = sentinelSendSlaveOf(slave,
                master->promoted_slave->addr->ip,
                master->promoted_slave->addr->port);
        if (retval == REDIS_OK) {
            // 将状态改为 SLAVEOF 命令已发送
            slave->flags |= SRI_RECONF_SENT;
            // 更新发送 SLAVEOF 命令的时间
            slave->slave_reconf_sent_time = mstime();
            sentinelEvent(REDIS_NOTICE,"+slave-reconf-sent",slave,"%@");
            // 增加当前正在同步的从服务器的数量
            in_progress++;
        }
    }
    dictReleaseIterator(di);
    // 判断是否所有从服务器的同步都已经完成
    sentinelFailoverDetectEnd(master);
}

void sentinelFailoverDetectEnd(sentinelRedisInstance *master) {
    int not_reconfigured = 0, timeout = 0;
    dictIterator *di;
    dictEntry *de;
    // 上次 failover 状态更新以来,经过的时间
    mstime_t elapsed = mstime() - master->failover_state_change_time;
    // 如果新主服务器已经下线,那么故障转移操作不成功
    if (master->promoted_slave == NULL ||
        master->promoted_slave->flags & SRI_S_DOWN) return;
    // 计算未完成同步的从服务器的数量,为 0 则代表完成
    di = dictGetIterator(master->slaves);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *slave = dictGetVal(de);
        // 新主服务器和已完成同步的从服务器不计算在内
        if (slave->flags & (SRI_PROMOTED|SRI_RECONF_DONE)) continue;
        // 已下线的从服务器不计算在内
        if (slave->flags & SRI_S_DOWN) continue;
        // 增一
        not_reconfigured++;
    }
    dictReleaseIterator(di);
    if (elapsed > master->failover_timeout) {
        // 忽略超时未完成的从服务器
        not_reconfigured = 0;
        // 打开超时标志
        timeout = 1;
        // 发送超时事件
        sentinelEvent(REDIS_WARNING,"+failover-end-for-timeout",master,"%@");
    }
    // 所有从服务器都已完成同步,故障转移结束
    if (not_reconfigured == 0) {
        sentinelEvent(REDIS_WARNING,"+failover-end",master,"%@");
        // 更新故障转移状态
        // 这一状态将告知 Sentinel ,所有从服务器都已经同步到新主服务器
        master->failover_state = SENTINEL_FAILOVER_STATE_UPDATE_CONFIG;
        // 更新状态改变的时间
        master->failover_state_change_time = mstime();
    }
		// 如果超时,那么我们还是再次向没有完成重新同步的从节点发送一次 slaveof 命令,让其与新主节点同步
    if (timeout) {
        dictIterator *di;
        dictEntry *de;
        // 遍历所有从服务器
        di = dictGetIterator(master->slaves);
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *slave = dictGetVal(de);
            int retval;
            // 跳过已发送 SLAVEOF 命令,以及已经完成同步的所有从服务器
            if (slave->flags &
                (SRI_RECONF_DONE|SRI_RECONF_SENT|SRI_DISCONNECTED)) continue;
            // 发送命令
            retval = sentinelSendSlaveOf(slave,
                    master->promoted_slave->addr->ip,
                    master->promoted_slave->addr->port);
            if (retval == REDIS_OK) {
                sentinelEvent(REDIS_NOTICE,"+slave-reconf-sent-be",slave,"%@");
                // 打开从服务器的 SLAVEOF 命令已发送标记
                slave->flags |= SRI_RECONF_SENT;
            }
        }
        dictReleaseIterator(di);
    }
}
收尾

sentinelHandleDictOfRedisInstances 中,会检查是否完成了故障转移,如果完成需要进行后续收尾:

void sentinelHandleDictOfRedisInstances(dict *instances) {
	  dictIterator *di;
    dictEntry *de;
    sentinelRedisInstance *switch_to_promoted = NULL;
    // 遍历多个实例,这些实例可以是多个主服务器、多个从服务器或者多个 sentinel
    di = dictGetIterator(instances);
    while((de = dictNext(di)) != NULL) {
        // 取出实例对应的实例结构
        sentinelRedisInstance *ri = dictGetVal(de);
        // 执行调度操作
        sentinelHandleRedisInstance(ri);
        // 如果被遍历的是主服务器,那么递归地遍历该主服务器的所有从服务器
        // 以及所有 sentinel
        if (ri->flags & SRI_MASTER) {
            // 所有从服务器
            sentinelHandleDictOfRedisInstances(ri->slaves);
            // 所有 sentinel
            sentinelHandleDictOfRedisInstances(ri->sentinels);
            // 对已下线主服务器(ri)的故障迁移已经完成
            // ri 的所有从服务器都已经同步到新主服务器
            if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
                // 已选出新的主服务器
                switch_to_promoted = ri;
            }
        }
    }
    // 将原主服务器(已下线)从主服务器表格中移除,并使用新主服务器代替它
    if (switch_to_promoted)
        sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
    dictReleaseIterator(di);
}

void sentinelFailoverSwitchToPromotedSlave(sentinelRedisInstance *master) {
    // 选出要添加的 master
    sentinelRedisInstance *ref = master->promoted_slave ?
                                 master->promoted_slave : master;
    // 发送更新 master 事件
    sentinelEvent(REDIS_WARNING,"+switch-master",master,"%s %s %d %s %d",
        // 原 master 信息
        master->name, master->addr->ip, master->addr->port,
        // 新 master 信息
        ref->addr->ip, ref->addr->port);
    // 用新主服务器的信息代替原 master 的信息
    sentinelResetMasterAndChangeAddress(master,ref->addr->ip,ref->addr->port);
}

int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, int port) {
    sentinelAddr *oldaddr, *newaddr;
    sentinelAddr **slaves = NULL;
    int numslaves = 0, j;
    dictIterator *di;
    dictEntry *de;

    // 根据 ip 和 port 参数,创建地址结构
    newaddr = createSentinelAddr(ip,port);
    if (newaddr == NULL) return REDIS_ERR;
		// 构造新主节点的从节点列表
    di = dictGetIterator(master->slaves);
    while((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *slave = dictGetVal(de);
        // 通过判断地址信息,跳过新主服务器
        if (sentinelAddrIsEqual(slave->addr,newaddr)) continue;
        // 将从服务器保存到数组中
        slaves = zrealloc(slaves,sizeof(sentinelAddr*)*(numslaves+1));
        slaves[numslaves++] = createSentinelAddr(slave->addr->ip,
                                                 slave->addr->port);
    }
    dictReleaseIterator(di);
    // 如果切换了地址,那么我们把之前的老主节点添加到从节点列表中
    if (!sentinelAddrIsEqual(newaddr,master->addr)) {
        slaves = zrealloc(slaves,sizeof(sentinelAddr*)*(numslaves+1));
        slaves[numslaves++] = createSentinelAddr(master->addr->ip,
                                                 master->addr->port);
    }
    // 重置主节点实例结构
    sentinelResetMaster(master,SENTINEL_RESET_NO_SENTINELS);
    oldaddr = master->addr;
    // 为主节点实例设置新的地址
    master->addr = newaddr;
    master->o_down_since_time = 0;
    master->s_down_since_time = 0;
    // 为实例加回之前保存的所有从服务器
    for (j = 0; j < numslaves; j++) {
        sentinelRedisInstance *slave;

        slave = createSentinelRedisInstance(NULL,SRI_SLAVE,slaves[j]->ip,
                    slaves[j]->port, master->quorum, master);
        releaseSentinelAddr(slaves[j]);
        if (slave) {
            sentinelEvent(REDIS_NOTICE,"+slave",slave,"%@");
            sentinelFlushConfig();
        }
    }
    zfree(slaves);
    // 释放旧地址,刷新配置文件
    releaseSentinelAddr(oldaddr);
    sentinelFlushConfig();
    return REDIS_OK;
}

#define SENTINEL_RESET_NO_SENTINELS (1<<0)
void sentinelResetMaster(sentinelRedisInstance *ri, int flags) {

    redisAssert(ri->flags & SRI_MASTER);

    dictRelease(ri->slaves);
    ri->slaves = dictCreate(&instancesDictType,NULL);

    if (!(flags & SENTINEL_RESET_NO_SENTINELS)) {
        dictRelease(ri->sentinels);
        ri->sentinels = dictCreate(&instancesDictType,NULL);
    }

    if (ri->cc) sentinelKillLink(ri,ri->cc);

    if (ri->pc) sentinelKillLink(ri,ri->pc);

    // 设置标识为断线的主服务器
    ri->flags &= SRI_MASTER|SRI_DISCONNECTED;

    if (ri->leader) {
        sdsfree(ri->leader);
        ri->leader = NULL;
    }

    ri->failover_state = SENTINEL_FAILOVER_STATE_NONE;
    ri->failover_state_change_time = 0;
    ri->failover_start_time = 0;
    ri->promoted_slave = NULL;
    sdsfree(ri->runid);
    sdsfree(ri->slave_master_host);
    ri->runid = NULL;
    ri->slave_master_host = NULL;
    ri->last_ping_time = mstime();
    ri->last_avail_time = mstime();
    ri->last_pong_time = mstime();
    ri->role_reported_time = mstime();
    ri->role_reported = SRI_MASTER;
    // 发送主服务器重置事件
    if (flags & SENTINEL_GENERATE_EVENT)
        sentinelEvent(REDIS_WARNING,"+reset-master",ri,"%@");
}

至此故障转移相关内容全部介绍完毕

总结

  1. sentinel 是 redis 官方提供的高可用解决方案,在主从架构的基础上引入 sentinel,提供了包括监控、自动故障转移等能力
  2. 因为主从集群的故障转移需要 sentinel 来实施,所以其本身需要集群部署,保证高可用性,redis 文档推荐最少使用 3 个 sentinel 实例
  3. sentinel 使用一个全局递增的正整数(纪元,epoch)来表示集群的当前状态,每一次故障转移的发起,都会导致纪元更新
  4. 当出现网络分区,小分区内不可能发生故障转移(因为没有超过半数的 sentinel)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值