Redis Sentinel原理解析

1、概述

这篇文章主要用来说述redis高可用的实现原理,学习redis高可用实现,可让咱们更好的进行系统设计。也能够学习redis中的设计,将其迁移到其余系统的实现中去。redis

2、总体架构

  • 监控:Sentinel节点会按期检测Redis数据节点(主从)、其他Sentinel节点是否可达。
  • 主节点故障转移:主节点不可用,将从节点晋升为主节点并维护后续正确的主从关系。
  • 通知:Sentinel节点会将故障转移的结果通知给应用方。
  • 配置提供者:在Redis Sentinel架构中,客户端在初始化的时候链接的是Sentinel节点集合,从中获取主节点信息

3、实现原理

节点通讯(互相通讯,实现功能)

1.每10s,sentinel会发送info给全部data节点。算法

  • info命令获取从节点信息
  • 新节点加入可马上感知,并加入集群

2.每2s,向_sentinel_:hello频道发送主节点判断以及当前sentinel节点的信息。服务器

  • 发现其余sentinel节点信息
  • 交换主节点状态。作一些逻辑处理(好比发现其余节点的master比本身新,则进行故障转移)。
<Sentinel节点IP> <Sentinel节点端口> <Sentinel节点runId> <Sentinel节点配置版本> <主节点名字> <主节点Ip> <主节点端口> <主节点配置版本>
复制代码

3.每1s,向其余节点发送ping(alive check)markdown

  • 判断节点是否存活

经过1和2,在sentinel启动的时候只须要配置要监控的master,便可实现对其余slave和sentinel的自动发现机制。主要依赖对redis data节点的发布订阅和info操做去作这个事情。网络

主观下线

主观上判断某个节点下线(在超时时间没有ping经过)数据结构

客观下线

若是主观下线的是主节点,则会经过sentinel is- master-down-by-addr命令向其余sentinel节点询问对主节点的判断。超高quorum数量,会进行客观下线。架构

is-master-down-by-addr命令

全部sentinel节点除了定时pub hello消息,还经过这个命令进行p2p通讯。及时告知其余sentinel信息。 该命令做用:app

  • 向其余sentinel寻问对主节点的判断
  • 领主选举投票
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
复制代码

若是runid为*表明主节点下线命令运维

若是不为*表明请求投票命令异步

主节点下线命令

sentinel is-master-down-by-addr 127.0.0.1 6379 0 *
复制代码

领导选举投票 使用raft的领主选举算法,经过该命令投票选举

命令返回结果

  • down_state
  • leader_runid *表明用来作主节点不可达,有具体的runid表明赞成runid成为领导者。
  • leader_epoch 领导者任期

故障转移

主节点故障,选举从节点 1.选哪一个?

(1)过滤不健康的

(2)选slave-priority最高的从节点列表

(3)选择复制偏移量最大的。

(4)选择runid最小的

2.执行slaveof no one

3.向剩余从节点发送命令,让其成为新master的从

4.将原来的主更新为从。并保持对其关注

4、源码分析

sentienl的存储结构。

图中每一个粉色的方框都表明一个SentinelRedisInstance,这是sentienl中统一的一个数据结构,简单来说就是一块内存,里面存储了节点的信息。每个redis data节点或者sentinel节点都会有一个SentinelRedisInstance(除了当前sentienl)。就比如每一条网络链接都有一个socket同样。

master表明监控的master信息。每一个sentinel有多个master(上面只画了一个),表明能够同时监控多个master节点。

SentinelRedisInstance结构有两个hash:key为名字,val为SentinelRedisInstance

一个用来存储当前master的slave,另外一个用来存储一样监控这个master的其余sentinel节点(如上图结构),使用hash 是为了经过key进行快速查找对应结构。

// 其余一样监控这个主服务器的全部 sentinel
dict *sentinels; /* Other sentinels monitoring the same master. */
// 若是这个实例表明的是一个主服务器
// 那么这个字典保存着主服务器属下的从服务器
// 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
dict *slaves; /* Slaves for this master instance. */
复制代码

固然只有SentinelRedisInstance为master的时候,这两个hash才会被用到。因此sentinel的全部操做都围绕这个master结构进行。sentinel经过info命令以及publish hello消息发现其余的slave和sentinel,新加入的节点会加入对应hash中。

flags

惟一须要强调的是SentinelRedisInstance中的flags变量。该变量记录了当前节点的状态。(flags经过标志位记录多种状态,每一个状态一个位,经过|~进行更新)

  • 若是是master,则记录master的状态。
  • 若是是sentinel,则记录当前sentienl一些判断状态。

因此sentinel彻底围绕这些SentinelRedisInstance去维护整个集群的逻辑。好比ping,info,就是遍历SentinelRedisInstance,对其hostport进行ping,info等操做。

遍历逻辑:先拿到master的SentinelRedisInstance,执行逻辑,而后再遍历两个hash处理。

服务启动

sentinel 模式启动会执行下面逻辑

if (server.sentinel_mode) {
    initSentinelConfig();
    initSentinel();
}
复制代码

initSentinel主要就是一些初始化工做,包括sentinel结构建立、一些全局变量的初始化、以及注册命令处理器。

sentinelHandleConfiguration

服务启动会先加载配置。这个方法主要是sentinel配置的解析逻辑。

if (!strcasecmp(argv[0],"monitor") && argc == 5) {
    /* monitor <name> <host> <port> <quorum> */
    // 读入 quorum 参数
    int quorum = atoi(argv[4]);
    // 检查 quorum 参数必须大于 0
    if (quorum <= 0) return "Quorum must be 1 or greater.";

    // 建立主服务器实例
    if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2],
                                    atoi(argv[3]),quorum,NULL) == NULL)
    {
        switch(errno) {
        case EBUSY: return "Duplicated master name.";
        case ENOENT: return "Can't resolve master instance hostname.";
        case EINVAL: return "Invalid port number";
        }
    }
}else if (!strcasecmp(argv[0],"down-after-milliseconds") && argc == 3) {

    /* down-after-milliseconds <name> <milliseconds> */
    // 查找主服务器
    ri = sentinelGetMasterByName(argv[1]);
    if (!ri) return "No such master with specified name.";

    // 设置选项
    ri->down_after_period = atoi(argv[2]);
    if (ri->down_after_period <= 0)
        return "negative or zero time parameter.";
    sentinelPropagateDownAfterPeriod(ri);
}
复制代码

核心就是对master的SentinelRedisInstance结构建立。而后将其余配置初始化到该结构中。上面只列举了部分代码。其余配置相似down-after-milliseconds的解析逻辑。

createSentinelRedisInstance

用来建立sentinel中实例。包括sentinel、被监控的redis的主从。每次有新增都会调用该方法建立一个实例,并加入到master的SentinelRedisInstance中(除了master自身)

  • SRI_MASTER 建立一个被监控的master实例。并将其加入到sentinel.masters这个hash表中
  • SRI_SLAVE 建立一个被监控的slave实例。并将其加入到master->slaves这个hash表中
  • SRI_SENTINEL 建立一个被监控的sentinel实例。并将其加入到master->sentinels这个hash表中

上面基本就是一些初始化工做。sentinel核心逻辑是须要时间函数驱动的,因此咱们直接看sentinelTimer逻辑。

sentinelTimer

该函数100ms执行一次。

void sentinelTimer(void) {
    // 记录本次 sentinel 调用的事件,判断是否须要进入 TITL 模式
    sentinelCheckTiltCondition();
    // 执行按期操做
    // 好比 PING 实例、分析主服务器和从服务器的 INFO 命令
    // 向其余监视相同主服务器的 sentinel 发送问候信息
    // 并接收其余 sentinel 发来的问候信息
    // 执行故障转移操做,等等
    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 = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}
复制代码

sentinelCheckTiltCondition方法用来判断是否进入ttl,而且记录执行时间。

TITL模式:由于sentinel依赖本机时间驱动,若是系统时间出问题,或者由于进程阻塞致使的时间函数延迟调用。这时再去参与集群逻辑会出现不正确的决策。所以若是当前时间和上一次执行时间差为负值或者超过2s,该节点会进入TILT模式。

sentinelHandleDictOfRedisInstances

void sentinelHandleDictOfRedisInstances(dict *instances) {
    dictIterator *di;
    dictEntry *de;
    sentinelRedisInstance *switch_to_promoted = NULL;

    /* There are a number of things we need to perform against every master. */
    // 遍历多个实例,这些实例能够是多个主服务器、多个从服务器或者多个 sentinel
    di = dictGetIterator(instances);
    while((de = dictNext(di)) != NULL) {

        sentinelRedisInstance *ri = dictGetVal(de);
        // 执行调度操做
        sentinelHandleRedisInstance(ri);

        // 若是被遍历的是主服务器,那么递归地遍历该主服务器的全部从服务器
        if (ri->flags & SRI_MASTER) {

            // 全部从服务器
            sentinelHandleDictOfRedisInstances(ri->slaves);
            // 全部 sentinel
            sentinelHandleDictOfRedisInstances(ri->sentinels);

            if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
                // 已选出新的主服务器
                switch_to_promoted = ri;
            }
        }
    }
    // 将原主服务器(已下线)从主服务器表格中移除,并使用新主服务器代替它
    if (switch_to_promoted)
        sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
    dictReleaseIterator(di);
}
复制代码

这个方法就递归遍历全部SentinelRedisInstance。。核心就是对被 Sentinel 监视的全部实例(包括主服务器、从服务器和其余 Sentinel ) 进行按期操做。

逻辑很简单,其实就是执行sentinelHandleRedisInstance方法。

若是有故障转移(从节点升级为主节点),则调用sentinelFailoverSwitchToPromotedSlave替换新主服务。

sentinelHandleRedisInstance方法

Sentinel的主逻辑流程,后面会一一介绍每一个方法的逻辑。该方法按期调用,非阻塞(中间的io命令都会异步出去)。

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    /* Every kind of instance */
    // 若是有须要的话,建立连向实例的网络链接
    sentinelReconnectInstance(ri);

    // 根据状况,向实例发送 PING、 INFO 或者 PUBLISH 命令
    sentinelSendPeriodicCommands(ri);
    /* ============== 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) {
        // 若是 TILI 模式未解除,那么不执行动做
        if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;

        // 时间已过,退出 TILT 模式
        sentinel.tilt = 0;
        sentinelEvent(REDIS_WARNING,"-tilt",NULL,"#tilt mode exited");
    }
    /* Every kind of instance */
    // 检查给定实例是否进入 SDOWN 状态
    sentinelCheckSubjectivelyDown(ri);

    /* Masters and slaves */
    if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
        /* Nothing so far. */
    }
    /* Only masters */
    /* 对主服务器进行处理 */
    if (ri->flags & SRI_MASTER) {

        // 判断 master 是否进入 ODOWN 状态
        sentinelCheckObjectivelyDown(ri);
        // 若是主服务器进入了 ODOWN 状态,那么开始一次故障转移操做
        if (sentinelStartFailoverIfNeeded(ri))
            // 强制向其余 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
            // 刷新其余 Sentinel 关于主服务器的状态
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        // 执行故障转移
        sentinelFailoverStateMachine(ri);
        // 若是有须要的话,向其余 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}
复制代码

1.sentinelReconnectInstance主要就是建立和实例的链接。若是是master或者slave实例,则订阅对应节点的__sentinel__:hello频道,用来接受其余sentinel广播的消息。每一个sentinel会按期给该频道publish。 由于启动的时候只是初始化了实例数据,并无建立链接。对于每个实例,都是在这里去建立链接的。

2.sentinelSendPeriodicCommands主要发送ping、info、publish命令。

3.若是当前在tilt模式中,直接返回,非tilt模式才会执行后续操做。

4.sentinelCheckSubjectivelyDown,检查给定实例是否进入 SDOWN 状态

5.若是当前实例为主服务器,则执行一些故障判断和故障转移操做。

能够看出来sentienl的全部逻辑都是围绕一个主服务进行的。

sentinelSendPeriodicCommands方法

这是咱们的核心关注点,sentinel之间的通讯逻辑。

这个方法主要是ping、info以及publish hello消息。到达执行时刻会执行对应逻辑。

  • ping每s执行一次,若是比设置的down_after_period(超时下线时间)小,则为down_after_period。
  • info每10s执行一次。若是一次未执行则直接执行。若是主节点SDOWN或SRI_FAILOVER_IN_PROGRESS(故障转移)则每s执行一次,为了更快的捕捉服务器变更。
  • publish每2s执行一次。(用于发现其余sentinel以及和其余sentinel通讯)

redis经过pending_commands记录当前正在异步执行的命令。若是超过100,则再也不发送,避免命令堆积。由于sentinel是依赖上一次响应时间来判断是否发送命令,若是出现网络阻塞或者波动,会致使频繁发送。

Ping命令

发送逻辑在sentinelSendPeriodicCommands方法中。

Ping命令回调方法sentinelPingReplyCallback:,若是回调正常,更新对应字段(last_avail_time、last_pong_time),sentienl依赖这些字段进行判活。若是节点执行lua超时,则调用SCRIPT KILL尝试杀死脚本。

info命令

info为获取redis节点信息的命令。处理返回结果的方法为sentinelRefreshInstanceInfo

sentinelRefreshInstanceInfo方法主要是解析并提取须要的数据信息。下面介绍一些核心信息。

1.若是是主节点,提取其从节点的hostport,并为从节点建立实例信息(若是是新发现的),sentinel依赖此方式发现其余的slave节点。

if (sentinelRedisInstanceLookupSlave(ri,ip,atoi(port)) == NULL) {
    if ((slave = createSentinelRedisInstance(NULL,SRI_SLAVE,ip,
                atoi(port), ri->quorum, ri)) != NULL)
    {
        sentinelEvent(REDIS_NOTICE,"+slave",slave,"%@");
    }
}
复制代码

2.若是是从节点,记录当前从服务器对应主节点的信息。由于sentinel的记录的主节点不必定是正确的(网络分区致使切换延迟),因此经过info获取到该从服务器最新信息。以供后面逻辑处理。

这里其实两种主(从)节点状态:一种是sentienl认为当前节点为主(从)节点,另外一种是当前节点认为的主(从)节点。

3.若是发生了角色转变(info返回的和当前sentinel记录的节点状态不一致),更新转变时间。若是tilt,直接返回。不然,根据该节点返回的role和sentinel记录的role进行一些逻辑,具体逻辑咱们后面再研究。

Publish Hello消息

给对应channel发送的信息以下,主要包括对应sentinel的信息和当前master的信息

snprintf(payload,sizeof(payload),
    "%s,%d,%s,%llu," /* Info about this sentinel. */
    "%s,%s,%d,%llu", /* Info about current master. */
    ip, server.port, server.runid,
    (unsigned long long) sentinel.current_epoch,
    /* --- */
    master->name,master_addr->ip,master_addr->port,
    (unsigned long long) master->config_epoch);
复制代码
订阅处理hello消息 (sentinelProcessHelloMessage)

该方法为sentinel处理hello信息的方法。

1.若是hello消息中sentinel节点为新发现的节点(当前setinel不存在的),为新节点建立实例,加入到列表中。这是sentinel发现其余sentinel的惟一方式。

2.更新current_epoch=max(当前current_epoch,hello的current_epoch)

3.若是hello消息的master_config_epoch比该节点master的config_epoch大。则调用sentinelResetMasterAndChangeAddress方法切换当前master。config_epoch为故障转移使用的纪元。故障转移以后会递增。若是发现比较大的,说明进行了故障转移,则信任hello中的master为最新master

if (master->config_epoch < master_config_epoch) {
    master->config_epoch = master_config_epoch;
    if (master_port != master->addr->port ||
        strcmp(master->addr->ip, token[5]))
    {
        sentinelAddr *old_addr;

        sentinelEvent(REDIS_WARNING,"+config-update-from",si,"%@");
        sentinelEvent(REDIS_WARNING,"+switch-master",
            master,"%s %s %d %s %d",
            master->name,
            master->addr->ip, master->addr->port,
            token[5], master_port);

        old_addr = dupSentinelAddr(master->addr);
        sentinelResetMasterAndChangeAddress(master, token[5], master_port);
        sentinelCallClientReconfScript(master,
            SENTINEL_OBSERVER,"start",
            old_addr,master->addr);
        releaseSentinelAddr(old_addr);
    }
}
复制代码
sentinelCheckSubjectivelyDown方法

检查实例是否进入 SDOWN 状态

1.若是实例符合断线重连的条件,则断开该实例链接,等待下次从新链接。其实就是对不活跃实例进行断线重连。

2.设置或者取消SDOWN标志

达到下面两个条件设置为SDOWN,不然取消SDOWN标志位

  • 超过超时时间没有回复命令,则设置为SDOWN
  • Sentinel认为实例是主服务器,这个服务器向 Sentinel 报告它将成为从服务器,但在超过给定时限以后,服务器仍然没有完成这一角色转换

主节点单独逻辑

if (ri->flags & SRI_MASTER) {
    // 判断 master 是否进入 ODOWN 状态
    sentinelCheckObjectivelyDown(ri);
    // 若是主服务器进入了 ODOWN 状态,那么开始一次故障转移操做
    if (sentinelStartFailoverIfNeeded(ri))
        // 强制向其余 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
        // 刷新其余 Sentinel 关于主服务器的状态
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);

    // 执行故障转移
    sentinelFailoverStateMachine(ri);
    // 若是有须要的话,向其余 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
    // 刷新其余 Sentinel 关于主服务器的状态
    // 这一句是对那些没有进入 if(sentinelStartFailoverIfNeeded(ri)) { /* ... */ }
    // 语句的主服务器使用的
    sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
复制代码

sentinelCheckObjectivelyDown

判断当前主节点是否进入ODown状态。

经过遍历全部sentinel实例的flags标志位进行判断。若是一半以上主观下线,则变动为客观下线。这个状态位是在is-master-down-by-addr命令回调中更新的。

sentinelStartFailoverIfNeeded

判断是否须要进行故障转移

void sentinelStartFailover(sentinelRedisInstance *master) {
    redisAssert(master->flags & SRI_MASTER);

    // 更新故障转移状态
    master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;

    // 更新主服务器状态
    master->flags |= SRI_FAILOVER_IN_PROGRESS;

    // 更新纪元
    master->failover_epoch = ++sentinel.current_epoch;

    sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
        (unsigned long long) sentinel.current_epoch);

    sentinelEvent(REDIS_WARNING,"+try-failover",master,"%@");

    // 记录故障转移状态的变动时间
    master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
    master->failover_state_change_time = mstime();
}
复制代码

判断条件故障转移:

1.进入ODOWN而且没有在故障转移中。

2.若是发现故障转移过于频繁也不执行。

若是须要故障转移,则更新当前master的信息,主要是failover_state、failover_epoch等字段。

failover_epoch为当前master纪元+1。

failover_epoch做用:

  • 其余sentinel依赖这个字段判断是否须要进行故障转移。这个在以前的hello中有说到。
  • 当前sentinel依赖这个纪元选出执行故障转移的leader。由于选举使用的也是该纪元。选举出来的leader的纪元应该一致。每次选举都会产生一个新的leader,最新的纪元最权威,这是raft领导选举的核心概念。虽然raft使用的是term。

failover_state表明当前故障转移状态。故障转移操做须要依赖该状态。

故障转移操做须要从sentienl中选举一个执行。因此这只是先更新状态。

sentinelAskMasterStateToOtherSentinels

向其余sentine询问master状态。

这会遍历全部sentinel,若是当前sentinel认为master下线,而且链接正常,会发送is-master-down-by-addr命令

sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
复制代码

若是本sentinel检测到master 主观下线(经过failover_state判断),则runid为当前server的runid,表明让其余sentienl给本身投票。

若是是master客观下线,则runid=*,表明告诉其余sentienl,主节点下线。这是sentinel通知其余sentinel主节点下线的惟一方式。

is-master-down-by-addr命令处理

该逻辑在sentinelCommand中执行

ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,
    c->argv[2]->ptr,port,NULL);

if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
                            (ri->flags & SRI_MASTER))
    isdown = 1;

/* Vote for the master (or fetch the previous vote) if the request * includes a runid, otherwise the sender is not seeking for a vote. */
if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
    leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
                                    c->argv[5]->ptr,
                                    &leader_epoch);
}

/* Reply with a three-elements multi-bulk reply: * down state, leader, vote epoch. */
// 多条回复
// 1) <down_state> 1 表明下线, 0 表明未下线
// 2) <leader_runid> Sentinel 选举做为领头 Sentinel 的运行 ID
// 3) <leader_epoch> 领头 Sentinel 目前的配置纪元
addReplyMultiBulkLen(c,3);
addReply(c, isdown ? shared.cone : shared.czero);
addReplyBulkCString(c, leader ? leader : "*");
addReplyLongLong(c, (long long)leader_epoch);
复制代码

若是节点下线回复down_state1,不然为0

根据ip和port获取ri(sentinelRedisInstance),若是ri是主节点,而且runid不为*,则进行选举投票。

投票逻辑

请求的req_epoch>当前sentinel的current_epoch(更新sentinel的current_epoch)

req_epoch>master的master.leader_epoch,而且>=sentinel.current_epoch,更新master的leader为req_runid。而后投票给当前req_runid节点。

其实就是判断epoch。若是请求的epoch比较大,那就投票便可。和raft的领导选举同样。

is-master-down-by-addr命令回调(sentinelReceiveIsMasterDownReply)

// 更新最后一次回复询问的时间
ri->last_master_down_reply_time = mstime();

// 设置 SENTINEL 认为主服务器的状态
if (r->element[0]->integer == 1) {
    // 已下线
    ri->flags |= SRI_MASTER_DOWN;
} else {
    // 未下线
    ri->flags &= ~SRI_MASTER_DOWN;
}
// 若是运行 ID 不是 "*" 的话,那么这是一个带投票的回复
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 (ri->leader_epoch != r->element[2]->integer)
        redisLog(REDIS_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;
}
复制代码
  • <down_state> 1 表明下线, 0 表明未下线
  • <leader_runid> Sentinel 选举做为领头 Sentinel 的运行 ID
  • <leader_epoch> 领头 Sentinel 目前的配置纪元

1.更新回复时间

2.更新被询问的sentinel的flag。用于ODOWN判断。

3.若是leader_runid非*,表明投票信息,更新该sentinel的leader信息。

ri的leader字段有两个状态:

  • 若是ri一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。
  • 若是ri一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。

执行故障转移(sentinelFailoverStateMachine)

switch(ri->failover_state) {

    // 等待故障转移开始
    case SENTINEL_FAILOVER_STATE_WAIT_START:
        sentinelFailoverWaitStart(ri);
        break;

    // 选择新主服务器
    case SENTINEL_FAILOVER_STATE_SELECT_SLAVE:
        sentinelFailoverSelectSlave(ri);
        break;
    
    // 升级被选中的从服务器为新主服务器
    case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
        sentinelFailoverSendSlaveOfNoOne(ri);
        break;

    // 等待升级生效,若是升级超时,那么从新选择新主服务器
    // 具体状况请看 sentinelRefreshInstanceInfo 函数
    case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
        sentinelFailoverWaitPromotion(ri);
        break;

    // 向从服务器发送 SLAVEOF 命令,让它们同步新主服务器
    case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
        sentinelFailoverReconfNextSlave(ri);
        break;
}
复制代码

整个流程如上:

1.sentinelFailoverWaitStart会根据当前全部sentinel的leader以及当前故障转移纪元选出来leader。若是发现leader是本身,则切换failover_state为SENTINEL_FAILOVER_STATE_SELECT_SLAVE,执行下一个case。不然跳出。

2.根据逻辑选举从服务器做为新的主节点,若是没有选出,清空故障转移状态。选出成功后继续更新状态执行下一个case。选择逻辑上面有说过。

3.升级新节点为主节点,若是发现断线并超时,则终止故障转移逻辑。这个其实就是异步发送slave of命令。让从节点升级为主节点。这个命令在redis复制源码解析中说过。

4.SENTINEL_FAILOVER_STATE_WAIT_PROMOTION状态只是等待升级的另外一个逻辑,若是升级超时则终止故障转移。 如何检测从节点升级主节点成功?

其实在info中有一段逻辑:

if ((ri->master->flags & SRI_FAILOVER_IN_PROGRESS) &&
    (ri->master->failover_state ==
        SENTINEL_FAILOVER_STATE_WAIT_PROMOTION))
{
    // 更新从服务器的主服务器(已下线)的配置纪元
    ri->master->config_epoch = ri->master->failover_epoch;
    // 设置从服务器的主服务器(已下线)的故障转移状态
    // 这个状态会让从服务器开始同步新的主服务器
    ri->master->failover_state = SENTINEL_FAILOVER_STATE_RECONF_SLAVES;
    // 更新从服务器的主服务器(已下线)的故障转移状态变动时间
    ri->master->failover_state_change_time = mstime();
    // 将当前 Sentinel 状态保存到配置文件里面
    sentinelFlushConfig();
    // 发送事件
    sentinelEvent(REDIS_WARNING,"+promoted-slave",ri,"%@");
    sentinelEvent(REDIS_WARNING,"+failover-state-reconf-slaves",
        ri->master,"%@");
    // 执行脚本
    sentinelCallClientReconfScript(ri->master,SENTINEL_LEADER,
        "start",ri->master->addr,ri->addr);
}
复制代码

在这里表明当前从节点已经升级成功,则更新config_epoch。config_epoch其实在hello消息中被用到。若是其余sentienl发现它的config_epoch小于hello消息中的config_epoch,则会重置master的地址。

5.SENTINEL_FAILOVER_STATE_RECONF_SLAVES

Info更新成功后,会执行sentinelFailoverReconfNextSlave方法。其实就是向全部从服务器发送slave of命令。 这里会受parallel_syncs参数限制。控制并行slave of数量,避免主节点网络压力。

成功以后更新failover_state状态为SENTINEL_FAILOVER_STATE_UPDATE_CONFIG

sentinelFailoverSwitchToPromotedSlave

最终会调用该方法切换主服务器,整个转移过程结束。

故障转移失败

若是执行故障转移的leader存活的状况,转移超时,则会调用sentinelAbortFailover方法终止故障转移。 若是转移过场中leader宕机,其余节点会继续执行故障转移逻辑。

5、开发运维

配置参数

down-after-milliseconds 节点alive check的ttl

sentinel parallel-syncs 主节点宕机,容许并行复制数量

sentinel failover-timeout 故障转移的超时时间,若是当前时间-上次转移状态更新时间大于该值,则会终止转移。

sentinel notification-script 故障监控,sentinel警告级别的事件发生,会出发对应路径下的脚本,经过脚本可接收参数。进行监控。

客户端链接sentinel

启动经过遍历sentinel获取redis主节点。而后订阅每一个sentinel的switch事件,保证在主备切换的时候能监听到。若是监听到变化 从新初始化链接池便可。

须要注意的是,只有故障转移完成才会发送此事件。

6、总结

整个实现比较复杂,可是按照每一个点仍是能够理清楚。经过ping、info、以及pubsub hello消息实现通讯。保证整个系统的一致性。每次故障转移只会由一个sentinel执行,这个选举过程依赖raft算法leader选举逻辑。故障转移逻辑依赖超时时间避免死状态。整个逻辑依赖状态机进行切换,有条不紊,值得借鉴。

相关文章
相关标签/搜索