Redis源码剖析和注释(二十三)— Redis...
zhangmin
zhangmin 617 0
2019-09-23 14:11
文水信息推广,找文水信息港,更迅速联。商务合作QQ:群:483714163
撸了今年阿里、腾讯和美团的面试,我有一个重要发现…….

作者:men_wen
出处:https://blog.csdn.net/men_wen/column/info/15428
1. Redis Sentinel 介绍和部署

请参考Redis Sentinel 介绍与部署
sentinel.c文件详细注释:Redis Sentinel详细注释
本文会分为两篇分别接受Redis Sentinel的实现,本篇主要将Redis哨兵的执行过程和执行的内容。
标题4将会在Redis Sentinel实现(下)中详细剖析。
## 2. Redis Sentinel 的执行过程和初始化

Sentinel本质上是一个运行在特殊模式下的Redis服务器,无论如何,都是执行服务器的main来启动。主函数中关于Sentinel启动的代码如下:
 int main(int argc, char argv) { // 1. 检查开启哨兵模式的两种方式 server.sentinel_mode = checkForSentinelMode(argc,argv); // 2. 如果已开启哨兵模式,初始化哨兵的配置 if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); } // 3. 载入配置文件 loadServerConfig(configfile,options); // 开启哨兵模式,哨兵模式和集群模式只能开启一种 if (!server.sentinel_mode) { // 在不是哨兵模式下,会载入AOF文件和RDB文件,打印内存警告,集群模式载入数据等等操作。 } else { sentinelIsRunning(); } }以上过程可以分为四步:

    [*]检查是否开启哨兵模式
    [*]初始化哨兵的配置
    [*]载入配置文件
    [*]开启哨兵模式
2.1 检查是否开启哨兵模式

Redis Sentinel 介绍与部署文章中,介绍了两种开启的方法:

    [*]redis-sentinel sentinel.conf
    [*]redis-server sentinel.conf --sentinel
主函数中调用了checkForSentinelMode()函数来判断是否开启哨兵模式。
 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; }如果开启了哨兵模式,就会将server.sentinel_mode设置为1。
2.2 初始化哨兵的配置

在主函数中调用了两个函数initSentinelConfig()和initSentinel(),前者用来初始化Sentinel节点的默认配置,后者用来初始化Sentinel节点的状态。sentinel.c文件详细注释:Redis Sentinel详细注释
在sentinel.c文件中定义了一个全局变量sentinel,它是struct sentinelState类型的,用于保存当前Sentinel的状态。

    [*]initSentinelConfig(),初始化哨兵节点的默认端口为26379。
// 设置Sentinel的默认端口,覆盖服务器的默认属性 void initSentinelConfig(void) { server.port = REDIS_SENTINEL_PORT; }

    [*]initSentinel(),初始化哨兵节点的状态
// 执行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模式的相关命令,Sentinel模式下一共11个命令 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的成员变量。我们重点关注这几个成员:

    [*]dict *masters :当前哨兵节点监控的主节点字典。字典的键是主节点实例的名字,字典的值是一个指针,指向一个sentinelRedisInstance类型的结构。
    [*]int running_scripts: 当前正在执行的脚本的数量。
    [*]list *scripts_queue:保存要执行用户脚本的队列。
2.3 载入配置文件

在启动哨兵节点时,要指定一个.conf配置文件,配置文件可以将配置项分为两类。
Sentinel配置说明


    [*]sentinel monitor \ \ \ \

      [*]例如:sentinel monitor mymaster 127.0.0.1 6379 2
      [*]当前Sentinel节点监控 127.0.0.1:6379 这个主节点
      [*]2 代表判断主节点失败至少需要2个Sentinel节点节点同意
      [*]mymaster 是主节点的别名

    [*]sentinel xxxxxx \ xxxxxx

      [*]例如:sentinel down-after-milliseconds mymaster 30000
      [*]每个Sentinel节点都要定期PING命令来判断Redis数据节点和其余Sentinel节点是否可达,如果超过30000毫秒且没有回复,则判定不可达。
      [*]例如:sentinel parallel-syncs mymaster 1
      [*]当Sentinel节点集合对主节点故障判定达成一致时,Sentinel领导者节点会做故障转移操作,选出新的主节点,原来的从节点会向新的主节点发起复制操作,限制每次向新的主节点发起复制操作的从节点个数为1。

配置文件以这样的格式告诉哨兵节点,监控的主节点是谁,有什么样的限制条件。如果想要监控多个主节点,只需按照此格式在配置文件中多写几份。
既然配置文件都是如此,那么处理的函数也是如此处理,由于配置项很多,但是大体相似,所以我们列举处理示例的代码块:
 sentinelRedisInstance *ri; // SENTINEL monitor选项 if (!strcasecmp(argv[0],"monitor") && argc == 5) { /* monitor     */ int quorum = atoi(argv[4]); //获取投票数 // 投票数必须大于等于1 if (quorum down_after_period = atoi(argv[2]); if (ri->down_after_period slaves或master->sentinels中如果该实例是从节点或者是哨兵节点,name参数被忽略,并且被自动设置为hostname:port
</ul>当根据flags能够获取实例的类型后,就会初始化一个sentinelRedisInstance类型的实例,添加到对应的字典中。
 typedef struct sentinelRedisInstance { // 标识值,记录了当前Redis实例的类型和状态 int flags; /* See SRI_... defines */ // 实例的名字 // 主节点的名字由用户在配置文件中设置 // 从节点以及Sentinel节点的名字由Sentinel自动设置,格式为:ip:port char *name; /* Master name from the point of view of this sentinel. */ // 实例运行的独一无二ID char *runid; /* Run ID of this instance, or unique ID if is a Sentinel.*/ // 配置纪元,用于实现故障转移 uint64_t config_epoch; /* Configuration epoch. */ // 实例地址:ip和port sentinelAddr *addr; /* Master host. */ // 实例的连接,有可能是被Sentinel共享的 instanceLink *link; /* Link to the instance, may be shared for Sentinels. */ // 最近一次通过 Pub/Sub 发送信息的时间 mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */ // 只有被Sentinel实例使用 // 最近一次接收到从Sentinel发送来hello的时间 mstime_t last_hello_time; // 最近一次回复SENTINEL is-master-down的时间 mstime_t last_master_down_reply_time; /* Time of last reply to SENTINEL is-master-down command. */ // 实例被判断为主观下线的时间 mstime_t s_down_since_time; /* Subjectively down since time. */ // 实例被判断为客观下线的时间 mstime_t o_down_since_time; /* Objectively down since time. */ // 实例无响应多少毫秒之后被判断为主观下线 // 由SENTINEL down-after-millisenconds配置设定 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. */ // 实例的角色 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. */ // 如果当前实例是主节点,那么slaves保存着该主节点的所有从节点实例 // 键是从节点命令,值是从节点服务器对应的sentinelRedisInstance dict *slaves; /* Slaves for this master instance. */ // 判定该主节点客观下线的投票数 // 由SENTINEL monitor    配置 unsigned int quorum;/* Number of sentinels that need to agree on failure. */ // 在故障转移时,可以同时对新的主节点进行同步的从节点数量 // 由sentinel parallel-syncs  配置 int parallel_syncs; /* How many slaves to reconfigure at same time. */ // 连接主节点和从节点的认证密码 char *auth_pass; /* Password to use for AUTH against master & slaves. */ /*----------------------------------从节点特有的属性----------------------------------*/ // 从节点复制操作断开时间 mstime_t master_link_down_time; /* Slave replication link down time. */ // 按照INFO命令输出的从节点优先级 int slave_priority; /* Slave priority according to its INFO output. */ // 故障转移时,从节点发送SLAVEOF 命令的时间 mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF  */ // 如果当前实例是从节点,那么保存该从节点连接的主节点实例 struct sentinelRedisInstance *master; /* Master instance if it's slave. */ // INFO命令的回复中记录的主节点的IP char *slave_master_host; /* Master host as reported by INFO */ // INFO命令的回复中记录的主节点的port 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. */ /*----------------------------------故障转移的属性----------------------------------*/ // 如果这是一个主节点实例,那么leader保存的是执行故障转移的Sentinel的runid // 如果这是一个Sentinel实例,那么leader保存的是当前这个Sentinel实例选举出来的领头的runid char *leader; // 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. */ // 更新故障转移状态的最大超时时间 mstime_t failover_timeout; /* Max time to refresh failover state. */ // 记录故障转移延迟的时间 mstime_t failover_delay_logged; // 晋升为新主节点的从节点实例 struct sentinelRedisInstance *promoted_slave; // 通知admin的可执行脚本的地址,如果设置为空,则没有执行的脚本 char *notification_script; // 通知配置的client的可执行脚本的地址,如果设置为空,则没有执行的脚本 char *client_reconfig_script; // 缓存INFO命令的输出 sds info; /* cached INFO output */ } sentinelRedisInstance;该实例用来抽象描述一个节点,可以是主节点、从节点或者是哨兵节点。
2.3.2 查找主节点

在配置文件中分的那两个部分,第一部分是创建上面给出的结构实例,另一部分则是配置其中的一部分成员。因此,第一步要根据名字在哨兵节点的主节点字典中找到主节点实例。
 sentinelRedisInstance *sentinelGetMasterByName(char *name) { sentinelRedisInstance *ri; sds sdsname = sdsnew(name); // 从Sentinel所监视的所有主节点中寻找名字为name的主节点,找到返回 ri = dictFetchValue(sentinel.masters,sdsname); sdsfree(sdsname); return ri; }当找到并返回主节点实例后,就可以配置其变量了。例如:ri->down_after_period = atoi(argv[2])
2.4 开启 Sentinel

载入完配置文件,就会调用sentinelIsRunning()函数开启Sentinel。该函数主要干了这几个事:

    [*]检查配置文件是否可写,因为要重写配置文件。
    [*]为没有runid的哨兵节点分配 ID,并重写到配置文件中,并且打印到日志中。
    [*]生成一个+monitor事件通知。
所以在启动一个哨兵节点时,查看日志会发现:
 12775:X 28 May 15:14:34.953 # Sentinel ID is a4dce0267abdb89f7422c9a42960e6cb6e4 d565a 12775:X 28 May 15:14:34.953 # +monitor master mymaster 127.0.0.1 6379 quorum 2至此,就正式启动了哨兵节点。我们用图片的方式来描述一下一个哨兵节点监控两个主节点的情况:

3&#46; Redis Sentinel 的所有操作

Redis哨兵的操作,都是放在时间处理器中执行。服务器在初始化时会创建时间事件,并安装执行时间事件的处理函数serverCron(),在该函数调用sentinelTimer()函数(如下代码所示)来每100ms执行一次哨兵的定时中断,或者叫执行哨兵的任务。sentinel.c文件详细注释:Redis Sentinel详细注释
 run_with_period(100) { if (server.sentinel_mode) sentinelTimer(); }sentinelTimer()函数就是Sentinel的主函数,他的执行过程非常清晰,我们直接给出代码:
 void sentinelTimer(void) { // 先检查Sentinel是否需要进入TITL模式,更新最近一次执行Sentinel模式的周期函数的时间 sentinelCheckTiltCondition(); // 对Sentinel监控的所有主节点进行递归式的执行周期性操作 sentinelHandleDictOfRedisInstances(sentinel.masters); // 运行在队列中等待的脚本 sentinelRunPendingScripts(); // 清理已成功执行的脚本,重试执行错误的脚本 sentinelCollectTerminatedScripts(); // 杀死执行超时的脚本,等到下个周期在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). */ // 我们不断改变Redis定期任务的执行频率,以便使每个Sentinel节点都不同步,这种不确定性可以避免Sentinel在同一时间开始完全继续保持同步,当被要求进行投票时,一次又一次在同一时间进行投票,因为脑裂导致有可能没有胜选者 server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ; }我们可以将哨兵的任务按顺序分为四部分:

    [*]TILT 模式判断
    [*]执行周期性任务。例如:定期发送PING、hello信息等等。
    [*]执行脚本任务
    [*]脑裂
接下来,依次分析
3.1 TILT 模式判断

TILT 模式是一种特殊的保护模式:当 Sentinel 发现系统有些不对劲时,Sentinel 就会进入 TILT 模式。
因为 Sentinel 的时间中断器默认每秒执行 10 次,所以我们预期时间中断器的两次执行之间的间隔为 100 毫秒左右。但是出现以下情况会出现异常:

    [*]Sentinel进程在某时被阻塞,有很多种原因,负载过大,IO任务密集,进程被信号停止等等。
    [*]系统时钟发送明显变化
Sentinel 的做法是(如下sentinelCheckTiltCondition()函数所示),记录上一次时间中断器执行时的时间,并将它和这一次时间中断器执行的时间进行对比:

    [*]如果两次调用时间之间的差距为负值,或者非常大(超过 2 秒钟),那么 Sentinel 进入 TILT 模式。
    [*]如果 Sentinel 已经进入 TILT 模式,那么 Sentinel 延迟退出 TILT 模式的时间。
void sentinelCheckTiltCondition(void) { mstime_t now = mstime(); // 最后一次执行Sentinel时间处理程序的时间过去了过久 mstime_t delta = now - sentinel.previous_time; // 差为负数,或者大于2秒 if (delta < 0 || delta > SENTINEL_TILT_TRIGGER) { // 设置Sentinel进入TILT状态 sentinel.tilt = 1; // 设置进入TILT状态的开始时间 sentinel.tilt_start_time = mstime(); sentinelEvent(LL_WARNING,"+tilt",NULL,"#tilt mode entered"); } // 设置最近一次执行Sentinel时间处理程序的时间 sentinel.previous_time = mstime(); }当 Sentinel 进入 TILT 模式时,它仍然会继续监视所有目标,但是:

    [*]它不再执行任何操作,比如故障转移。
    [*]当有实例向这个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令时,Sentinel 返回负值:因为这个 Sentinel 所进行的下线判断已经不再准确。
如果 TILT 可以正常维持 30 秒钟,那么 Sentinel 退出 TILT 模式。
3.2 执行周期性任务

我们先来看看在执行周期性任务的函数sentinelHandleDictOfRedisInstances()
 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()函数。
由于这里的操作过多,因此先跳过,单独在标题4进行剖析。
3.3 执行脚本任务

在Sentinel的定时任务分为三步,也就是sentinelTimer()哨兵模式主函数中的三个函数:

    [*]sentinelRunPendingScripts():运行在队列中等待的脚本。
    [*]sentinelCollectTerminatedScripts():清理已成功执行的脚本,重试执行错误的脚本。
    [*]sentinelKillTimedoutScripts():杀死执行超时的脚本,等到下个周期在sentinelCollectTerminatedScripts()函数中重试执行。
3.3.1 准备脚本

我们先来说明脚本任务是如何加入到sentinel.scripts_queue中的。
首先在Sentinel中有两种脚本,分别是,都定义在sentinelRedisInstance结构中

    [*]通知admin的脚本。char *notification_script
    [*]重配置client的脚本。char *client_reconfig_script
在发生主从切换后,会调用sentinelCallClientReconfScript()函数,将重配置client的脚本放入脚本队列中。
在发生LL_WARNING级别的事件通知时,会调用sentinelEvent()函数,将通知admin的脚本放入脚本队列中。
然而这两个函数,都会调用最底层的sentinelScheduleScriptExecution()函数将脚本添加到脚本链表队列中。该函数源码如下:
 #define SENTINEL_SCRIPT_MAX_ARGS 16 // 将给定参数和脚本放入用户脚本队列中 void sentinelScheduleScriptExecution(char *path, ...) { va_list ap; char *argv[SENTINEL_SCRIPT_MAX_ARGS+1]; int argc = 1; sentinelScriptJob *sj; va_start(ap, path); // 将参数保存到argv中 while(argc < SENTINEL_SCRIPT_MAX_ARGS) { argv[argc] = va_arg(ap,char*); if (!argv[argc]) break; argv[argc] = sdsnew(argv[argc]); /* Copy the string. */ argc++; } va_end(ap); // 第一个参数是脚本的路径 argv[0] = sdsnew(path); // 分配脚本任务结构的空间 sj = zmalloc(sizeof(*sj)); sj->flags = SENTINEL_SCRIPT_NONE; //脚本限制 sj->retry_num = 0; //执行次数 sj->argv = zmalloc(sizeof(char*)*(argc+1)); //参数列表 sj->start_time = 0; //开始时间 sj->pid = 0; //执行脚本子进程的pid // 设置脚本的参数列表 memcpy(sj->argv,argv,sizeof(char*)*(argc+1)); // 添加到脚本队列中 listAddNodeTail(sentinel.scripts_queue,sj); /* Remove the oldest non running script if we already hit the limit. */ // 如果队列长度大于256个,那么删除最旧的脚本,只保留255个 if (listLength(sentinel.scripts_queue) > SENTINEL_SCRIPT_MAX_QUEUE) { listNode *ln; listIter li; listRewind(sentinel.scripts_queue,&li); // 遍历脚本链表队列 while ((ln = listNext(&li)) != NULL) { sj = ln->value; // 跳过正在执行的脚本 if (sj->flags & SENTINEL_SCRIPT_RUNNING) continue; /* The first node is the oldest as we add on tail. */ // 删除最旧的脚本 listDelNode(sentinel.scripts_queue,ln); // 释放一个脚本任务结构和所有关联的数据 sentinelReleaseScriptJob(sj); break; } serverAssert(listLength(sentinel.scripts_queue) value; pid_t pid; /* Skip if already running. */ // 跳过正在运行的脚本 if (sj->flags & SENTINEL_SCRIPT_RUNNING) continue; /* Skip if it's a retry, but not enough time has elapsed. */ // 该脚本没有到达重新执行的时间,跳过 if (sj->start_time && sj->start_time > now) continue; // 设置正在执行标志 sj->flags |= SENTINEL_SCRIPT_RUNNING; // 开始执行时间 sj->start_time = mstime(); // 执行次数加1 sj->retry_num++; // 创建子进程执行 pid = fork(); // fork()失败,报告错误 if (pid == -1) { sentinelEvent(LL_WARNING,"-script-error",NULL, "%s %d %d", sj->argv[0], 99, 0); sj->flags &= ~SENTINEL_SCRIPT_RUNNING; sj->pid = 0; // 子进程执行的代码 } else if (pid == 0) { /* Child */ // 执行该脚本 execve(sj->argv[0],sj->argv,environ); /* If we are here an error occurred. */ // 如果执行_exit(2),表示发生了错误,不能重新执行 _exit(2); /* Don't retry execution. */ // 父进程,更新脚本的pid,和同时执行脚本的个数 } else { sentinel.running_scripts++; sj->pid = pid; // 并且通知事件 sentinelEvent(LL_DEBUG,"+script-child",NULL,"%ld",(long)pid); } } }因为Redis是单线程架构的,所以和持久化一样,执行脚本需要创建一个子进程。

    [*]子进程:执行没有正在执行和已经到了执行时间的脚本任务。
    [*]父进程:更新脚本的信息。例如:正在执行的个数和执行脚本的子进程的pid等等。
父进程更新完脚本的信息后就会继续执行下一个sentinelCollectTerminatedScripts()函数
3.3.3 脚本清理工作


    [*]如果在子进程执行的脚本已经执行完成,则可以从脚本队列中将其删除。
    [*]如果在子进程执行的脚本执行出错,但是可以在规定时间后重新执行,那么设置其执行的时间,下个周期重新执行。
    [*]如果在子进程执行的脚本执行出错,但是无法在执行,那么也会脚本队里中将其删除。
函数sentinelCollectTerminatedScripts()源码如下:
 void sentinelCollectTerminatedScripts(void) { int statloc; pid_t pid; // 接受子进程退出码 // WNOHANG:如果没有子进程退出,则立刻返回 while ((pid = wait3(&statloc,WNOHANG,NULL)) > 0) { int exitcode = WEXITSTATUS(statloc); int bysignal = 0; listNode *ln; sentinelScriptJob *sj; // 获取造成脚本终止的信号 if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc); sentinelEvent(LL_DEBUG,"-script-child",NULL,"%ld %d %d", (long)pid, exitcode, bysignal); // 根据pid查找并返回正在运行的脚本节点 ln = sentinelGetScriptListNodeByPid(pid); if (ln == NULL) { serverLog(LL_WARNING,"wait3() returned a pid (%ld) we can't find in our scripts execution queue!", (long)pid); continue; } sj = ln->value; // 如果退出码是1并且没到脚本最大的重试数量 if ((bysignal || exitcode == 1) && sj->retry_num != SENTINEL_SCRIPT_MAX_RETRY) { // 取消正在执行的标志 sj->flags &= ~SENTINEL_SCRIPT_RUNNING; sj->pid = 0; // 设置下次执行脚本的时间 sj->start_time = mstime() + sentinelScriptRetryDelay(sj->retry_num); // 脚本不能重新执行 } else { // 发送脚本错误的事件通知 if (bysignal || exitcode != 0) { sentinelEvent(LL_WARNING,"-script-error",NULL, "%s %d %d", sj->argv[0], bysignal, exitcode); } // 从脚本队列中删除脚本 listDelNode(sentinel.scripts_queue,ln); // 释放一个脚本任务结构和所有关联的数据 sentinelReleaseScriptJob(sj); // 目前正在执行脚本的数量减1 sentinel.running_scripts--; } } }3.3.4 杀死超时脚本

Sentinel规定一个脚本最多执行60s,如果执行超时,则会杀死正在执行的脚本。
 void sentinelKillTimedoutScripts(void) { listNode *ln; listIter li; mstime_t now = mstime(); listRewind(sentinel.scripts_queue,&li); // 遍历脚本队列 while ((ln = listNext(&li)) != NULL) { sentinelScriptJob *sj = ln->value; // 如果当前脚本正在执行且执行,且脚本执行的时间超过60s if (sj->flags & SENTINEL_SCRIPT_RUNNING && (now - sj->start_time) > SENTINEL_SCRIPT_MAX_RUNTIME) { // 发送脚本超时的事件 sentinelEvent(LL_WARNING,"-script-timeout",NULL,"%s %ld", sj->argv[0], (long)sj->pid); // 杀死执行脚本的子进程 kill(sj->pid,SIGKILL); } } }3.4 脑裂

在Redis的官方Sentinel文档中给出了一种关于脑裂的场景。
 +----+ +----+ | M1 |---------| R1 | | S1 | | S2 | +----+ +----+ Configuration: quorum = 1 // M1是主节点 // R1是从节点 // S1、S2是哨兵节点在此种情况中,如果主节点M1出现故障,那么R1将被晋升为主节点,因为两个Sentinel节点可以就配置的quorum = 1达成一致,并且会执行故障转移操作。如下图所示:
 +----+ +------+ | M1 |----//-----| [M1] | | S1 | | S2 | +----+ +------+如果执行了故障转移之后,就会完全以对称的方式创建了两个主节点。客户端可能会不明确的写入数据到两个主节点,这就可能造成很多严重的后果,例如:争抢服务器的资源,争抢应用服务,数据损坏等等。
因此,最好不要进行这样的部署。
在哨兵模式的主函数sentinelTimer(),为了防止这样的部署造成的一些后果,所以每次执行后都会更改服务器的周期任务执行频率,如下所述:
 server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;不断改变Redis定期任务的执行频率,以便使每个Sentinel节点都不同步,这种不确定性可以避免Sentinel在同一时间开始完全继续保持同步,当被要求进行投票时,一次又一次在同一时间进行投票,因为脑裂导致有可能没有胜选者。
4&#46; 哨兵的使命

sentinel.c文件详细注释:Redis Sentinel详细注释
该部分在Redis Sentinel实现(下)中单独剖析。

本文内容来源文水手机台,如有侵权请立即与我们联系,我们将及时处理!
分享:
游客
要评论请先登录 或者 注册
返回顶部