Redis Sentinel原理与实现 (中)

时间:2022-07-23
本文章向大家介绍Redis Sentinel原理与实现 (中),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Sentinel源码分析

在启动Redis时可以传入“--sentinel”参数来启动Sentinel,在main()函数中可以看到处理Sentinel的逻辑,如下:

int main(int argc, char **argv) {
 ...
    server.sentinel_mode = checkForSentinelMode(argc,argv);
 ...
 if (server.sentinel_mode) {
        initSentinelConfig();
        initSentinel();
    }
 ...
 if (!server.sentinel_mode) {
        ...
    } else {
        sentinelIsRunning();
    }
 ...
    aeMain(server.el);
    aeDeleteEventLoop(server.el);
 return 0;
}

在main()函数中,首先会判断是否以Sentinel模式启动的,如果是就调用initSentinelConfig()函数载入Sentinel的配置文件,接着调用initSentinel()函数初始化Sentinel的运行环境。我们看看initSentinel()做了哪些初始化处理吧:

void initSentinel(void) {
 unsigned int j;
 /* Remove usual Redis commands from the command table, then just add
     * the SENTINEL command. */
    dictEmpty(server.commands,NULL);
 for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
 int retval;
 struct redisCommand *cmd = sentinelcmds+j;
        retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
        serverAssert(retval == DICT_OK);
    }
 /* Initialize various data structures. */
 sentinel.current_epoch = 0;
 sentinel.masters = dictCreate(&instancesDictType,NULL);
 sentinel.tilt = 0;
 sentinel.tilt_start_time = 0;
 sentinel.previous_time = mstime();
 sentinel.running_scripts = 0;
 sentinel.scripts_queue = listCreate();
 sentinel.announce_ip = NULL;
 sentinel.announce_port = 0;
 sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE;
    memset(sentinel.myid,0,sizeof(sentinel.myid));
}

在initSentinel()函数中,首先把Sentinel服务器可用的命令导入到命令表中,这就是为什么Sentinel与普通Redis所支持的命令不一样的原因。接着初始化sentinel变量的成员,sentinel的masters成员变量保存的是Sentinel服务器监控的主Redis服务器。

初始化完Sentinel的运行环境后,程序会调用aeMain()函数进入主循环。这时Sentinel服务器主动去连接被监控的Redis服务器。

主动连接Redis服务器在serverCron()函数中实现,我们看看这个函数的代码:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
 ...
    run_with_period(100) {
 if (server.sentinel_mode) sentinelTimer();
    }
 ...
}

如果以Sentinel模式运行,serverCron()函数会调用sentinelTimer()函数。我们来看看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;
}

sentinelTimer()函数是Sentinel的核心内容,下面我们会对这个定时器作深入的分析。

TITL模式

sentinelCheckTiltCondition()函数用于检查Sentinel是否进入TITL模式。Sentinel非常依赖系统时间,例如它会使用系统时间来判断一个PING回复用了多久的时间。然而,假如系统时间被修改了,或者是系统十分繁忙,或者是进程堵塞了,Sentinel可能会出现运行不正常的情况。

当系统的稳定性下降时,TILT模式是Sentinel可以进入的一种的保护模式。当进入TILT模式时,Sentinel会继续监控工作,但是它不会有任何其他动作,它也不会去回应is-master-down-by-addr这样的命令了,因为它在TILT模式下,检测失效节点的能力已经变得让人不可信任了。如果系统恢复正常,持续30秒钟,Sentinel就会退出TITL模式。

我们来看看怎么进入TITL模式的:

void sentinelCheckTiltCondition(void) {
 mstime_t now = mstime();
 mstime_t delta = now - sentinel.previous_time;
 if (delta < 0 || delta > SENTINEL_TILT_TRIGGER) { // 2000
 sentinel.tilt = 1;
 sentinel.tilt_start_time = mstime();
        sentinelEvent(LL_WARNING,"+tilt",NULL,"#tilt mode entered");
    }
 sentinel.previous_time = mstime();
}

从代码可知,当时间发生倒退或者处理时间超过SENTINEL_TITL_TRIGGER(2秒)时便会进入TITL模式。

监控检测

sentinelHandleDictOfRedisInstances()函数的主要工作是遍历所有Redis和Sentinel服务器,并调用sentinelHandleRedisInstance()对其进行处理,sentinelHandleRedisInstance()函数的主要工作包括:

1.建立与Redis服务器或者其他Sentinel服务器的连接。

2.发生info/ping/hello等消息。

3.检查主Redis服务器是否下线,如果下线便进行故障转移。

下面看看sentinelHandleRedisInstance()函数的代码:

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
 /* ========== MONITORING HALF ============ */
 /* Every kind of instance */
    sentinelReconnectInstance(ri);  // 建立与其他服务器的连接
    sentinelSendPeriodicCommands(ri); // 发送info/ping/hello消息
 /* ============== 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. */
 if (sentinel.tilt) {
 if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
 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) { // 如果是master服务器
        sentinelCheckObjectivelyDown(ri);
 if (sentinelStartFailoverIfNeeded(ri))
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        sentinelFailoverStateMachine(ri);
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

在Sentinel内部,对于每个Redis或者其他Sentinel服务器都会使用使用一个sentinelRedisInstance的结构体来保存其数据与信息。sentinelRedisInstance结构体的成员比较多,我们要注意的是以下几个成员:

typedef struct sentinelRedisInstance {
 ...
 /* Master specific. */
    dict *sentinels; /* Other sentinels monitoring the same master. */
 dict *slaves;       /* Slaves for this master instance. */
 ...
 struct sentinelRedisInstance *master; /* Master instance if it's slave. */
 ...
} sentinelRedisInstance;

sentinels成员保存的是监控这个服务器的Sentinel服务器列表,slaves成员保存的是这台服务器的从服务器列表(如果是主服务器的话),master成员保存的是这台服务器的主服务器(如果是从服务器的话)。

我们可以通过下图形象的表示它们之间的关系:

从上图可以知道,Sentinel通过这个关系可以找到所有监控的服务器(包括主从Redis服务器和其他Sentinel服务器)。有了这些信息,Sentinel就可以方便地对这些服务器进行监控和通信。

sentinelHandleRedisInstance()调用了sentinelReconnectInstance()与其他服务器进行连接。sentinelReconnectInstance()的主要工作是为对端服务器建立连接。下面来看看代码:

void sentinelReconnectInstance(sentinelRedisInstance *ri) {
 ...
 if (link->cc == NULL) {
        link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
 if (link->cc->err) {
            ...
        } else {
            ...
            redisAsyncSetConnectCallback(link->cc,
                    sentinelLinkEstablishedCallback);
            redisAsyncSetDisconnectCallback(link->cc,
                    sentinelDisconnectCallback);
            ...
            sentinelSendPing(ri);
        }
    }
 /* Pub / Sub */
 if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && link->pc == NULL) {
 link->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
 if (link->pc->err) {
 ...
        } else {
 ...
            redisAsyncSetConnectCallback(link->pc,
                    sentinelLinkEstablishedCallback);
            redisAsyncSetDisconnectCallback(link->pc,
 sentinelDisconnectCallback);
 ...
            retval = redisAsyncCommand(link->pc,
                sentinelReceiveHelloMessages, ri, "SUBSCRIBE %s",
                    SENTINEL_HELLO_CHANNEL);
 ...
        }
    }
 ...
}

sentinelReconnectInstance()函数首先与对端服务器建立一个命令连接,然后发送一个ping命令给对端服务器。如果对端服务器是一个Redis实例,那还会建立一个发布订阅连接,用于订阅其“Hello频道”。

Sentinel服务器会定时向“Hello频道”发布一些监测数据,订阅“Hello频道”的服务器可以从中获取Sentinel的监测结果。如下图:

Sentinel服务器向“Hello频道”发布的数据包括:当前Sentinel的IP地址、端口、runid和当前配置版本,以及被监控Reids的名称、IP、端口和当前配置版本。所以,监听同一台Redis服务器的所有Sentinel可以通过“Hello频道”来互相交换信息。新加入到集群的Sentinel只需要监听“Hello频道”就可以知道所有Sentinel的IP地址和端口,从而可以主动连接它们,这就是Sentinel的自动发现机制。

那么在什么时候Sentinel会向“Hello频道”发送消息呢?Sentinel通过在定时器sentinelTimer()中调用sentinelSendHello()来想“Hello频道”发送消息。

在sentinelHandleRedisInstance()函数中还有一个重要的处理,就是监控Redis是否下线。在sentinelHandleRedisInstance()函数中调用了sentinelCheckSubjectivelyDown()函数,其作用是检查Redis服务器是否主观下线。主观下线的意思是指只有当前Sentinel认为监控的Redis下线了,此时需要询问其他Sentinel服务器是否也认为此Redis下线才能确认为主观下线。

检测Redis是否主观下线的方法是:通过发送ping命令给Redis服务器,如果Redis服务器在一定时间内还没回复,那么就可以认为是主观下线。在定时器sentinelTimer()中会调用sentinelSendPing()发送ping命令给Redis服务器,定时发送ping命令称为“心跳”机制。

我们来看看sentinelCheckSubjectivelyDown()代码实现:

void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
 mstime_t elapsed = 0;
 if (ri->link->act_ping_time)
        elapsed = mstime() - ri->link->act_ping_time;
 else if (ri->link->disconnected)
        elapsed = mstime() - ri->link->last_avail_time;
 ...
 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) {
            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);
        }
    }
}

从上面的代码可以知道,如果Redis服务器在一定的时间内没有响应ping命令,那么就把Redis标识为主观下线(添加SRI_S_DOWN标志)。

如果被监控的服务器是Redis主服务器,那么还需要检测其是否客观下线。这因为主观下线有可能不可靠(如Sentinel本身网络不通),所以必须通过检测其为客观下线才能认为是真正下线,Sentinel通过sentinelCheckObjectivelyDown()函数检测Redis主服务器是否客观下线。代码如下:

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
 dictIterator *di;
 dictEntry *de;
 unsigned int quorum = 0, odown = 0;
 if (master->flags & SRI_S_DOWN) {
 /* Is down for enough sentinels? */
        quorum = 1; /* the current sentinel. */
 /* Count all the other sentinels. */
        di = dictGetIterator(master->sentinels);
 while((de = dictNext(di)) != NULL) {
 sentinelRedisInstance *ri = dictGetVal(de);
 if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
        dictReleaseIterator(di);
 if (quorum >= master->quorum) odown = 1;
    }
 /* Set the flag accordingly to the outcome. */
 if (odown) {
 if ((master->flags & SRI_O_DOWN) == 0) {
            sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
            master->flags |= SRI_O_DOWN;
            master->o_down_since_time = mstime();
        }
    } else {
 if (master->flags & SRI_O_DOWN) {
            sentinelEvent(LL_WARNING,"-odown",master,"%@");
            master->flags &= ~SRI_O_DOWN;
        }
    }
}

从上面的代码首先判断Redis主服务器是否主观下线(也就是判断是否被标志位SRI_S_DOWN),如果被认为是主观下线,那么就遍历监控这台主服务器的所有Sentinel,然后判断它们是否也认为是主观下线,如果认为主观下线的Sentinel数大于quorum(我们在配置文件设置的,讲解配置的时候讲过),那么就认为是客观下线。

如果Redis主服务器被认为是客观下线,那么就开始故障转移。通过调用sentinelStartFailover()函数可以一次故障转移。sentinelStartFailover()代码如下:

void sentinelStartFailover(sentinelRedisInstance *master) {
    master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
    master->flags |= SRI_FAILOVER_IN_PROGRESS;
    master->failover_epoch = ++sentinel.current_epoch;
    sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",
        (unsigned long long) sentinel.current_epoch);
    sentinelEvent(LL_WARNING,"+try-failover",master,"%@");
    master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
    master->failover_state_change_time = mstime();
}

sentinelStartFailover()函数的代码很简单,只是把故障转移状态设置为SENTINEL_FAILOVER_STATE_WAIT_START表示等待开始故障转移,而真正的故障转移在sentinelFailoverStateMachine()函数中实现。