上一篇我们将redis哨兵初始化分析完成,接下来我们就可以开始分析redis如何通过raft完成哨兵leader选举,并完成主从节点故障转移工作,因为篇幅原因,关于redis故障转移的内容将分为两个篇章,而这篇讨论的是哨兵如何完成主观下线的判定。
一、详解哨兵的主观认定下线的流程
1. 简述raft协议
在正式开始后续的文章讨论前,我们先来简单介绍一下分布式共识raft协议,这个是分布式系统中保证高可用的选举协议。该协议将所有分布式系统的节点分为3个角色:
- leader: 当前分布式集群中的主节点,即集群中的领导角色,负责承载当前系统中的核心业务。
- follower: 从节点,作为leader节点的跟随节点。
- candidate:一旦leader发生故障被slave感知,那么这些节点会将自身角色转为Canadian,并发起选举,得票数最多的Canadian将转为新的leader。
正常情况下,被选举为leader的节点会向follower节点发送心跳,告知自己当前还未下线:
一旦follower感知到leader下线,就会将自己身份转换为candidate,通过选举竞争leader,每一个candidate都会给自己投一票然后向其他选举节点获取选票,在选举计时时间以内,超过半数以上得票的candidate就会被选举为新的leader节点,其余candidate收到此leader的心跳消息后身份就会转为最新leader节点的follower:
2. redis中的raft协议与核心流程
与传统raft协议实现有所不同,redis哨兵在未发生选举时地位是对等并无leader和follower等概念,只有感知到监听主节点下线时才会借助raft的协议触发选举,选举出一个哨兵作为leader完成故障转移之后,leader哨兵会再次回归对等地位。
redis哨兵执行的生命周期还是交由时间事件定时执行,它的整体工作流程为:
- 检查自己所监听的master连接情况,检查是否与监听的master节点断开连接,如果发现连接断开则进行断线重连。
- 再对master节点进行消息通信,这期间哨兵会发送ping与主节点保持通信,再发送info请求master最新信息。
- 一旦发现master长时间未与自己进行心跳,则主观视为监听节点下线,并通过频道告知其他哨兵获取其他哨兵对于主节点的结果判断。
- 如果哨兵一致认定当前监听节点下线,则会选举出一个哨兵作为leader进行故障转移,即在所有从节点中找到一个优先级最高的从节点作为新的master。
对此我我们给出程序执行的入口来查看这块核心的主流程,可以看到serverCron定时执行的时间时间会每100ms执行一次哨兵的时间事件sentinelTimer,对此我们不妨步入sentinelTimer查看实现细节:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//......
//100ms一次,如果是哨兵模式则运行哨兵的时间事件
run_with_period(100) {
if (server.sentinel_mode) sentinelTimer();
}
//......
}
步入sentinelTimer,该函数会先判断哨兵执行时间是否过长,如果发现时钟回拨或者长时间才进行处理则触发tilt模式,该模式下哨兵只会定期发送和接收消息,不做其他任务处理。
再调用sentinelHandleDictOfRedisInstances遍历哨兵中的master开始开始进行我们上述所说的判断与master连接状态、进行通信和info消息获取、主观下线判断、客观下线判断、故障转移。
完成这些步骤之后,更新下一次的执行时间,可以看到redis对于这个时间设置做了一个巧妙的设计,我们都知道哨兵判定节点下线后就会发起选举,为了避免哨兵集群所有节点同时发起选举投票从而得到相同票数的情况而导致本轮选举失败而进行反复选举的情况,redis会在哨兵本次时间事件执行完成之后,通过随机种子调整哨兵时间下一次的执行时机,尽可能避免选举时反复出现选票一致的情况:
对此我们也给出sentinelTimer的实现细节:
void sentinelTimer(void) {
// 前置检查事件定期任务是否因为系统负载过大或者各种原因导致时钟回拨,或者处理过长,进入tilt模式,该模式哨兵只会定期发送和接收命令
sentinelCheckTiltCondition();
//监听的master节点作为参数传入,进行逐个通信处理
sentinelHandleDictOfRedisInstances(sentinel.masters);
//......
//随机调整执行频率避免同时执行,确保提高选举一次性成功的概率
server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}
我们再次步入核心方法sentinelHandleDictOfRedisInstances它会遍历每一个master节点,然后调用sentinelHandleRedisInstance处理每一个哨兵所监听的master实例:
void sentinelHandleDictOfRedisInstances(dict *instances) {
//.......
//迭代出每一个master实例再对主节点进行处理
di = dictGetIterator(instances);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
//迭代并处理每一个master实例
sentinelHandleRedisInstance(ri);
//.......
}
//.......
}
步入sentinelHandleRedisInstance即可看到我们上文所说的而核心逻辑,它对于笔者上文的每一个流程都做了抽象,可以看到它会先尝试和断线的master建立连接,然后发送ping和info获取master节点的确认和master实时消息,最后在检查master是否超时未回复发起主观下线,然后再发起客观下线请求确认其他哨兵回复。 最后明确master节点确实下线之后再发起选举,得出leader后由leader进行故障转移,挑选出新的master承载核心业务。
//这个入参包含恰哨兵实例和当前主节点的从节点信息
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
/* ========== MONITORING HALF ============ */
/* Every kind of instance */
//1. 尝试和断连的实例重新建立连接
sentinelReconnectInstance(ri);
//2. 向实例发送ping和info等命令
sentinelSendPeriodicCommands(ri);
//......
/* Every kind of instance */
//3. 主观判断是否下线
sentinelCheckSubjectivelyDown(ri);
/* Masters and slaves */
if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
/* Nothing so far. */
}
/* Only masters */
if (ri->flags & SRI_MASTER) {
//4. 检查其当前是否客观下线
sentinelCheckObjectivelyDown(ri);
//5. 判断是否要进行故障切换,如果要启动故障切换,则获取其他哨兵对于该节点的判断
if (sentinelStartFailoverIfNeeded(ri))
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
//6. 执行故障切换
sentinelFailoverStateMachine(ri);
//7. 再次获取哨兵实例对主节点状态的判断
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}
3. 断线重连检查
基于上文我们了解哨兵时间事件执行的大体流程,接下来我们会针对每一个流程进行详细的分析,首先我们先来了解一下对于断线重连检查方法,对于断线重连检查,redis哨兵通过两个异步的连接进行处理,它通过cc这个异步连接和master建立通信完成PING和INFO的消息发送,再通过pc处理各种广播消息:
我们都知道redis将哨兵中每一个维护的master封装成sentinelRedisInstance ,这其中就有cc和pc两个连接指针,用于和当前哨兵建立连接和通信:
typedef struct sentinelRedisInstance {
//......
//异步发送命令的连接
redisAsyncContext *cc; /* Hiredis context for commands. */
//pub/sub发送通道,用于处理频道消息的收发
redisAsyncContext *pc;
//......
}
此时我们再来查看sentinelReconnectInstance方法内部,即非常直观了解到其内部对于断开或者为空的连接会调用redisAsyncConnectBind方法通过外部遍历master传入的master结构体信息发起异步连接重建:
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
if (!(ri->flags & SRI_DISCONNECTED)) return;
/* Commands connection. */
//如果命令指针cc为空,则进行一次异步重连
if (ri->cc == NULL) {
//基于外部遍历传入的master指针进行异步重连
ri->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,REDIS_BIND_ADDR);
//如果连接失败则调用sentinelKillLink销毁该连接
if (ri->cc->err) {
sentinelEvent(REDIS_DEBUG,"-cmd-link-reconnection",ri,"%@ #%s",
ri->cc->errstr);
sentinelKillLink(ri,ri->cc);
} else {
//......
}
}
//检查发布订阅pc,如果为空则将外部传入的master信息通过异步的方式重新和频道建立连接
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && ri->pc == NULL) {
ri->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,REDIS_BIND_ADDR);
if (ri->pc->err) {
sentinelEvent(REDIS_DEBUG,"-pubsub-link-reconnection",ri,"%@ #%s",
ri->pc->errstr);
sentinelKillLink(ri,ri->pc);
} else {
//......
}
}
//......
}
4. 消息通信
完成连接重建之后,在所有连接正常的情况下,哨兵会检查当前发送上次ping间隔是否超过指定间隔,如果是则通过cc指指针向master发送ping。 同理如果info消息超过发送间隔也会生成当前哨兵ip端口等基本信息通过cc通道发送给masrter:
对此我们给出命令定期发送函数sentinelSendPeriodicCommands的入口,可以看到它会依次检查ping和hello消息的间隔逻辑,然后按需通过cc发送ping或者hello消息:
void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
//......
//和其他哨兵处理的逻辑
if ((ri->flags & SRI_SENTINEL) == 0 &&
(ri->info_refresh == 0 ||
(now - ri->info_refresh) > info_period))
{
//......
} else if ((now - ri->last_pong_time) > ping_period) {//超过ping间隔发ping
sentinelSendPing(ri);
} else if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {//超过pub间隔通过cc发送当前哨兵个人信息
sentinelSendHello(ri);
}
}
我们步入sentinelSendPing可以看到其内部逻辑比较简单,通过cc发送ping然后更新上次发送ping的时间戳字段last_ping_time:
int sentinelSendPing(sentinelRedisInstance *ri) {
//通过cc异步命令接口发送ping
int retval = redisAsyncCommand(ri->cc,
sentinelPingReplyCallback, NULL, "PING");
//如果得到正常响应则更新last_ping_time
if (retval == REDIS_OK) {
ri->pending_commands++;
if (ri->last_ping_time == 0) ri->last_ping_time = mstime();
return 1;
} else {
return 0;
}
}
同理我们给出sentinelSendHello函数,可以看到其内部会组装当前哨兵的ip和端口以及master的地址信息通过cc发送到__sentinel__:hello这个频道中进行广播:
int sentinelSendHello(sentinelRedisInstance *ri) {
//......
/* Format and send the Hello message. */
//将哨兵ip 端口以及master地址信息数据拼接到payload中
snprintf(payload,sizeof(payload),
"%s,%d,%s,%llu," /* Info about this sentinel. */
"%s,%s,%d,%llu", /* Info about current master. */
announce_ip, announce_port, server.runid,
(unsigned long long) sentinel.current_epoch,
/* --- */
master->name,master_addr->ip,master_addr->port,
(unsigned long long) master->config_epoch);
//通过cc异步发送到__sentinel__:hello频道中
retval = redisAsyncCommand(ri->cc,
sentinelPublishReplyCallback, NULL, "PUBLISH %s %s",
SENTINEL_HELLO_CHANNEL,payload);
if (retval != REDIS_OK) return REDIS_ERR;
ri->pending_commands++;
return REDIS_OK;
}
5. 判定主观下线
然后就开始主观下线的检查,可以看到redis一旦发现master长时间未与当前哨兵进行通信,亦或者在很长一段时间都被报告为从节点,则将主观判定其下线,再通过或预运算符将ri的flags标志位注明这个master已经主观的被认定为下线。最后通过通过 +sdown这个channel 发送主观下线的消息,让他们各自检查,从而开始后续客观下线检查及选举和故障转移等操作:
对应的我们也给出sentinelCheckSubjectivelyDown函数的实现,可以我们补充弄一下down_after_period 这个是就是决定Sentinel判断实例进入主观下线所需的时间长度,默认情况下是30000毫秒,如果需要修改我们可以在redis.conf中用down-after-milliseconds指定:
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
//......
//如果上一次到现在的间隔elapsed 大于down_after_period ,则当前哨兵会主观认定其下线
if (elapsed > ri->down_after_period ||
//或者当前哨兵认定它是master而其他报告长时间的反馈都是从节点,则当前哨兵会主观认定其下线
(ri->flags & SRI_MASTER &&
ri->role_reported == SRI_SLAVE &&
mstime() - ri->role_reported_time >
(ri->down_after_period+SENTINEL_INFO_PERIOD*2)))
{
//通过 +sdown这个channel 发送主观下线的消息
if ((ri->flags & SRI_S_DOWN) == 0) {
sentinelEvent(REDIS_WARNING,"+sdown",ri,"%@");
//设置当前监控的master实例为主观下线
ri->s_down_since_time = mstime();
ri->flags |= SRI_S_DOWN;
}
} else {
//......
}
}
二、小结
自此我们将redis哨兵主观下线的核心流程分析完成,我们来简单小结一下哨兵判断主观下线的流程:
- 哨兵实例随机一个hz参数作为定时器执行间隔,即执行一个哨兵定时事件sentinelTimer,
- sentinelTimer会定期调用sentinelHandleDictOfRedisInstances遍历检查监控的master进行定时的交互。
- 哨兵实例定期发送ping和hello亦或者info请求给master。
- master超过down_after_period设置的时间没有回应,或者其他角色长时间报告这个master已经是slave,则当前哨兵会主观认定其下线,并将消息发送到+sdown中。
- 结束一次定时任务后,定时器sentinelTimer执行完后设置下一次随机执行时间,保证在主观与客观认定master下线后通过随机性提升选举的效率。