在上一篇文章中我们介绍了 Sentinel 的初始化和主循环,这一篇我们介绍Sentinel 的网络连接以及 tilt 模式。 Sentinel的网络连接 在前面的文章曾经提到每个 Sentinel 实例会维护与所监测的主从实例的两个连接,分别是命令连接(Command Connection)和发布订阅连接(Pub/Sub Connection)。但是需要注意的是,Sentinel 和其他Sentinel 之间只有一个命令连接。下面将分别介绍命令连接和发布订阅连接的作用。 命令连接 Sentinel 维护命令连接是为了与其他主从实例以及 Sentinel 实例通过发送接收命令的方式进行通信,例如:

  1. Sentinel会默认以每1s 间隔发送 PING 命令给其他实例以主观判断其他实例是否下线。
  2. Sentinel会通过Sentinel 和主实例之间的命令连接每隔 10s 发送 INFO 命令给主从实例以得到主实例和从实例的最新信息。
  3. 在主实例下线的情况下,Sentinel 会通过 Sentinel 和从实例的命令连接发送 SLAVEOF NO ONE 命令给选定的从实例从而使从实例提升为新的主节点。
  4. Sentinel会默认每隔1s 发送 is-master-down-by-addr 命令以询问其他Sentinel 节点关于监控的主节点是否下线。 在 sentinel.c 中的 sentinelReconnectInstance函数中,命令连接的初始化如下:

/* Commands connection. */  
    if (link->cc == NULL) {  
        link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);  
        if (!link->cc->err && server.tls_replication &&  
                (instanceLinkNegotiateTLS(link->cc) == C_ERR)) {  
            sentinelEvent(LL_DEBUG,"-cmd-link-reconnection",ri,"%@ #Failed to initialize TLS");  
            instanceLinkCloseConnection(link,link->cc);  
        } else if (link->cc->err) {  
            sentinelEvent(LL_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",  
                link->cc->errstr);  
            instanceLinkCloseConnection(link,link->cc);  
        } else {  
            link->pending_commands = 0;  
            link->cc_conn_time = mstime();  
            link->cc->data = link;  
            redisAeAttach(server.el,link->cc);  
            redisAsyncSetConnectCallback(link->cc,  
                    sentinelLinkEstablishedCallback);  
            redisAsyncSetDisconnectCallback(link->cc,  
                    sentinelDisconnectCallback);  
            sentinelSendAuthIfNeeded(ri,link->cc);  
            sentinelSetClientName(ri,link->cc,"cmd");  
  
            /* Send a PING ASAP when reconnecting. */  
            sentinelSendPing(ri);  
        }  
   }

发布订阅连接 Sentinel 维护和其他主从节点的发布订阅连接作用是为了获知其他监控相同主从实例的Sentinel 实例的存在,并且从其他 Sentinel 实例中更新对所监控的主从实例以及发送的Sentinel 实例的认知。例如在主备倒换完成后,其他 Sentinel 通过读取领头的Sentinel 的频道消息来更新新的主节点的相关信息(地址,端口号等)。 Sentinel 在默认每隔 2 秒钟会发送Hello 消息包到其对应的主从实例的__sentinel__:hello 频道中。Hello 消息格式如下: _sentinel:hello <sentinel 地址> <sentinel 端口号> <sentinel 运行id> <sentinel 配置纪元> <主节点名字 > <主节点地址> <主节点端口号> <主节点配置纪元> 当 Sentinel 通过订阅连接收到其他 Sentinel 发送的的 Hello 包时,会更新对主从节点以及 S 发送 Sentinel 的认知,如果收到自己发送的 Hello 包,则简单的丢弃不做任何处理。这部分代码逻辑是在 sentinel.c 中的sentinelProcessHelloMessage 函数中定义的,由于篇幅原因在这里不做详细介绍。 在 sentinel.c 中的 sentinelReconnectInstance函数中,发布订阅连接初始化如下:


/* 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 && server.tls_replication &&  
               (instanceLinkNegotiateTLS(link->pc) == C_ERR)) {  
           sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #Failed to initialize TLS");  
       } else if (link->pc->err) {  
           sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",  
               link->pc->errstr);  
           instanceLinkCloseConnection(link,link->pc);  
       } else {  
           int retval;  
  
           link->pc_conn_time = mstime();  
           link->pc->data = link;  
           redisAeAttach(server.el,link->pc);  
           redisAsyncSetConnectCallback(link->pc,  
                   sentinelLinkEstablishedCallback);  
           redisAsyncSetDisconnectCallback(link->pc,  
                   sentinelDisconnectCallback);  
           sentinelSendAuthIfNeeded(ri,link->pc);  
           sentinelSetClientName(ri,link->pc,"pubsub");  
           /* Now we subscribe to the Sentinels "Hello" channel. */  
           retval = redisAsyncCommand(link->pc,  
               sentinelReceiveHelloMessages, ri, "%s %s",  
               sentinelInstanceMapCommand(ri,"SUBSCRIBE"),  
               SENTINEL_HELLO_CHANNEL);  
           if (retval != C_OK) {  
               /* If we can't subscribe, the Pub/Sub connection is useless 
                * and we can simply disconnect it and try again. */  
               instanceLinkCloseConnection(link,link->pc);  
               return;  
           }  
       }  
   }

is-master-down-by-addr 命令 Sentinel 会默认每隔 1s 通过命令连接发送is-master-down-by-addr 命令以询问其他 Sentinel节点关于监控的主节点是否下线。另外,在主实例下线的情况下,Sentinel 之间也通过 is-master-down-by-addr 命令来获得投票并选举领头 Sentinel。is-master-down-by-addr 格式如下: is-master-down-by-addr: <主实例地址> <主实例端口号> <当前配置纪元> <运行 ID> 如果不是在选举领头 Sentinel 过程中, <runid>项总为*,相反地,如果在 Sentinel 向其他 Sentinel 发送投票请求情况下,<runid>项为自己的运行 id。这部分代码如下:

  
   	if ((master->flags & SRI_S_DOWN) == 0) continue;  
   if (ri->link->disconnected) continue;  
   if (!(flags & SENTINEL_ASK_FORCED) &&  
       mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)  
       continue;  
 
   /* Ask */  
   ll2string(port,sizeof(port),master->addr->port);  
   retval = redisAsyncCommand(ri->link->cc,  
               sentinelReceiveIsMasterDownReply, ri,  
               "%s is-master-down-by-addr %s %s %llu %s",  
               sentinelInstanceMapCommand(ri,"SENTINEL"),  
               master->addr->ip, port,  
               sentinel.current_epoch,  
               (master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?  
               sentinel.myid : "*");  
   if (retval == C_OK) ri->link->pending_commands++;
   	

is-master-down-by-addr 的命令回复格式如下:

  1. <主节点下线状态>
  2. <领头 Sentinel 运行 ID >
  3. <领头 Sentinel 配置纪元> Sentinel 在收到其他 Sentinel命令回复后,会记录其他 Sentinel 回复的主实例在线状态信息,以及在选举领头 Sentinel 过程中的投票情况,这部分的代码逻辑定义在 sentinel.c 中的sentinelReceiveIsMasterDownByReply 函数:

/* Ignore every error or unexpected reply. 
 * Note that if the command returns an error for any reason we'll 
 * end clearing the SRI_MASTER_DOWN flag for timeout anyway. */  
  
if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 &&  
    r->element[0]->type == REDIS_REPLY_INTEGER &&  
    r->element[1]->type == REDIS_REPLY_STRING &&  
    r->element[2]->type == REDIS_REPLY_INTEGER)  
{  
    ri->last_master_down_reply_time = mstime();  
    if (r->element[0]->integer == 1) {  
        ri->flags |= SRI_MASTER_DOWN;  
    } else {  
        ri->flags &= ~SRI_MASTER_DOWN;  
    }  
    if (strcmp(r->element[1]->str,"*")) {  
        /* If the runid in the reply is not "*" the Sentinel actually 
         * replied with a vote. */  
        sdsfree(ri->leader);  
        if ((long long)ri->leader_epoch != r->element[2]->integer)  
            serverLog(LL_WARNING,  
                "%s voted for %s %llu", ri->name,  
                r->element[1]->str,  
                (unsigned long long) r->element[2]->integer);  
        ri->leader = sdsnew(r->element[1]->str);  
        ri->leader_epoch = r->element[2]->integer;  
    }  
}

Tilt模式 Sentinel 的 Tilt 模式会在以下两种情况下开启:

  1. Sentinel进程被阻塞超过SENTINEL_TILT_TRIGGER 时间(默认为 2s),可能因为进程或系统I/O(内存,网络,存储)请求过多。
  2. 系统时钟调整到之前某个时间值。 Tilt 模式是一种保护机制,处于该模式下 Sentinel 除了发送必要的 PING 及 INFO 命令外,不会主动做其他操作,例如主备倒换,标志主观、客观下线等。但可以通过 INFO命令及发布订阅连接的 HELLO 消息包来获取外界信息并对自身结构进行更新,直到 SENTINEL_TILT_PERIOD 时长(默认为 30s)结束为止,我们可以认为Tilt 模式是 Sentinel 的被动模式。 判断 Tilt 模式的代码逻辑定义如下:

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