Redis Sentinel实现(下)
本文是Redis Sentinel实现(上)篇文章的下半部分剖析。主要剖析以下内容:
4. 哨兵的使命
sentinel.c
文件详细注释:Redis Sentinel详细注释
我们这一部分将会详细介绍标题3.2
小节的内容,以下分析,是站在哨兵节点的视角。
我们当时已经简单分析到
sentinelHandleRedisInstance()
函数用来处理主节点、从节点和哨兵节点的周期性操作。sentinelFailoverSwitchToPromotedSlave()
函数用来处理发生主从切换的情况。
因此,先来看第一部分。
4.1 周期性的操作
我们先来看看sentinelHandleRedisInstance()
函数中到底实现了哪些操作:
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
/* ========== MONITORING HALF ============ */
/* ========== 一半监控操作 ============ */
/* Every kind of instance */
/* 对所有的类型的实例进行操作 */
// 为Sentinel和ri实例创建一个网络连接,包括cc和pc
sentinelReconnectInstance(ri);
// 定期发送PING、PONG、PUBLISH命令到ri实例中
sentinelSendPeriodicCommands(ri);
/* ============== ACTING HALF ============= */
/* ============== 一半故障检测 ============= */
// 如果Sentinel处于TILT模式,则不进行故障检测
if (sentinel.tilt) {
// 如果TILT模式的时间没到,则不执行后面的动作,直接返回
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
// 如果TILT模式时间已经到了,取消TILT模式的标识
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) {
// 检查从节点是否客观下线
sentinelCheckObjectivelyDown(ri);
// 如果处于客观下线状态,则进行故障转移的状态设置
if (sentinelStartFailoverIfNeeded(ri))
// 强制向其他Sentinel节点发送SENTINEL IS-MASTER-DOWN-BY-ADDR给所有的Sentinel获取回复
// 尝试获得足够的票数,标记主节点为客观下线状态,触发故障转移
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 执行故障转移操作
sentinelFailoverStateMachine(ri);
// 主节点ri没有处于客观下线的状态,那么也要尝试发送SENTINEL IS-MASTER-DOWN-BY-ADDR给所有的Sentinel获取回复
// 因为ri主节点如果有回复延迟等等状况,可以通过该命令,更新一些主节点状态
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}
很明显,该函数将周期性的操作分为两个部分,一部分是对一个的实例进行监控的操作,另一部分是对该实例执行故障检测。
接下来就针对上面代码的每一步进行详细剖析,每一个标题对应一步。
4.1.1 建立连接
首先,执行的第一个函数就是sentinelReconnectInstance()
函数,因为在载入配置的时候,我们将创建的主节点实例加入到sentinel.masters
字典的时候,该主节点的连接是关闭的,所以第一件事就是为主节点和哨兵节点建立网络连接。
查看sentinelReconnectInstance()
函数代码:
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
// 如果ri实例没有连接中断,则直接返回
if (ri->link->disconnected == 0) return;
// ri实例地址非法
if (ri->addr->port == 0) return; /* port == 0 means invalid address. */
instanceLink *link = ri->link;
mstime_t now = mstime();
// 如果还没有最近一次重连的时间距离现在太短,小于1s,则直接返回
if (now - ri->link->last_reconn_time < SENTINEL_PING_PERIOD) return;
// 设置最近重连的时间
ri->link->last_reconn_time = now;
/* Commands connection. */
// cc:命令连接
if (link->cc == NULL) {
// 绑定ri实例的连接地址并建立连接
link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
// 命令连接失败,则事件通知,且断开cc连接
if (link->cc->err) {
sentinelEvent(LL_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",
link->cc->errstr);
instanceLinkCloseConnection(link,link->cc);
// 命令连接成功
} else {
// 重置cc连接的属性
link->pending_commands = 0;
link->cc_conn_time = mstime();
link->cc->data = link;
// 将服务器的事件循环关联到cc连接的上下文中
redisAeAttach(server.el,link->cc);
// 设置确立连接的回调函数
redisAsyncSetConnectCallback(link->cc,
sentinelLinkEstablishedCallback);
// 设置断开连接的回调处理
redisAsyncSetDisconnectCallback(link->cc,
sentinelDisconnectCallback);
// 发送AUTH 命令认证
sentinelSendAuthIfNeeded(ri,link->cc);
// 发送连接名字
sentinelSetClientName(ri,link->cc,"cmd");
/* Send a PING ASAP when reconnecting. */
// 立即向ri实例发送PING命令
sentinelSendPing(ri);
}
}
/* Pub / Sub */
// pc:发布订阅连接
// 只对主节点和从节点如果没有设置pc连接则建立一个
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && link->pc == NULL) {
// 绑定指定ri的连接地址并建立连接
link->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
// pc连接失败,则事件通知,且断开pc连接
if (link->pc->err) {
sentinelEvent(LL_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
link->pc->errstr);
instanceLinkCloseConnection(link,link->pc);
// pc连接成功
} else {
int retval;
link->pc_conn_time = mstime();
link->pc->data = link;
// 将服务器的事件循环关联到pc连接的上下文中
redisAeAttach(server.el,link->pc);
// 设置确立连接的回调函数
redisAsyncSetConnectCallback(link->pc,
sentinelLinkEstablishedCallback);
// 设置断开连接的回调处理
redisAsyncSetDisconnectCallback(link->pc,
sentinelDisconnectCallback);
// 发送AUTH 命令认证
sentinelSendAuthIfNeeded(ri,link->pc);
// 发送连接名字
sentinelSetClientName(ri,link->pc,"pubsub");
// 发送订阅 __sentinel__:hello 频道的命令,设置回调函数处理回复
// sentinelReceiveHelloMessages是处理Pub/Sub的频道返回信息的回调函数,可以发现订阅同一master的Sentinel节点
retval = redisAsyncCommand(link->pc,
sentinelReceiveHelloMessages, ri, "SUBSCRIBE %s",
SENTINEL_HELLO_CHANNEL);
// 订阅频道出错,关闭
if (retval != C_OK) {
// 关闭pc连接
instanceLinkCloseConnection(link,link->pc);
return;
}
}
}
// 如果已经建立了新的连接,则清除断开连接的状态。表示已经建立了连接
if (link->cc && (ri->flags & SRI_SENTINEL || link->pc))
link->disconnected = 0;
}
建立连接的函数redisAsyncConnectBind()
是Redis
的官方C语言客户端hiredis
的异步连接函数,当连接成功时需要调用redisAeAttach()
函数来将服务器的事件循环(ae)与连接的上下文相关联起来(因为hiredis提供了多种适配器,包括事件ae,libev,libevent,libuv),在关联的时候,会设置了网络连接的可写可读事件的处理程序。接下来还会设置该连接的确立时和断开时的回调函数redisAsyncSetConnectCallback()
和redisAsyncSetDisconnectCallback()
,为什么这么做,就是因为该连接是异步的。
了解了以上这些,继续分析节点实例和当前哨兵的连接建立。从该函数中可以很明显的看出来:
- 如论是主节点、从节点还是哨兵节点,都会与当前哨兵建立命令连接(Commands connection)。
- 只有主节点或从节点才会建立发布订阅连接(Pub / Sub connection)。
当建立了命令连接(cc)之后立即执行了三个动作:
执行函数 | 执行函数的目的 | 回复函数 | 回复函数操作 |
---|---|---|---|
sentinelSendAuthIfNeeded() |
发送 AUTH 命令进行认证 |
sentinelDiscardReplyCallback() |
丢弃回复,只是将已发送但未回复的命令个数减1 |
sentinelSetClientName() |
发送 CLIENT SETNAME 命令设置连接的名字 |
sentinelDiscardReplyCallback() |
丢弃回复,只是将已发送但未回复的命令个数减1 |
sentinelSendPing() |
发送 PING 命令来判断连接状态 |
sentinelPingReplyCallback() |
根据回复的内容来更新一些连接交互时间,等。 |
当建立了发布订阅连接(pc)之后立即执行的动作:(前两个动作与命令连接相同,只列出不相同的第三个)
执行函数 | 执行函数的目的 | 回复函数 | 回复函数操作 |
---|---|---|---|
redisAsyncCommand() |
发送 SUBSCRIBE 命令来判订阅__sentinel__:hello 频道的事件提醒 |
sentinelReceiveHelloMessages() |
哨兵节点处理从实例(主节点或从节点)发送过来的hello 信息,来获取其他哨兵节点或主节点的从节点信息。 |
如果成功建立连接,之后会清除连接断开的标志,以表示连接已建立。
如果不是第一次执行,那么会判断连接是否建立,如果断开,则重新给建立,如果