哨兵调用重配置脚本
哨兵客户端重配置脚本参数:sentinel client-reconfig-script
哨兵节点sentinel.conf中配置重配置脚本参数,可以指定重配置脚本的路径。当主节点因发生failover而切换时,哨兵节点会调用这个脚本
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
脚本传入的参数:
start,leader或observer
void sentinelCallClientReconfScript(sentinelRedisInstance *master, int role, char *state, sentinelAddr *from, sentinelAddr *to) {
char fromport[32], toport[32];
if (master->client_reconfig_script == NULL) return;
ll2string(fromport,sizeof(fromport),from->port);
ll2string(toport,sizeof(toport),to->port);
// 将脚本添加到待执行队列中
sentinelScheduleScriptExecution(master->client_reconfig_script,
master->name,
(role == SENTINEL_LEADER) ? "leader" : "observer",
state, announceSentinelAddr(from), fromport,
announceSentinelAddr(to), toport, NULL);
}
void sentinelScheduleScriptExecution(char *path, ...) {
sentinelScriptJob *sj;
// 添加到队列尾部,队列为sentinel.scripts_queue
listAddNodeTail(sentinel.scripts_queue,sj);
}
每个哨兵都有一个脚本执行队列,脚本执行队列中存放的是脚本执行任务,每个任务都包含脚本路径、参数等。
struct sentinelState {
int running_scripts; /* Number of scripts in execution right now. */
list *scripts_queue; /* Queue of user scripts to execute. */
// ...
} sentinel;
脚本执行任务会通过sentinelRunPendingScripts函数执行,该函数会遍历脚本执行队列,并执行第一个任务。
/* Run pending scripts if we are not already at max number of running
* scripts. */
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();
sj->retry_num++;
pid = fork(); // fork子进程去执行脚本任务
if (pid == -1) {
/* Parent (fork error).
* We report fork errors as signal 99, in order to unify the
* reporting with other kind of errors. */
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 */
tlsCleanup();
// 子进程执行脚本
execve(sj->argv[0],sj->argv,environ);
/* If we are here an error occurred. */
_exit(2); /* Don't retry execution. */
} else {
// 父进程
sentinel.running_scripts++;
sj->pid = pid;
sentinelEvent(LL_DEBUG,"+script-child",NULL,"%ld",(long)pid);
}
}
}
谁会调用sentinelRunPendingScripts函数呢?是serverCron函数中通过sentinelTimer会调用。
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
/* Run the Sentinel timer if we are in sentinel mode. */
if (server.sentinel_mode) sentinelTimer();
}
void sentinelTimer(void) {
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 = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}
serverCron是Redis事件循环中注册的定时事件处理器,承担着服务端后台维护任务的核心职责:
main(int argc, char **argv) {
initServer() {
// 注册定时任务,serverCron
aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR);
}
aeMain(server.el); // 启动事件循环
}
什么时候会触发重配置脚本?
hello消息中获取到主节点变更信息后触发
当Sentinel节点订阅的__sentinel__:hello通道收到消息时触发,这是Sentinel节点间通信的核心机制。
通过Sentinel的PUBLISH命令:Sentinel会处理发送到__sentinel__:hello通道的PUBLISH命令
void sentinelReceiveHelloMessages(redisAsyncContext *c, void *reply, void *privdata) {
// 获取hello消息,处理hello消息
sentinelProcessHelloMessage(r->element[2]->str, r->element[2]->len);
}
这个函数处理通过Pub/Sub从master或slave实例接收到的hello消息,或者通过Sentinel的(fake) PUBLISH命令直接发送到这个sentinel的消息。如果消息中指定的master名称未知,该消息将被丢弃。
void sentinelProcessHelloMessage(char *hello, int hello_len) {
if (numtokens == 8) {
// 如果收到的master配置epoch比本地的epoch新
/* Update master info if received configuration is newer. */
if (si && master->config_epoch < master_config_epoch) {
// 更新master的epoch
master->config_epoch = master_config_epoch;
// 如果master地址或者端口变更
if (master_port != master->addr->port ||
!sentinelAddrEqualsHostname(master->addr, token[5]))
{
sentinelAddr *old_addr;
sentinelEvent(LL_WARNING,"+config-update-from",si,"%@");
sentinelEvent(LL_WARNING,"+switch-master",
master,"%s %s %d %s %d",
master->name,
announceSentinelAddr(master->addr), master->addr->port,
token[5], master_port);
old_addr = dupSentinelAddr(master->addr);
// 更新master地址
sentinelResetMasterAndChangeAddress(master, token[5], master_port);
// 运行重配置脚本
sentinelCallClientReconfScript(master,
SENTINEL_OBSERVER,"start",
old_addr,master->addr);
releaseSentinelAddr(old_addr);
}
}
}
}
Sentinel从监控的Redis实例收到Info命令
sentinel从监控的redis实例收到info命令时触发,处理info信息。
void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) {
sds *lines;
// 处理master变为slave的情况
/* Handle master -> slave role switch. */
if ((ri->flags & SRI_MASTER) && role == SRI_SLAVE) {
/* Nothing to do, but masters claiming to be slaves are
* considered to be unreachable by Sentinel, so eventually
* a failover will be triggered. */
}
// 处理slave变为master的情况
/* Handle slave -> master role switch. */
if ((ri->flags & SRI_SLAVE) && role == SRI_MASTER) {
/* If this is a promoted slave we can change state to the
* failover state machine. */
// 故障转移成功完成,被提升的slave报告自己是master,
if ((ri->flags & SRI_PROMOTED) &&
(ri->master->flags & SRI_FAILOVER_IN_PROGRESS) &&
(ri->master->failover_state ==
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION))
{
/* Now that we are sure the slave was reconfigured as a master
* set the master configuration epoch to the epoch we won the
* election to perform this failover. This will force the other
* Sentinels to update their config (assuming there is not
* a newer one already available). */
ri->master->config_epoch = ri->master->failover_epoch;
ri->master->failover_state = SENTINEL_FAILOVER_STATE_RECONF_SLAVES;
ri->master->failover_state_change_time = mstime();
sentinelFlushConfig();
sentinelEvent(LL_WARNING,"+promoted-slave",ri,"%@");
if (sentinel.simfailure_flags &
SENTINEL_SIMFAILURE_CRASH_AFTER_PROMOTION)
sentinelSimFailureCrash();
sentinelEvent(LL_WARNING,"+failover-state-reconf-slaves",
ri->master,"%@");
// 运行客户端重配置脚本
sentinelCallClientReconfScript(ri->master,SENTINEL_LEADER,
"start",ri->master->addr,ri->addr);
// 发送hello消息更新集群视图
sentinelForceHelloUpdateForMaster(ri->master);
} else {
// 意外的slave变为master,不是预期的故障转移结果,尝试将该实例重新配置为slave
/* A slave turned into a master. We want to force our view and
* reconfigure as slave. Wait some time after the change before
* going forward, to receive new configs if any. */
mstime_t wait_time = sentinel_publish_period*4;
if (!(ri->flags & SRI_PROMOTED) &&
sentinelMasterLooksSane(ri->master) &&
sentinelRedisInstanceNoDownFor(ri,wait_time) &&
mstime() - ri->role_reported_time > wait_time)
{
int retval = sentinelSendSlaveOf(ri,ri->master->addr);
if (retval == C_OK)
sentinelEvent(LL_NOTICE,"+convert-to-slave",ri,"%@");
}
}
}
}