有了在主从节点之间同步数据的解决方案之后,我们已经有了运行多个 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,"%@");
}
至此故障转移相关内容全部介绍完毕
总结
- sentinel 是 redis 官方提供的高可用解决方案,在主从架构的基础上引入 sentinel,提供了包括监控、自动故障转移等能力
- 因为主从集群的故障转移需要 sentinel 来实施,所以其本身需要集群部署,保证高可用性,redis 文档推荐最少使用 3 个 sentinel 实例
- sentinel 使用一个全局递增的正整数(纪元,epoch)来表示集群的当前状态,每一次故障转移的发起,都会导致纪元更新
- 当出现网络分区,小分区内不可能发生故障转移(因为没有超过半数的 sentinel)