上一篇整理了sentinal的理论部分,本篇继续看下源码部分,源码在sentinel.c 。
redis的sentinel的解决方案基于主从复制结构着眼于分布式存储的高可用方案,那么数据的持久化跟复制就是基础。而sentinel就是针对异常情况下,如何对于实现主从切换,并且对于客户端透明。所以从整个系统来看,sentinel本身是监督者的身份,没有存储功能。而为了实现监督就会产生交互,sentinel与主服务器、sentinel与从服务器、sentinel与其他sentinel.所以要构建网络,网络里面的角色、交互内容不同。围绕这个目标,来看看redis具体是如何实现的。
Sentinel也是Redis服务器,只是与普通服务器职责不同,其负责监视Redis服务器,以提高服务器集群的可靠性。Sentinel与普通服务器共用一套框架(网络框架,底层数据结构,订阅与发布机制),但又有其独立的运行代码。
Redis为Sentinel维护了的数据结构sentinelState,保存了服务状态。而在main函数看到服务器是如何向Sentinel转化的,源码在server.c:
int main(int argc, char **argv) {
struct timeval tv;
int j;
#ifdef REDIS_TEST //测试使用
if (argc == 3 && !strcasecmp(argv[1], "test")) {
if (!strcasecmp(argv[2], "ziplist")) {
return ziplistTest(argc, argv);
} else if (!strcasecmp(argv[2], "quicklist")) {
quicklistTest(argc, argv);
} else if (!strcasecmp(argv[2], "intset")) {
return intsetTest(argc, argv);
} else if (!strcasecmp(argv[2], "zipmap")) {
return zipmapTest(argc, argv);
} else if (!strcasecmp(argv[2], "sha1test")) {
return sha1Test(argc, argv);
} else if (!strcasecmp(argv[2], "util")) {
return utilTest(argc, argv);
} else if (!strcasecmp(argv[2], "sds")) {
return sdsTest(argc, argv);
} else if (!strcasecmp(argv[2], "endianconv")) {
return endianconvTest(argc, argv);
} else if (!strcasecmp(argv[2], "crc64")) {
return crc64Test(argc, argv);
}
return -1; /* test not found */
}
#endif
/* We need to initialize our libraries, and the server configuration. */
// 初始化库
#ifdef INIT_SETPROCTITLE_REPLACEMENT
spt_init(argc, argv);
#endif
// 本函数用来配置地域的信息,设置当前程序使用的本地化信息,LC_COLLATE 配置字符串比较
setlocale(LC_COLLATE,"");
// 设置线程安全
zmalloc_enable_thread_safeness();
// 设置内存溢出的处理函数
zmalloc_set_oom_handler(redisOutOfMemoryHandler);
// 随机种子,一般rand() 产生随机数的函数会用到
srand(time(NULL)^getpid());
// 保存当前信息
gettimeofday(&tv,NULL);
// 设置哈希函数的种子
dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
// 检查服务器是否以 Sentinel 模式启动
server.sentinel_mode = checkForSentinelMode(argc,argv);
// 初始化服务器(主要是填充redisServer 结构体中的各种参数)
initServerConfig();
/* Store the executable path and arguments in a safe place in order
* to be able to restart the server later. */
// 设置可执行文件的绝对路径
server.executable = getAbsolutePath(argv[0]);
// 分配执行executable文件的参数列表的空间
server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
server.exec_argv[argc] = NULL;
// 保存当前参数
for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
/* We need to init sentinel right now as parsing the configuration file
* in sentinel mode will have the effect of populating the sentinel
* data structures with master nodes to monitor. */
// 如果服务器以 Sentinel 模式启动,那么进行 Sentinel 功能相关的初始化
// 并为要监视的主服务器创建一些相应的数据结构
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
}
/* Check if we need to start in redis-check-rdb mode. We just execute
* the program main. However the program is part of the Redis executable
* so that we can easily execute an RDB check on loading errors. */
// 检查是否执行"redis-check-rdb"检查程序
if (strstr(argv[0],"redis-check-rdb") != NULL)
redis_check_rdb_main(argc,argv);
// 检查用户是否指定了配置文件,或者配置选项
if (argc >= 2) {
j = 1; /* First option to parse in argv[] */
sds options = sdsempty();
char *configfile = NULL;
/* Handle special options --help and --version */
// 指定了打印版本信息,然后退出
if (strcmp(argv[1], "-v") == 0 ||
strcmp(argv[1], "--version") == 0) version();
// 执行帮助信息,然后退出
if (strcmp(argv[1], "--help") == 0 ||
strcmp(argv[1], "-h") == 0) usage();
// 执行内存测试程序
if (strcmp(argv[1], "--test-memory") == 0) {
if (argc == 3) {
memtest(atoi(argv[2]),50);
exit(0);
} else {
fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");
fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");
exit(1);
}
}
/* First argument is the config file name? */
// 如果第1个参数(argv[1])不是'-',那么是配置文件
if (argv[j][0] != '-' || argv[j][1] != '-') {
configfile = argv[j];
// 设置配置文件的绝对路径
server.configfile = getAbsolutePath(configfile);
/* Replace the config file in server.exec_argv with
* its absoulte path. */
zfree(server.exec_argv[j]);
// 设置可执行的参数列表
server.exec_argv[j] = zstrdup(server.configfile);
j++;
}
/* All the other options are parsed and conceptually appended to the
* configuration file. For instance --port 6380 will generate the
* string "port 6380\n" to be parsed after the actual file name
* is parsed, if any. */
// 对用户给定的其余选项进行分析,并将分析所得的字符串追加稍后载入的配置文件的内容之后
// 比如 --port 6380 会被分析为 "port 6380\n"
while(j != argc) {
// 如果是以'-'开头
if (argv[j][0] == '-' && argv[j][1] == '-') {
/* Option name */
// 跳过"--check-rdb"
if (!strcmp(argv[j], "--check-rdb")) {
/* Argument has no options, need to skip for parsing. */
j++;
continue;
}
// 每个选项之间用'\n'隔开
if (sdslen(options)) options = sdscat(options,"\n");
// 将选项追加在sds中
options = sdscat(options,argv[j]+2);
// 选项和参数用 " "隔开
options = sdscat(options," ");
} else {
/* Option argument */
// 追加选项参数
options = sdscatrepr(options,argv[j],strlen(argv[j]));
options = sdscat(options," ");
}
j++;
}
// 如果开启哨兵模式,哨兵模式配置文件不正确
if (server.sentinel_mode && configfile && *configfile == '-') {
serverLog(LL_WARNING,
"Sentinel config from STDIN not allowed.");
serverLog(LL_WARNING,
"Sentinel needs config file on disk to save state. Exiting...");
exit(1);
}
// 重置save命令的参数
resetServerSaveParams();
// 载入配置文件, options 是前面分析出的给定选项
loadServerConfig(configfile,options);
sdsfree(options);
} else {
serverLog(LL_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis");
}
// 是否被监视
server.supervised = redisIsSupervised(server.supervised_mode);
// 是否以守护进程的方式运行
int background = server.daemonize && !server.supervised;
if (background) daemonize();
// 初始化服务器
initServer();
// 创建保存pid的文件
if (background || server.pidfile) createPidFile();
// 为服务器进程设置标题
redisSetProcTitle(argv[0]);
// 打印Redis的ASCII logo
redisAsciiArt();
// 检查backlog队列
checkTcpBacklogSettings();
// 如果不是哨兵模式
if (!server.sentinel_mode) {
/* Things not needed when running in Sentinel mode. */
// 打印问候语
serverLog(LL_WARNING,"Server started, Redis version " REDIS_VERSION);
#ifdef __linux__
// 打印内存警告
linuxMemoryWarnings();
#endif
// 从 AOF 文件或者 RDB 文件中载入数据
loadDataFromDisk();
// 如果开启了集群模式
if (server.cluster_enabled) {
// 集群模式下验证载入的数据
if (verifyClusterConfigWithData() == C_ERR) {
serverLog(LL_WARNING,
"You can't have keys in a DB different than DB 0 when in "
"Cluster mode. Exiting.");
exit(1);
}
}
// 打印端口号
if (server.ipfd_count > 0)
serverLog(LL_NOTICE,"The server is now ready to accept connections on port %d", server.port);
// 打印本地套接字fd
if (server.sofd > 0)
serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
} else {
// 开启哨兵模式,哨兵模式和集群模式只能开启一种
sentinelIsRunning();
}
/* Warning the user about suspicious maxmemory setting. */
// 检查不正常的 maxmemory 配置
if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
serverLog(LL_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);
}
// 运行事件处理器,一直到服务器关闭为止
aeSetBeforeSleepProc(server.el,beforeSleep);
// 运行事件循环,一直到服务器关闭
aeMain(server.el);
// 服务器关闭,删除事件循环
aeDeleteEventLoop(server.el);
return 0;
}
上面的main可以看到,main跟sentinel有关的过程可以分为四步:
server.sentinel_mode
设置为1
。initSentinelConfig()
和initSentinel()
,分别用来初始化Sentinel
节点的默认配置,Sentinel
节点的状态。/* This function overwrites a few normal Redis config default with Sentinel
* specific defaults. */
// 这个函数会用 Sentinel 所属的属性覆盖服务器默认的属性
void initSentinelConfig(void) {
server.port = REDIS_SENTINEL_PORT;
}
/* Perform the Sentinel mode initialization. */
// 以 Sentinel 模式初始化服务器
void initSentinel(void) {
unsigned int j;
/* Remove usual Redis commands from the command table, then just add
* the SENTINEL command. */
// 将服务器的命令表清空
dictEmpty(server.commands,NULL);
// 只添加Sentinel模式的相关命令
for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
int retval;
struct redisCommand *cmd = sentinelcmds+j;
retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
serverAssert(retval == DICT_OK);
}
/* Initialize various data structures. */
/* 初始化 Sentinel 的状态 */
// 初始化纪元,用于实现故障转移操作
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();
// Sentinel通过流言协议接收关于主服务器的ip和port
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
// 故障模拟
sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE;
// Sentinel的ID置为0
memset(sentinel.myid,0,sizeof(sentinel.myid));
}
在哨兵模式下,只有11条命令可以使用,因此要用哨兵模式的命令表来代替Redis原来的命令表。
Sentinel要想对某个Redis服务器进行监视,则首先要做的就是先对Redis服务器进行连接,在连接之前需要完成配置工作(如IP,port)。从配置文件sentinel.conf
中的配置:如sentinel monitor
配置文件以这样的格式告诉哨兵节点,监控的主节点是谁,有什么样的限制条件。对以上命令的解析与配置是通过调用函数sentinelHandleConfiguration
完成的,具体的调用链如下:
server.c main()-->config.c loadServerConfig()-->loadServerConfigFromString()-->sentinel.c sentinelHandleConfiguration()
代码调用过程跳过了,配置项可能很多,解析也就多,举例如下:
// 配置处理
char *sentinelHandleConfiguration(char **argv, int argc) {
sentinelRedisInstance *ri;
// SENTINEL monitor选项
if (!strcasecmp(argv[0],"monitor") && argc == 5) {
/* monitor */
int quorum = atoi(argv[4]);//获取投票数
// 投票数必须大于等于1
if (quorum <= 0) return "Quorum must be 1 or greater.";
// 创建一个主节点实例,并加入到Sentinel所监控的master字典中
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";
}
}
// sentinel down-after-milliseconds选项
} else if (!strcasecmp(argv[0],"down-after-milliseconds") && argc == 3) {
....
}
sentinelHandleConfiguration
主要调用了createSentinelRedisInstance
函数,这个函数的工作就是初始化sentinelRedisInstance
结构体。
// 创建一个Redis实例,调用者应该设置以下参数
/*
runid: 设置为空,但是被接收到的INFO命令的输出所设置
info_refresh:如果设置为0,以为这从来没有都没有接收到INFO
*/
/*
如果flags设置了SRI_MASTER,该实例被添加进sentinel.masters表中
如果flags设置了SRI_SLAVE or SRI_SENTINEL,'master'一定不为空并且该实例被添加到master->slaves或master->sentinels中
如果该实例是从节点或者是Sentinel节点,name参数被忽略,并且被自动设置为hostname:port
*/
// 如果hostname不能被解析或者端口号非法那么函数执行失败,返回null并且设置errno
// 如果一个有相同的名字主节点,或者一个有相同的地址从节点或者一个有相同的ID的Sentinel节点已经存在,那么函数执行失败,返回NULL,设置errno为EBUSY
sentinelRedisInstance *createSentinelRedisInstance(char *name, int flags, char *hostname, int port, int quorum, sentinelRedisInstance *master) {
sentinelRedisInstance *ri;
sentinelAddr *addr;
dict *table = NULL;
char slavename[NET_PEER_ID_LEN], *sdsname;
serverAssert(flags & (SRI_MASTER|SRI_SLAVE|SRI_SENTINEL));
// 如果该实例不是主节点,那么一定就是从节点或这Sentinel节点,那么master实例一定不为空
serverAssert((flags & SRI_MASTER) || master != NULL);
/* Check address validity. */
// 创建地址对象
addr = createSentinelAddr(hostname,port);
if (addr == NULL) return NULL;
/* For slaves use ip:port as name. */
// 如果实例是从服务器或者 sentinel ,那么使用 ip:port 格式为实例设置名字
if (flags & SRI_SLAVE) {
anetFormatAddr(slavename, sizeof(slavename), hostname, port);
name = slavename;
}
/* Make sure the entry is not duplicated. This may happen when the same
* name for a master is used multiple times inside the configuration or
* if we try to add multiple times a slave or sentinel with same ip/port
* to a master. */
// 相同name的主节点被多次设置或者添加了相同ip/port的从节点或Sentinel节点,就可能会出现重复添加的实例的情况
// 为了避免这种情况,需要先检查实例是否存在
// 注意主服务会被添加到 sentinel.masters 表
// 而从服务器和 sentinel 则会被添加到 master 所属的 slaves 表和 sentinels 表中
// 如果master实例是主节点,获取当前Sentinel监控的主节点表
if (flags & SRI_MASTER) table = sentinel.masters;
// 如果master实例是从节点,获取与master实例所建立的从节点表
else if (flags & SRI_SLAVE) table = master->slaves;
// 如果master实例是sentinel节点,获取其他监控相同master主节点的Sentinel节点的字典
else if (flags & SRI_SENTINEL) table = master->sentinels;
sdsname = sdsnew(name);
// 从对应的表中找到name的节点,如果实例已经存在则返回
if (dictFind(table,sdsname)) {
releaseSentinelAddr(addr);
sdsfree(sdsname);
errno = EBUSY;
return NULL;
}
/* Create the instance object. */
// 表中不存在该实例,则创建一个实例对象
ri = zmalloc(sizeof(*ri));
/* Note that all the instances are started in the disconnected state,
* the event loop will take care of connecting them. */
// 注意所有的实例在开始时都是断开网络连接的状态,在事件循环中会为他们创建连接
ri->flags = flags;
ri->name = sdsname;
ri->runid = NULL;
ri->config_epoch = 0;
ri->addr = addr;
ri->link = createInstanceLink();
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;
ri->info = NULL;
/* Role */
ri->role_reported = ri->flags & (SRI_MASTER|SRI_SLAVE);
ri->role_reported_time = mstime();
ri->slave_conf_change_time = mstime();
/* Add into the right table. */
// 将新创建的实例添加到对应的字典中,键是名字,值是实例的地址
dictAdd(table, ri->name, ri);
return ri;
}
调用createSentinelRedisInstance()函数创建被该哨兵节点所监控的主节点实例,然后将新创建的主节点实例保存到sentinel.masters字典中,也就是初始化时创建的字典。该函数是一个通用的函数,根据参数flags不同创建不同类型的实例,并且将实例保存到不同的字典中:
SRI_MASTER:创建一个主节点实例,保存到当前哨兵节点监控的主节点字典中。
SRI_SLAVE:创建一个从节点实例,保存到主节点实例的从节点字典中。
SRI_SENTINE:创建一个哨兵节点实例,保存到其他监控该主节点实例的哨兵节点的字典中。
在这里Sentinel并没有马上去连接Redis服务器,而只是将sentinelRedisInstance.flag
状态标记为了SRI_DISCONNECT
,真正的连接工作其实在定时程序中,因为无论是主从服务器之间的连接,还是Sentinel与Redis服务器之间的连接,要想保持其连接状态,就需要定期检查,所以就直接将连接放到了定时程序中统一处理。
载入完配置文件,就会调用sentinelIsRunning()
函数开启Sentinel
。该函数主要干了这几个事:
runid
的哨兵节点分配 ID,并重写到配置文件中,并且打印到日志中。+monitor
事件通知。 服务器在初始化时会创建时间事件,并安装执行时间事件的处理函数serverCron()
,调用关系如下:
serverCron()-->sentinelTimer()->sentinelHandleDictOfRedisInstance()
//serverCron调用
void sentinelTimer(void) {
// 记录本次 sentinel 调用的事件,
// 并判断是否需要进入 TITL 模式
sentinelCheckTiltCondition();
// 执行定期操作
// 比如 PING 实例、分析主服务器和从服务器的 INFO 命令
// 向其他监视相同主服务器的 sentinel 发送问候信息
// 并接收其他 sentinel 发来的问候信息
// 执行故障转移操作,等等
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 = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}
我们可以将哨兵的任务按顺序分为四部分:
TILT 模式判断 :是一种特殊的保护模式:当 Sentinel 发现系统异常,Sentinel 就会进入 TILT 模式。当 Sentinel 进入 TILT 模式时,它仍然会继续监视所有目标,但是:
定时任务:在Sentinel的定时任务分为三步,也就是sentinelTimer()哨兵模式主函数中的三个函数:
sentinelRunPendingScripts():运行在队列中等待的脚本。
sentinelCollectTerminatedScripts():清理已成功执行的脚本,重试执行错误的脚本。
sentinelKillTimedoutScripts():杀死执行超时的脚本,等到下个周期在sentinelCollectTerminatedScripts()函数中重试执行。
这里跟 脑裂不展开。主要关注周期性任务。
我们看看在执行周期性任务的函数sentinelHandleDictOfRedisInstances()
/* Perform scheduled operations for all the instances in the dictionary.
* Recursively call the function against dictionaries of slaves. */
// 对在instances字典中的所有实例执行周期性操作。并且递归的调用
void sentinelHandleDictOfRedisInstances(dict *instances) {
dictIterator *di;
dictEntry *de;
sentinelRedisInstance *switch_to_promoted = NULL;
/* There are a number of things we need to perform against every master. */
di = dictGetIterator(instances);
// 遍历字典中所有的实例
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
// 对指定的ri实例执行周期性操作
sentinelHandleRedisInstance(ri);
// 如果ri实例是主节点
if (ri->flags & SRI_MASTER) {
// 递归的对主节点从属的从节点执行周期性操作
sentinelHandleDictOfRedisInstances(ri->slaves);
// 递归的对监控主节点的Sentinel节点执行周期性操作
sentinelHandleDictOfRedisInstances(ri->sentinels);
// 如果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.masters,和所有主节点的所有从节点ri->slaves,和所有监控该主节点的其他所有哨兵节点ri->sentinels执行周期性操作。也就是sentinelHandleRedisInstance()函数。
在执行操作的过程中,可能发生主从切换的情况,因此要给所有原来主节点的从节点(除了被选为当做晋升的从节点)发送slaveof命令去复制新的主节点(晋升为主节点的从节点)。对应sentinelFailoverSwitchToPromotedSlave()函数。
先看sentinelHandleRedisInstance 函数:
// Sentinel定时任务处理,这是Sentinel的main函数,Sentinel是完全的非阻塞。每秒调用一次该函数
/* Perform scheduled operations for the specified Redis instance. */
// 对指定的ri实例执行周期性操作
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
/* ========== MONITORING HALF ============ */
/* ========== 一半监控操作 ============ */
/* Every kind of instance */
/* 对所有的类型的实例进行操作 */
// 为Sentinel和ri实例创建一个网络连接,包括cc和pc
sentinelReconnectInstance(ri);
// 定期发送PING、PONG、PUBLISH命令到ri实例中
sentinelSendPeriodicCommands(ri);
/* ============== ACTING HALF ============= */
/* ============== 一半故障检测 ============= */
/* We don't proceed with the acting half if we are in TILT mode.
* TILT happens when we find something odd with the time, like a
* sudden change in the clock. */
// 如果Sentinel处于TILT模式,则不进行故障检测
if (sentinel.tilt) {
// 如果TILT模式的时间没到,则不执行后面的动作,直接返回
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
// 如果TILT模式时间已经到了,取消TILT模式的标识
sentinel.tilt = 0;
sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
}
/* Every kind of instance */
// 对于各种实例进行是否下线的检测,是否处于主观下线状态
sentinelCheckSubjectivelyDown(ri);
/* Masters and slaves */
// 目前对主节点和从节点的实例什么都不做
if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
/* Nothing so far. */
}
/* Only masters */
// 只对主节点进行操作
if (ri->flags & SRI_MASTER) {
// 检查从节点是否客观下线
sentinelCheckObjectivelyDown(ri);
// 如果处于客观下线状态,则进行故障转移的状态设置
if (sentinelStartFailoverIfNeeded(ri))
// 强制向其他Sentinel节点发送SENTINEL IS-MASTER-DOWN-BY-ADDR给所有的Sentinel获取回复
// 尝试获得足够的票数,标记主节点为客观下线状态,触发故障转移
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 执行故障转移操作
sentinelFailoverStateMachine(ri);
// 主节点ri没有处于客观下线的状态,那么也要尝试发送SENTINEL IS-MASTER-DOWN-BY-ADDR给所有的Sentinel获取回复
// 因为ri主节点如果有回复延迟等等状况,可以通过该命令,更新一些主节点状态
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}
该函数将周期性的操作分为两个部分,一部分是对一个的实例进行监控的操作,另一部分对改实例进行故障检测。
下面逐步分析实现过程。
第一个函数就是sentinelReconnectInstance()
函数,上面2.3我们说了创建主节点实例加入到sentinel.masters
字典的时候,该主节点的连接是关闭的,所以第一件事就是为主节点和哨兵节点建立网络连接。
/* Create the async connections for the instance link if the link
* is disconnected. Note that link->disconnected is true even if just
* one of the two links (commands and pub/sub) is missing. */
// 如果 sentinel 与实例处于断线(未连接)状态,那么创建连向实例的异步连接。
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
// 示例未断线(已连接),返回
if (ri->link->disconnected == 0) return;
// ri实例地址非法
if (ri->addr->port == 0) return; /* port == 0 means invalid address. */
instanceLink *link = ri->link;
mstime_t now = mstime();
// 如果还没有最近一次重连的时间距离现在太短,小于1s,则直接返回
if (now - ri->link->last_reconn_time < SENTINEL_PING_PERIOD) return;
// 设置最近重连的时间
ri->link->last_reconn_time = now;
/* Commands connection. */
// cc:命令连接( 对所有实例创建一个用于发送 Redis 命令的连接, 包括主服务器,从服务器,和其他Sentinel)
if (link->cc == NULL) {
// 绑定ri实例的连接地址并建立连接
link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
// 命令连接失败,则事件通知,且断开cc连接
if (link->cc->err) {
sentinelEvent(LL_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",
link->cc->errstr);
instanceLinkCloseConnection(link,link->cc);
} else {// 命令连接成功
// 设置cc连接属性
link->pending_commands = 0;
link->cc_conn_time = mstime();
link->cc->data = link;
// 将服务器的事件循环关联到cc连接的上下文中
redisAeAttach(server.el,link->cc);
// 设置确立连接的回调函数callback
redisAsyncSetConnectCallback(link->cc,
sentinelLinkEstablishedCallback);
// 设置断开连接的回调处理callback
redisAsyncSetDisconnectCallback(link->cc,
sentinelDisconnectCallback);
// 发送AUTH 命令认证
sentinelSendAuthIfNeeded(ri,link->cc);
// 发送连接名字
sentinelSetClientName(ri,link->cc,"cmd");
/* Send a PING ASAP when reconnecting. */
// 立即向ri实例发送PING命令
sentinelSendPing(ri);
}
}
/* Pub / Sub */
// pc:发布订阅连接
// 对主服务器和从服务器,创建一个用于订阅频道的连接
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && link->pc == NULL) {
// 绑定指定ri的连接地址并建立连接
link->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
// pc连接失败,则事件通知,且断开pc连接
if (link->pc->err) {
sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
link->pc->errstr);
instanceLinkCloseConnection(link,link->pc);
} else {// 连接成功
int retval;
// 设置连接属性
link->pc_conn_time = mstime();
link->pc->data = link;
// 将服务器的事件循环关联到pc连接的上下文中
redisAeAttach(server.el,link->pc);
// 设置确立连接的callback
redisAsyncSetConnectCallback(link->pc,
sentinelLinkEstablishedCallback);
// 设置断线 callback
redisAsyncSetDisconnectCallback(link->pc,
sentinelDisconnectCallback);
// 发送AUTH 命令认证
sentinelSendAuthIfNeeded(ri,link->pc);
// 发送连接名字
sentinelSetClientName(ri,link->pc,"pubsub");
/* Now we subscribe to the Sentinels "Hello" channel. */
// 发送 SUBSCRIBE __sentinel__:hello 命令,订阅频道
retval = redisAsyncCommand(link->pc,
sentinelReceiveHelloMessages, ri, "SUBSCRIBE %s",
SENTINEL_HELLO_CHANNEL);
// 订阅频道出错,关闭
if (retval != C_OK) {
/* If we can't subscribe, the Pub/Sub connection is useless
* and we can simply disconnect it and try again. */
// 关闭pc连接
instanceLinkCloseConnection(link,link->pc);
return;
}
}
}
/* Clear the disconnected status only if we have both the connections
* (or just the commands connection if this is a sentinel instance). */
// 如果已经建立了新的连接,则清除断开连接的状态
if (link->cc && (ri->flags & SRI_SENTINEL || link->pc))
link->disconnected = 0;
}
sentinelReconnectInstance()
函数的作用就是连接标记为SRI_DISCONNECT
的服务器,其对Redis发起了两种连接:
· 命令连接(Commands connection):用于向主服务器发布Sentinel的命令,并接收回复(这里Sentinel是主服务器的客户端)。
· 订阅与发布连接(Pub / Sub connection):用于订阅主服务器的__sentinel__:hello
频道。这是因为Redis的发布与订阅功能中,被发布的信息不会保存在Redis服务器里面,因此,为了不丢失__sentinel__:hello
频道的任何信息,Sentinel专门用一个连接来接收。
当建立了命令连接(cc)之后立即执行了三个动作:
当建立了发布订阅连接(pc)之后立即执行的动作:(前两个动作与命令连接相同,只列出不相同的第三个)
如果成功建立连接,之后会清除连接断开的标志,以表示连接已建立。另外之前书上还给出了解释:
Sentinel对主从服务器需要维护两个连接,而对其他Sentinel只需要维护命令连接,这是因为订阅连接的作用其实是为了自动发现:一个Sentinel可以通过分析接收到的订阅频道信息来获知其他Sentinel的存在,并通过发送频道信息来让其他Sentinel知道自己的存在(将信息发送给主从服务器,主从服务器发布信息,使得所有监视服务器的Sentinel获知信息),所以用户在使用Sentinel的时候不需要提供各个Sentinel的地址信息,监视同一个服务器的多个Sentinel可以自动发现对方,只需要维护一个命令连接进行通信就足够了。
执行完建立网络连接的函数,接下来会执行sentinelSendPeriodicCommands()
函数,该函数就是定期发送一些监控命令到主节点或从节点或哨兵节点,这些节点会将哨兵节点作为客户端来处理。
/* Send periodic PING, INFO, and PUBLISH to the Hello channel to
* the specified master or slave instance. */
// 定期发送PING PONG 和 PUBLISH 命令到指定的主节点和从节点实例的hello频道中
void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
mstime_t now = mstime();
mstime_t info_period, ping_period;
int retval;
/* Return ASAP if we have already a PING or INFO already pending, or
* in the case the instance is not properly connected. */
// 如果ri实例连接处于关闭状态,直接返回(函数不能在网络连接未创建时执行)
if (ri->link->disconnected) return;
/* For INFO, PING, PUBLISH that are not critical commands to send we
* also have a limit of SENTINEL_MAX_PENDING_COMMANDS. We don't
* want to use a lot of memory just because a link is not working
* properly (note that anyway there is a redundant protection about this,
* that is, the link will be disconnected and reconnected if a long
* timeout condition is detected. */
// 为了避免 sentinel 在实例处于不正常状态时,发送过多命令
// sentinel 只在待发送命令的数量未超过 SENTINEL_MAX_PENDING_COMMANDS 常量时才进行命令发送
// 我们不想使用大量的内存,只是因为连接对象无法正常工作, 每个实例的已发送未回复的命令个数不能超过100个,否则直接返回
if (ri->link->pending_commands >=
SENTINEL_MAX_PENDING_COMMANDS * ri->link->refcount) return;
/* If this is a slave of a master in O_DOWN condition we start sending
* it INFO every second, instead of the usual SENTINEL_INFO_PERIOD
* period. In this state we want to closely monitor slaves in case they
* are turned into masters by another Sentinel, or by the sysadmin.
*
* Similarly we monitor the INFO output more often if the slave reports
* to be disconnected from the master, so that we can have a fresh
* disconnection time figure. */
// 如果主节点处于O_DOWN状态下,那么Sentinel默认每秒发送INFO命令给它的从节点,而不是通常的SENTINEL_INFO_PERIOD(10s)周期。
// 在这种状态下,我们想更密切的监控从节点,万一他们被其他的Sentinel晋升为主节点
// 如果从节点报告和主节点断开连接,我们同样也监控INFO命令的输出更加频繁,以便我们能有一个更新鲜的断开连接的时间
// 如果ri是从节点,且他的主节点处于故障状态的状态或者从节点和主节点断开复制了
if ((ri->flags & SRI_SLAVE) &&
((ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS)) ||
(ri->master_link_down_time != 0)))
{ // 设置INFO命令的周期时间为1s
info_period = 1000;
} else {// 否则就是默认的10s
info_period = SENTINEL_INFO_PERIOD;
}
/* We ping instances every time the last received pong is older than
* the configured 'down-after-milliseconds' time, but every second
* anyway if 'down-after-milliseconds' is greater than 1 second. */
// 每次最后一次接收到的PONG比配置的 'down-after-milliseconds' 时间更长,
// 但是如果 'down-after-milliseconds'大于1秒,则每秒钟进行一次ping
// 获取ri设置的主观下线的时间
ping_period = ri->down_after_period;
// 如果大于1秒,则设置为1秒
if (ping_period > SENTINEL_PING_PERIOD) ping_period = SENTINEL_PING_PERIOD;
// 实例不是 Sentinel (主服务器或者从服务器)
// 并且以下条件的其中一个成立:
// 1)SENTINEL 未收到过这个服务器的 INFO 命令回复
// 2)距离上一次该实例回复 INFO 命令已经超过 info_period 间隔
// 那么向实例发送 INFO 命令
if ((ri->flags & SRI_SENTINEL) == 0 &&
(ri->info_refresh == 0 ||
(now - ri->info_refresh) > info_period))
{
/* Send INFO to masters and slaves, not sentinels. */
// 发送INFO命令给主节点和从节点
retval = redisAsyncCommand(ri->link->cc,
sentinelInfoReplyCallback, ri, "INFO");
// 已发送未回复的命令个数加1
if (retval == C_OK) ri->link->pending_commands++;
// 如果发送和回复PING命令超时
} else if ((now - ri->link->last_pong_time) > ping_period &&
(now - ri->link->last_ping_time) > ping_period/2) {
/* Send PING to all the three kinds of instances. */
// 发送一个PING命令给ri实例,并且更新act_ping_time
sentinelSendPing(ri);
// 发送频道的定时命令超时
} else if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
/* PUBLISH hello messages to all the three kinds of instances. */
// 发布hello信息给ri实例
sentinelSendHello(ri);
}
}
INFO
命令的频率为1s
,否则就是默认的10s
发送一次INFO
命令。PING
命令的频率是1s
发送一次。INFO
Sentinel会以十秒一次的频率首先向所监视的主机发送INFO命令:其调用过程如下:sentinelTimer()->sentinelHandleDictOfRedisInstances()->sentinelHandleRedisInstance()->sentinelSendPeriodicCommands()
,
Sentinel同样做了两件事,一个是发送了INFO命令,另一个是注册了sentinelInfoReplyCallback()
回调函数。
// 处理 INFO 命令的回复
void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
sentinelRedisInstance *ri = privdata;
instanceLink *link = c->data;
redisReply *r;
if (!reply || !link) return;
// 设置已发送但未回复的命令数减1
link->pending_commands--;
r = reply;
// 处理从主节点返回INFO命令的输出
if (r->type == REDIS_REPLY_STRING)
sentinelRefreshInstanceInfo(ri,r->str);
}
具体的处理命令sentinelRefreshInstanceInfo很长,几百行不贴了。主要是完成对服务器回复信息的处理(这其中包括,主从复制信息,存储的键值对数量,Sentinel判断是否下线等),并根据获取到所的从服务器信息实现对从服务器的监视。这也是Sentinel自动发现的部分。
hello(publish)
Sentinel初始化订阅连接的时候进行了两个操作,一个是想服务器发送了HELLO命令,二是注册了回调函数sentinelReceiveHelloMessages。
sentinelTimer()->sentinelHandleDictOfRedisInstance()->sentinelHandleRedisInstance()->SentinelSendPeriodicCommand()中,
Sentinel会向服务器的hello频道发布数据,其中由sentinelSendHello函数实现
int sentinelSendHello(sentinelRedisInstance *ri) {
char ip[NET_IP_STR_LEN];
char payload[NET_IP_STR_LEN+1024];
int retval;
char *announce_ip;
int announce_port;
// 如果实例是主节点,那么使用此实例的信息
// 如果实例是从节点,那么使用这个从节点的主节点的信息
sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master;
// 获取主节点地址信息
sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master);
// 如果连接处于关闭状态,返回C_ERR
if (ri->link->disconnected) return C_ERR;
/* Use the specified announce address if specified, otherwise try to
* obtain our own IP address. */
// 如果Sentinel指定了宣告地址(announce address),则使用该地址
if (sentinel.announce_ip) {
announce_ip = sentinel.announce_ip;
// 否则使用主节点自己的地址
} else {// 获取主节点的地址
if (anetSockName(ri->link->cc->c.fd,ip,sizeof(ip),NULL) == -1)
return C_ERR;
announce_ip = ip;
}
// 获取端口
announce_port = sentinel.announce_port ?
sentinel.announce_port : server.port;
/* Format and send the Hello message. */
// 格式化信息(按照格式将hello信息写到payload中)
snprintf(payload,sizeof(payload),
"%s,%d,%s,%llu," /* Info about this sentinel. */
"%s,%s,%d,%llu", /* Info about current master. */
announce_ip, announce_port, sentinel.myid,
(unsigned long long) sentinel.current_epoch,
/* --- */
master->name,master_addr->ip,master_addr->port,
(unsigned long long) master->config_epoch);
// 异步执行PUBLISH命令,发布hello信息
retval = redisAsyncCommand(ri->link->cc,
sentinelPublishReplyCallback, ri, "PUBLISH %s %s",
SENTINEL_HELLO_CHANNEL,payload);
if (retval != C_OK) return C_ERR;
// 已发送未回复的命令个数加1
ri->link->pending_commands++;
return C_OK;
}
通过发送PUBLISH
命令给任意类型实例,最终都是将主节点信息和当前哨兵信息广播给所有的订阅指定频道的哨兵节点,这样就可以将监控相同主节点的哨兵保存在哨兵实例的sentinels
字典中。发送完这些命令,就会获取所有节点的新的状态。因此,要根据这些状态要判断是否出现网络故障。具体可以参照sentinelReceiveHelloMessages()->sentinelProcessHelloMessage()
心跳检测是判断两台机器是否连接正常的常用手段,接收方在收到心跳包之后,会更新收到心跳的时间,在某个事件点如果检测到心跳包多久没有收到(超时),则证明网络状况不好,或对方很忙,也为接下来的行动提供指导,如延迟所需要进行的后续操作,指导心跳检测正常。
这里的函数sentinelSendPing()
函数和在第一次创建命令连接时执行的函数操作一样。
// 发送一个PING命令给指定的实例,并且更新act_ping_time,出错返回0
int sentinelSendPing(sentinelRedisInstance *ri) {
// 异步发送一个PING命令给实例ri
int retval = redisAsyncCommand(ri->link->cc,
sentinelPingReplyCallback, ri, "PING");
// 发送成功
if (retval == C_OK) {
// 已发送未回复的命令个数加1
ri->link->pending_commands++;
// 更新最近一次发送PING命令的时间
ri->link->last_ping_time = mstime();
/* We update the active ping time only if we received the pong for
* the previous ping, otherwise we are technically waiting since the
* first ping that did not received a reply. */
// 更新最近一次发送PING命令,但没有收到PONG命令的时间
if (ri->link->act_ping_time == 0)
ri->link->act_ping_time = ri->link->last_ping_time;
return 1;
} else {
return 0;
}
}
PING命令的回复有以下两种:
无论如何,只要接受到回复,都会更新最近一次收到PING命令回复的状态,表示连接可达。
Sentinel根据主观判断与客观判断来完成在线状态监测:
主观下线:是根据Sentinel自己观测某个服务器的信息;
客观下线:是通过综合所有监测某服务器的Sentinel的信息
这同样是通过心跳检测发送PING
实现的。调用sentinelCheckSubjectivelyDown()
函数进行主观下线判断
/* Is this instance down from our point of view? */
//主观下线:检查实例是否以下线(从本 Sentinel 的角度来看)
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
mstime_t elapsed = 0;
// 获取ri实例回复命令已经过去的时长
if (ri->link->act_ping_time)
// 获取最近一次发送PING命令过去了多少时间
elapsed = mstime() - ri->link->act_ping_time;
// 如果实例的连接已经断开
else if (ri->link->disconnected)
// 获取最近一次回复PING命令过去了多少时间
elapsed = mstime() - ri->link->last_avail_time;
/* Check if we are in need for a reconnection of one of the
* links, because we are detecting low activity.
* 如果检测到连接的活跃度(activity)很低,那么考虑重断开连接,并进行重连
* 1) Check if the command link seems connected, was connected not less
* than SENTINEL_MIN_LINK_RECONNECT_PERIOD, but still we have a
* pending ping for more than half the timeout. */
//cc命令连接超过了1.5s,并且之前发送过PING命令但是连接活跃度很低
if (ri->link->cc &&
(mstime() - ri->link->cc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
ri->link->act_ping_time != 0 && /* Ther is a pending ping... */
/* The pending ping is delayed, and we did not received
* error replies as well. */
(mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) &&
(mstime() - ri->link->last_pong_time) > (ri->down_after_period/2))
{
// 断开ri实例的cc命令连接
instanceLinkCloseConnection(ri->link,ri->link->cc);
}
/* 2) Check if the pubsub link seems connected, was connected not less
* than SENTINEL_MIN_LINK_RECONNECT_PERIOD, but still we have no
* activity in the Pub/Sub channel for more than
* SENTINEL_PUBLISH_PERIOD * 3.
*/
// 检查pc发布订阅的连接是否也处于低活跃状态
if (ri->link->pc &&
(mstime() - ri->link->pc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
(mstime() - ri->link->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
{
// 断开ri实例的pc发布订阅连接
instanceLinkCloseConnection(ri->link,ri->link->pc);
}
/* Update the SDOWN flag. We believe the instance is SDOWN if:
* 更新 SDOWN 标识。如果以下条件被满足,那么 Sentinel 认为实例已下线:
* 1) It is not replying.
它没有回应命令
* 2) We believe it is a master, it reports to be a slave for enough time
* to meet the down_after_period, plus enough time to get two times
* INFO report from the instance.
* Sentinel 认为实例是主服务器,这个服务器向 Sentinel 报告它将成为从服务器,
* 但在超过给定时限之后,服务器仍然没有完成这一角色转换。
*/
// ri实例回复命令已经过去的时长已经超过主观下线的时限,并且ri实例是主节点,但是报告是从节点
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)))
{
/* Is subjectively down */
// 设置主观下线的标识
if ((ri->flags & SRI_S_DOWN) == 0) {
// 发送"+sdown"的事件通知
sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
// 设置实例被判断主观下线的时间
ri->s_down_since_time = mstime();
ri->flags |= SRI_S_DOWN;
}
} else {
/* Is subjectively up */
// 如果设置了主观下线的标识,则取消标识
if (ri->flags & SRI_S_DOWN) {
// 发送事件
sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
// 移除相关标志
ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
}
}
}
客观下线状态的判断只针对主节点而言。之前已经判断过主观下线,因此只有被当前哨兵节点判断为主观下线的主节点才会继续执行客观下线的判断。
/* Is this instance down according to the configured quorum?
* 根据给定数量的 Sentinel 投票,判断实例是否已下线。
* Note that ODOWN is a weak quorum, it only means that enough Sentinels
* reported in a given time range that the instance was not reachable.
* 注意 ODOWN 是一个 weak quorum ,它只意味着有足够多的 Sentinel
* 在**给定的时间范围内**报告实例不可达。
* However messages can be delayed so there are no strong guarantees about
* N instances agreeing at the same time about the down state.
* 因为 Sentinel 对实例的检测信息可能带有延迟,
* 所以实际上 N 个 Sentinel **不可能在同一时间内**判断主服务器进入了下线状态。
*/
//客观下线判断
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
dictIterator *di;
dictEntry *de;
unsigned int quorum = 0, odown = 0;
// 如果当前 Sentinel 将主服务器判断为主观下线
// 那么检查是否有其他 Sentinel 同意这一判断
// 当同意的数量足够时,将主服务器判断为客观下线
if (master->flags & SRI_S_DOWN) {
/* Is down for enough sentinels? */
// 统计同意的 Sentinel 数量(当前Sentinel节点认为下线投1票)
quorum = 1; /* the current sentinel. */
/* Count all the other sentinels. */
// 统计其他认为 master 进入下线状态的 Sentinel 的数量
di = dictGetIterator(master->sentinels);
// 遍历监控该master实例的所有的Sentinel节点
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
// 如果Sentinel也认为master实例客观下线,那么增加投票数
if (ri->flags & SRI_MASTER_DOWN) quorum++;
}
dictReleaseIterator(di);
// 如果超过master设置的客观下线票数,则设置客观下线标识
if (quorum >= master->quorum) odown = 1;
}
/* Set the flag accordingly to the outcome. */
// 如果被判断为客观下线
if (odown) {
// master没有客观下线标识则要设置
if ((master->flags & SRI_O_DOWN) == 0) {
// 发送"+odown"事件通知
sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
quorum, master->quorum);
// 设置master客观下线标识
master->flags |= SRI_O_DOWN;
// 设置master被判断客观下线的时间
master->o_down_since_time = mstime();
}
// master实例没有客观下线
} else {
// 如果 master 曾经进入过 ODOWN 状态,那么移除该状态
if (master->flags & SRI_O_DOWN) {
// 发送事件
sentinelEvent(LL_WARNING,"-odown",master,"%@");
// 移除 ODOWN 标志
master->flags &= ~SRI_O_DOWN;
}
}
}
执行完的客观下线判断,如果发现主节点打开了客观下线的状态标识,那么就进一步进行判断,否则就执行跳过判断。执行这进一步判断的函数是:sentinelStartFailoverIfNeeded()
。该函数用来判断能不能进行故障转移:
int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
/* We can't failover if the master is not in O_DOWN state. */
// 不处于客观下线状态
if (!(master->flags & SRI_O_DOWN)) return 0;
/* Failover already in progress? */
// 已经存在Sentinel节点正在对主节点进行故障转移
if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;
/* Last failover attempt started too little time ago? */
// 如果故障转移时间相隔没有超过限制的两倍,则不进行故障转移,防止频繁执行故障转移
if (mstime() - master->failover_start_time <
master->failover_timeout*2)
{
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;
serverLog(LL_WARNING,
"Next failover delay: I will not start a failover before %s",
ctimebuf);
}
return 0;
}
// 以上条件满足,设置主节点状态为开始故障转移
sentinelStartFailover(master);
return 1;
}
如果以上条件都满足,那么会调用sentinelStartFailover()
函数,将更新主节点的故障转移状态failover_state=SENTINEL_FAILOVER_STATE_WAIT_START。 并且返回1
,执行if
条件中的代码:
sentinelAskMasterStateToOtherSentinels
如果满足条件,主节点的客观下线判断完毕,如果确认了客观下线,那么就会执行故障转移操作。
故障转移的sentinelFailoverStateMachine 函数非常清晰。
// 执行故障转移
void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
// ri实例必须是主节点
serverAssert(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;
// 发送slaveof no one命令,使从节点变为主节点
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;
}
}
这几部是连续的,成功执行完一步操作,都会将状态设置为下一步状态。下面分别来看看。
WAIT_START
1、当一个主服务器被判断为客观下线时,监视这个主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对主服务器进行故障转移操作。此状态下调用函数sentinelFailoverWaitStart
所进行的工作主要是判断自己是否为领头Sentinel:
// 准备执行故障转移
void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
char *leader;
int isleader;
/* Check if we are the leader for the failover epoch. */
// 获取给定纪元的领头 Sentinel
leader = sentinelGetLeader(ri, ri->failover_epoch);
// 本 Sentinel 是否为领头 Sentinel ?
isleader = leader && strcasecmp(leader,sentinel.myid) == 0;
sdsfree(leader);
/* If I'm not the leader, and it is not a forced failover via
* SENTINEL FAILOVER, then I can't continue with the failover. */
// 如果当前Sentinel节点不是领头节点,并且这次故障转移不是强制故障转移,那么就会返回
if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
int election_timeout = SENTINEL_ELECTION_TIMEOUT;
/* The election timeout is the MIN between SENTINEL_ELECTION_TIMEOUT
* and the configured failover timeout. */
// 当选的超时时间是SENTINEL_ELECTION_TIMEOUT和配置的故障转移时间failover_timeout两者中最小的
if (election_timeout > ri->failover_timeout)
election_timeout = ri->failover_timeout;
/* Abort the failover if I'm not the leader after some time. */
// 如果Sentinel当选领头节点时间超时,取消故障转移计划
if (mstime() - ri->failover_start_time > election_timeout) {
// 发送取消故障转移的通知
sentinelEvent(LL_WARNING,"-failover-abort-not-elected",ri,"%@");
// 取消故障转移
sentinelAbortFailover(ri);
}
return;
}
// 本 Sentinel 作为领头,开始执行故障迁移操作
//发送“赢得指定纪元的选举,可以进行故障迁移操作了”的事件通知
sentinelEvent(LL_WARNING,"+elected-leader",ri,"%@");
// 是否指定了模拟故障,当选之后的模拟故障
if (sentinel.simfailure_flags & SENTINEL_SIMFAILURE_CRASH_AFTER_ELECTION)
sentinelSimFailureCrash(); //退出当前Sentinel节点程序
// 设置故障转移状态为:选择从服务器状态
ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
// 更新故障转移操作状态改变时间
ri->failover_state_change_time = mstime();
// 发送“Sentinel 正在寻找可以升级为主服务器的从服务器。”事件通知
sentinelEvent(LL_WARNING,"+failover-state-select-slave",ri,"%@");
}
使用Raft
算法来选举领头。算法待补充。
SELECT_SLAVE 选择一个要晋升的从节点
sentinelFailoverSelectSlave()
函数用来选择一个要晋升的从节点。该函数调用sentinelSelectSlave()
函数来选则一个晋升的从节点。
// 从master节点中选出一个可以晋升的从节点,如果没有则返回NULL
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;
// master主节点处于主观下线,计算出主节点被判断为处于主观下线的最大时长
// 这个值可以保证被选中的从服务器的数据库不会太旧
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;
//跳过所有已下线的从节点
if (slave->flags & (SRI_S_DOWN|SRI_O_DOWN)) continue;
//跳过已断开的从节点
if (slave->link->disconnected) continue;
//跳过回复PING命令过于久远的从节点
if (mstime() - slave->link->last_avail_time > SENTINEL_PING_PERIOD*5) continue;
//跳过优先级为0的从节点
if (slave->slave_priority == 0) continue;
/* If the master is in SDOWN state we get INFO for slaves every second.
* Otherwise we get it with the usual period so we need to account for
* a larger delay. */
// 如果主节点处于 SDOWN 状态,那么 Sentinel 以每秒一次的频率向从节点发送 INFO 命令
// 否则以平常频率向从节点发送 INFO 命令
// 这里要检查 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;
}
1. 不选有以下状态的从节点: S_DOWN, O_DOWN, DISCONNECTED.
2. 最近一次回复PING命令超过5s的从节点
3. 最近一次获取INFO命令回复的时间不超过`info_refresh`的三倍时间长度
4. 主从节点之间断开操作的时间不超过:从当前的Sentinel节点来看,主节点处于下线状态,从节点和主节点断开连接的时间不能超过down-after-period的10倍,这看起来非常魔幻(black magic),但是实际上,当主节点不可达时,主从连接会断开,但是必然不超过一定时间。意思是,主从断开,一定是主节点造成的,而不是从节点。无论如何,我们将根据复制偏移量选择最佳的从节点。
5. 从节点的优先级不能为0,优先级为0的从节点被抛弃。
如果以上条件都满足,那么按照一下顺序排序,compareSlavesForPromotion()函数指定排序方法:
因此,当选择出一个适合晋升的从节点后,sentinelFailoverSelectSlave()会打开该从节点的SRI_PROMOTED晋升标识,并且保存起来,最后更新故障转移到下一步状态。
SLAVEOF_NOONE 使从节点变为主节点
函数sentinelFailoverSendSlaveOfNoOne()
会调用sentinelSendSlaveOf()
函数发送一个slaveof no one
命令,使从晋升的节点和原来的主节点断绝主从关系,成为新的主节点。
// 向被选中的从节点发送 SLAVEOF no one 命令
// 将它升级为新的主节点
void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) {
int retval;
/* We can't send the command to the promoted slave if it is now
* disconnected. Retry again and again with this state until the timeout
* is reached, then abort the failover. */
// 如果要晋升的从节点处于断开连接的状态,那么不能发送命令。在当前状态,在规定的故障转移超时时间内可以重试。
// 如果给定时间内选中的从节点也没有上线,那么终止故障迁移操作
if (ri->promoted_slave->link->disconnected) {
// 如果超过时限,就不再重试,中断本次故障转移后返回
if (mstime() - ri->failover_state_change_time > ri->failover_timeout) {
sentinelEvent(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
sentinelAbortFailover(ri);
}
return;
}
/* Send SLAVEOF NO ONE command to turn the slave into a master.
* 向被升级的从节点发送 SLAVEOF NO ONE 命令,将它变为一个主节点。
* We actually register a generic callback for this command as we don't
* really care about the reply. We check if it worked indirectly observing
* if INFO returns a different role (master instead of slave).
* 这里没有为命令回复关联一个回调函数,因为从节点是否已经转变为主节点可以
* 通过向从节点发送 INFO 命令来确认
*/
// 发送 SLAVEOF NO ONE 命令将从节点晋升为主节点
retval = sentinelSendSlaveOf(ri->promoted_slave,NULL,0);
if (retval != C_OK) return;
// 命令发送成功,发送事件通知
sentinelEvent(LL_NOTICE, "+failover-state-wait-promotion",
ri->promoted_slave,"%@");
// 更新状态
// 这个状态会让 Sentinel 等待被选中的从节点升级为主节点
ri->failover_state = SENTINEL_FAILOVER_STATE_WAIT_PROMOTION;
// 更新状态改变的时间
ri->failover_state_change_time = mstime();
}
4 WAIT_PROMOTION 等待从节点晋升为主节点
调用sentinelFailoverWaitPromotion()
来等待从节点晋升为主节点,但是该函数只是处理故障转移操作超时的情况。
/* We actually wait for promotion indirectly checking with INFO when the
* slave turns into a master. */
// Sentinel 会通过 INFO 命令的回复检查从节点是否已经转变为主节点
// 这里只负责检查时限
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(LL_WARNING,"-failover-abort-slave-timeout",ri,"%@");
sentinelAbortFailover(ri);
}
}
5RECONF_SLAVE
主要做的是向其他候选从服务器发送slaveof promote_slave
,使其成为他们的主机
/* Send SLAVE OF to all the remaining slaves that
* still don't appear to have the configuration updated. */
// 向所有尚未同步新主节点的从节点发送 SLAVEOF 命令
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);
// 计算处于已经发送同步命令或者已经正在同步的从节点
if (slave->flags & (SRI_RECONF_SENT|SRI_RECONF_INPROG))
in_progress++;
}
dictReleaseIterator(di);
di = dictGetIterator(master->slaves);
// 如果正在同步的从节点的数量少于 parallel-syncs 选项的值
// 那么继续遍历从节点,并让从节点对新主节点进行同步
while(in_progress < master->parallel_syncs &&
(de = dictNext(di)) != NULL)
{
sentinelRedisInstance *slave = dictGetVal(de);
int retval;
/* Skip the promoted slave, and already configured slaves. */
// 跳过被晋升的从节点和已经完成同步的从节点
if (slave->flags & (SRI_PROMOTED|SRI_RECONF_DONE)) continue;
/* If too much time elapsed without the slave moving forward to
* the next state, consider it reconfigured even if it is not.
* Sentinels will detect the slave as misconfigured and fix its
* configuration later. */
// 如果从节点设置了发送slaveof命令,但是故障转移更新到下一个状态超时
if ((slave->flags & SRI_RECONF_SENT) &&
(mstime() - slave->slave_reconf_sent_time) >
SENTINEL_SLAVE_RECONF_TIMEOUT)
{
// 发送重拾同步事件
sentinelEvent(LL_NOTICE,"-slave-reconf-sent-timeout",slave,"%@");
// 清除已发送slaveof命令的标识
slave->flags &= ~SRI_RECONF_SENT;
// 设置为完成同步的标识,随后重新发送SLAVEOF命令,进行同步
slave->flags |= SRI_RECONF_DONE;
}
/* Nothing to do for instances that are disconnected or already
* in RECONF_SENT state. */
// 跳过已经发送了命令或者已经正在同步的从节点
if (slave->flags & (SRI_RECONF_SENT|SRI_RECONF_INPROG)) continue;
// 跳过连接断开的从节点
if (slave->link->disconnected) continue;
/* Send SLAVEOF . */
// 向从节点发送 SLAVEOF 命令,让它同步新主节点(包括刚才超时的从节点)
retval = sentinelSendSlaveOf(slave,
master->promoted_slave->addr->ip,
master->promoted_slave->addr->port);
// 如果发送成功
if (retval == C_OK) {
// 设置已经发送了SLAVEOF命令标识
slave->flags |= SRI_RECONF_SENT;
// 更新发送 SLAVEOF 命令的时间
slave->slave_reconf_sent_time = mstime();
//发送事件
sentinelEvent(LL_NOTICE,"+slave-reconf-sent",slave,"%@");
in_progress++;
}
}
dictReleaseIterator(di);
/* Check if all the slaves are reconfigured and handle timeout. */
// 判断故障转移是否结束
sentinelFailoverDetectEnd(master);
}
执行完这五步故障转移操作后,回到sentinelHandleRedisInstance()
函数,该函数就剩最后一步操作了。
更新主节点的状态
执行该函数,尝试发送SENTINEL IS-MASTER-DOWN-BY-ADDR
给所有的哨兵节点获取回复,更新一些主节点状态。
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
周期性操作sentinelHandleRedisInstance()
的所有操作全部剖析完成。
sentinelHandleDictOfRedisInstances()-->.sentinelFailoverSwitchToPromotedSlave()
/* This function is called when the slave is in
* SENTINEL_FAILOVER_STATE_UPDATE_CONFIG state. In this state we need
* to remove it from the master table and add the promoted slave instead. */
// 这个函数在 master 已下线,并且对这个 master 的故障迁移操作已经完成时调用
// 这个 master 会被移除出 master 表格,并由新的主节点代替
void sentinelFailoverSwitchToPromotedSlave(sentinelRedisInstance *master) {
// 获取要添加的master
sentinelRedisInstance *ref = master->promoted_slave ?
master->promoted_slave : master;
// 发送更新 master 事件(这是绝大多数外部用户都关心的信息IP,port。发送通知)
sentinelEvent(LL_WARNING,"+switch-master",master,"%s %s %d %s %d",
master->name, master->addr->ip, master->addr->port,
ref->addr->ip, ref->addr->port);
// 用新晋升的主节点代替旧的主节点,包括所有从节点和旧的主节点从属当前新的主节点
sentinelResetMasterAndChangeAddress(master,ref->addr->ip,ref->addr->port);
}
至此,故障转移操作完成.
参考:
https://www.jianshu.com/p/ec837cd18faf
https://blog.csdn.net/men_wen/article/details/72805897