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()函数中实现。