Redis源码剖析和注释(二十三)--- Redis Sentinel实现(哨兵的执行过程和执行的内容)
2017-05-29 23:37
911 查看
Redis Sentinel实现(上)
1. Redis Sentinel 介绍和部署
请参考Redis Sentinel 介绍与部署sentinel.c文件详细注释:Redis Sentinel详细注释
本文会分为两篇分别接受
Redis Sentinel的实现,本篇主要将
Redis哨兵的执行过程和执行的内容。
Redis Sentinel实现上
Redis Sentinel 介绍和部署
Redis Sentinel 的执行过程和初始化
1 检查是否开启哨兵模式
2 初始化哨兵的配置
3 载入配置文件
31 创建实例
32 查找主节点
4 开启 Sentinel
Redis Sentinel 的所有操作
1 TILT 模式判断
2 执行周期性任务
3 执行脚本任务
31 准备脚本
32 执行脚本
33 脚本清理工作
34 杀死超时脚本
4 脑裂
哨兵的使命
标题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 <name> <host> <port> <quorum> */ 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) { /* down-after-milliseconds <name> <milliseconds> */ // 获取根据name查找主节点实例 ri = sentinelGetMasterByName(argv[1]); if (!ri) return "No such master with specified name."; // 设置主节点实例的主观下线的判断时间 ri->down_after_period = atoi(argv[2]); if (ri->down_after_period <= 0) return "negative or zero time parameter."; // 根据ri主节点的down_after_period字段的值设置所有连接该主节点的从节点和Sentinel实例的主观下线的判断时间 sentinelPropagateDownAfterPeriod(ri);
载入配置文件主要使用了两个函数
createSentinelRedisInstance()和
sentinelGetMasterByName()。前者用来根据指定监控的主节点来创建实例,而后者则要根据名字找到对应的主节点实例来设置配置的参数。
2.3.1 创建实例
调用createSentinelRedisInstance()函数创建被该哨兵节点所监控的主节点实例,然后将新创建的主节点实例保存到
sentinel.masters字典中,也就是初始化时创建的字典。该函数是一个通用的函数,根据参数
flags不同创建不同类型的实例,并且将实例保存到不同的字典中:
SRI_MASTER:创建一个主节点实例,保存到当前哨兵节点监控的主节点字典中。
SRI_SLAVE:创建一个从节点实例,保存到主节点实例的从节点字典中。
SRI_SENTINE:创建一个哨兵节点实例,保存到其他监控该主节点实例的哨兵节点的字典中。
我们先列出函数的原型:
sentinelRedisInstance *createSentinelRedisInstance(char *name, int flags, char *hostname, int port, int quorum, sentinelRedisInstance *master)
如果flags设置了
SRI_MASTER,该实例被添加进
sentinel.masters表中
如果flags设置了
SRI_SLAVE或者
SRI_SENTINEL,
master一定不为空并且该实例被添加到
master->slaves或
master->sentinels中
如果该实例是从节点或者是哨兵节点,
name参数被忽略,并且被自动设置为
hostname:port
当根据
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 <master-name> <ip> <port> <quorum>配置 unsigned 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. */ /*----------------------------------从节点特有的属性----------------------------------*/ // 从节点复制操作断开时间 mstime_t master_link_down_time; /* Slave replication link down time. */ // 按照INFO命令输出的从节点优先级 int slave_priority; /* Slave priority according to its INFO output. */ // 故障转移时,从节点发送SLAVEOF <new>命令的时间 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命令的回复中记录的主节点的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. 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) <= SENTINEL_SCRIPT_MAX_QUEUE); } }
Redis使用了
sentinelScriptJob结构来管理脚本的一些信息,正如上述代码初始化那一部分。
而且当前哨兵维护的哨兵队列最多只能保留最新的255个脚本,如果脚本过多就会从队列中删除对旧的脚本。
3.3.2 执行脚本
当要执行脚本放入了队列中,等到周期性函数sentinelTimer()时,就会执行。我们来执行脚本的函数
sentinelRunPendingScripts()代码:
void sentinelRunPendingScripts(void) { listNode *ln; listIter li; mstime_t now = mstime(); /* Find jobs that are not running and run them, from the top to the * tail of the queue, so we run older jobs first. */ listRewind(sentinel.scripts_queue,&li); // 遍历脚本链表队列,如果没有超过同一时刻最多运行脚本的数量,找到没有正在运行的脚本 while (sentinel.running_scripts < SENTINEL_SCRIPT_MAX_RUNNING && (ln = listNext(&li)) != NULL) { sentinelScriptJob *sj = ln->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. 哨兵的使命
sentinel.c文件详细注释:Redis Sentinel详细注释
该部分在Redis Sentinel实现(下)中单独剖析。
相关文章推荐
- Redis源码剖析和注释(二十四)--- Redis Sentinel实现(哨兵操作的深入剖析)
- Redis源码剖析和注释(二十一)--- 单机服务器实现
- Redis源码剖析和注释(二十八)--- Redis 事务实现和乐观锁
- Redis源码剖析和注释(十三)--- 有序集合类型键实现(t_zset)
- Redis源码剖析和注释(九)--- 字符串命令的实现(t_string)
- Redis源码剖析和注释(十九)--- Redis 事件处理实现
- Redis源码剖析和注释(十一)--- 哈希键命令的实现(t_hash)
- Redis源码剖析和注释(十)--- 列表键命令实现(t_list)
- Redis源码剖析和注释(十五)---- 通知功能实现与实战 (notify)
- Redis源码剖析和注释(十八)--- Redis AOF持久化机制
- Redis源码剖析和注释(二十二)--- Redis 复制(replicate)源码详细解析
- Redis 3.2.8源码剖析和注释系列文章地址归总
- 结合redis设计与实现的redis源码学习-21-哨兵(Sentinel.c)
- Redis源码剖析和注释(二十六)--- Redis 集群伸缩原理源码剖析
- 从源码剖析一个Spark WordCount Job执行的全过程
- UDT源码剖析(三):UDT::startup()过程代码注释
- 【Redis源码剖析】 - Redis之事务的实现原理
- Redis源码剖析和注释(十六)---- Redis输入输出的抽象(rio)
- Go内核源码剖析 一 程序执行启动过程
- 一个通用的分页存储过程实现-SqlServer(附上sql源码,一键执行即刻搭建运行环境)