(十六)高并发redis学习笔记:哨兵底层原理以及slave选举算法

1、sdown和odown转换机制

sdown和odown两种失败状态:

  • sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机。sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为master宕机
  • odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机。

sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机。

我们可以通过源码来看看哨兵是如何工作的:
首先,在定时任务severCron()函数中,如果发现是哨兵则会执行哨兵的计时器sentinelTimer()

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ...
    /* Run the Sentinel timer if we are in sentinel mode. */
    if (server.sentinel_mode) sentinelTimer();
    ...
}

sentinelTimer()计时器中,会针对每一个哨兵进行处理,调用函数为sentinelHandleDictOfRedisInstances(),检查所有的实例。

void sentinelTimer(void) {
    ...
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    ...
} 

sentinelHandleDictOfRedisInstances()函数中,主要是递归遍历,调用sentinelHandleRedisInstance()对实例进行处理。


void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {

    sentinelReconnectInstance(ri);
    // 发送ping消息给这个实例
    sentinelSendPeriodicCommands(ri);
    if (sentinel.tilt) {
        if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
        sentinel.tilt = 0;
        sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
    }
    // 判断是否主观下线
    sentinelCheckSubjectivelyDown(ri);
    
    if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
        /* Nothing so far. */
    }
    // 只针对master节点
    if (ri->flags & SRI_MASTER) {
        // 判断是都客观下线
        sentinelCheckObjectivelyDown(ri);
        if (sentinelStartFailoverIfNeeded(ri))
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        sentinelFailoverStateMachine(ri);
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}

如何判断主观下线:

void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;
    // act_ping_time:最后一个未完成的ping(之后没有收到pong)发出的时间。当接收到一个pong时,该字段被设置为0,如果该值为0并且发送了一个新的ping,则再次设置为当前时间。(说明已经出现ping过没回复)
    if (ri->link->act_ping_time)
        elapsed = mstime() - ri->link->act_ping_time;
    else if (ri->link->disconnected)
        // 连接不上,还没有出现ping过没回复
        elapsed = mstime() - ri->link->last_avail_time;
    // 如果检测到连接的活跃度(activity)很低,那么考虑重断开连接,并进行重连
    if (ri->link->cc &&
        (mstime() - ri->link->cc_conn_time) >
        SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
        ri->link->act_ping_time != 0 && /* There is a pending ping... */
        (mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) &&
        (mstime() - ri->link->last_pong_time) > (ri->down_after_period/2))
    {
        // 连接时长已超过最短连接间隔 ping已经发出,但在down_after_period/2的时间内没有收到pong响应
        // 断开实例的cc连接(命令连接)
        instanceLinkCloseConnection(ri->link,ri->link->cc);
    }
   // 最后一次从这个服务器接收消息的时间大于发布publish的最大时间的3倍,也就是6秒时断开订阅频道信息的连接
    if (ri->link->pc &&
        (mstime() - ri->link->pc_conn_time) >
         SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
        (mstime() - ri->link->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
    {
        // 断开发布订阅的连接 pc
        instanceLinkCloseConnection(ri->link,ri->link->pc);
    }
    
    //现在距离上次ping的时间已经超过了down_after_period,可以判定为主观宕机
    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)))
    {
        //判断为主观宕机
        if ((ri->flags & SRI_S_DOWN) == 0) {
            // 发送事件
            sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
            // 记录主观宕机的时间
            ri->s_down_since_time = mstime();
            //更新主观宕机的状态
            ri->flags |= SRI_S_DOWN;
        }
    } else {
        /* 它活过来了,判定会不宕机*/
        if (ri->flags & SRI_S_DOWN) {
            // 发送事件
            sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
            // 移除掉之前的宕机标识
            ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
        }
    }
}


如何判断客观宕机,sentinelCheckObjectivelyDown(ri)


void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    unsigned int quorum = 0, odown = 0;
    //当前的sentinel判断它为客观单价
    if (master->flags & SRI_S_DOWN) {
        /* Is down for enough sentinels? */
        //需要满足条件
        quorum = 1; /* the current sentinel. */
        /* Count all the other sentinels. */
        // 遍历所有的sentinel
        di = dictGetIterator(master->sentinels);
        while((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);
            if (ri->flags & SRI_MASTER_DOWN) quorum++;
        }
        //释放迭代器
        dictReleaseIterator(di);
        //是否满足认同数量大于等于quorum,满足则认为是客观宕机(odown)
        if (quorum >= master->quorum) odown = 1;
    }
    /* 如果是odown*/
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            // 发送事件 
            sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
                quorum, master->quorum);
                // 设置odown标识
            master->flags |= SRI_O_DOWN;
            // 记录事件
            master->o_down_since_time = mstime();
        }
    } else {
        if (master->flags & SRI_O_DOWN) {
            // 没有进入odown,发送事件
            sentinelEvent(LL_WARNING,"-odown",master,"%@");
            // 移除标识
            master->flags &= ~SRI_O_DOWN;
        }
    }
}


2、哨兵集群的自动发现机制

哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
消息主要包括:

  • s_ip: sentinel的ip
  • s_port: sentinel的端口port
  • s_runnid:sentinel的runnid
  • s_epoch:纪元,版本
  • m_name:master的名称
  • m_ip:master的ip
  • m_port:master 的端口
  • m_epoch:master的epoch

每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hellochannel里发送一个消息,内容是自己的hostiprunid还有对这个master的监控配置

每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在

每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步

3、slave配置的自动纠正

哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上

4、slave->master选举算法

如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,优先级会考虑slave的一些信息。

  • (1)跟master断开连接的时长,如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,也就是:(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state ,那么slave就被认为不适合选举为master,被排除掉。(这个仅仅是用于排除,不用于排序),剩下的执行后面的三个比较。
  • (2)slave优先级,slave priority越低,优先级就越高,这个是可以进行配置的。默认是100,越小越优先。
  • (3)复制offset,如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
  • (4)run id,选择一个run id比较小的那个slave

5、quorum和majority

每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换

如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换

但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换。

这个quorum是我们自己设置的, majority是客观存在的。

6、configuration epoch

哨兵会对一套redis master+slave进行监控,有相应的监控的配置,也就是可以理解为配置版本。

执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的

如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号。

7、configuraiton传播

哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制。要知道,主备切换是一个哨兵在干,干完了总得让别人知道是不是。

这里之前的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的。其他的哨兵会订阅这个消息,发现更新的epoch就认为master已经切换过了,接着把自己存的信息更新。这样,就可以保持哨兵里面的信息一致性了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章