[redis 源码走读] sentinel 哨兵 - 主客观下线

redis 哨兵集群有 3 个角色:sentinel/master/slave,每个角色都可能出现故障,故障转移主要针对 master ,而且故障转移是个复杂的工作流程。在分布式系统中,多个节点要保证数据一致性,需要相互通信协调,要经历几个环节:

master 主观下线 --> master 客观下线 --> 投票选举 leader --> leader 执行故障转移。

本章重点走读 redis 源码,理解 sentinel 检测 master 节点的主客观下线流程。

🔥 文章来源 《[redis 源码走读] sentinel 哨兵 - 主客观下线》


1. 故障转移流程

  1. sentinel 时钟定时检查监控的各个 redis 实例角色,是否通信异常。
  2. 发现 master 主观下线。
  3. 向其它 sentinel 节点询问它们是否也检测到该 master 主观下线。
  4. sentinel 通过询问,确认 master 客观下线。
  5. 进入选举环节,sentinel 向其它 sentinel 节点拉票,希望它们选自己为代表进行故障转移。
  6. 少数服从多数,当超过法定 sentinel 个数选择某个 sentinel 为代表。
  7. sentinel 代表执行故障转移。
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    ...
    /* 检查 sentinel 是否处在异常状态,例如本地时间忽然改变,因为心跳通信等,依赖时间。*/
    if (sentinel.tilt) {
        if (mstime() - sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
        sentinel.tilt = 0;
        sentinelEvent(LL_WARNING, "-tilt", NULL, "#tilt mode exited");
    }

    /* 检查所有节点类型 sentinel/master/slave,是否主观下线。*/
    sentinelCheckSubjectivelyDown(ri);
    ...
    if (ri->flags & SRI_MASTER) {
        /* 检查 master 是否客观下线。 */
        sentinelCheckObjectivelyDown(ri);
        /* 是否满足故障转移条件,开启故障转移。 */
        if (sentinelStartFailoverIfNeeded(ri))
            /* 满足条件,进入故障转移环节,马上向其它 sentinel 节点选举拉票。 */
            sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_ASK_FORCED);
        /* 通过状态机,处理故障转移对应各个环节。 */
        sentinelFailoverStateMachine(ri);
        /* 定时向其它 sentinel 节点询问 master 主观下线状况或选举拉票。 */
        sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_NO_FLAGS);
    }
}

2. 故障发现

主客观下线时序

2.1. 主观下线

主要检查节点间的 <font color=red>心跳</font> 通信是否正常。

  • 检测异步链接是否超时,超时则关闭链接。
  • 检测心跳是否超时,超时则标识主观下线,否则恢复正常。
  • master 角色误报,超时标识主观下线。
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
    mstime_t elapsed = 0;

    /* 通过心跳通信间隔判断掉线逻辑。 */
    if (ri->link->act_ping_time)
        elapsed = mstime() - ri->link->act_ping_time;
    else if (ri->link->disconnected)
        elapsed = mstime() - ri->link->last_avail_time;

    /* tcp 异步链接通信超时关闭对应链接。 */
    ...

    /* 主观下线
     * 1. 心跳通信超时。
     * 2. 主服务节点却上报从服务角色,异常情况超时。 */
    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))) {
        /* Is subjectively down */
        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);
        }
    }
}

2.2. 客观下线

  • 询问主观下线。

当 sentinel 检测到 master 主观下线,它会询问其它 sentinel(发送 IS-MASTER-DOWN-BY-ADDR 请求):是否也检测到该 master 已经主观下线了。


SENTINEL IS-MASTER-DOWN-BY-ADDR 命令有两个作用:

  1. 询问其它 sentinel 节点,该 master 是否已经主观下线。命令最后一个参数为 <*>。
  2. 确认 master 客观下线,当前 sentinel 向其它 sentinel 拉选票,让其它 sentinel 选自己为 “代表”。命令最后一个参数为 <sentinel_runid>,sentinel 自己的 runid。

这里是 sentinel 发现了 master 主观下线,所以先进入询问环节,再进行选举拉票。


# is-master-down-by-addr 命令格式。
SENTINEL is-master-down-by-addr <masterip> <masterport> <sentinel.current_epoch> <*>
/* If we think the master is down, we start sending
 * SENTINEL IS-MASTER-DOWN-BY-ADDR requests to other sentinels
 * in order to get the replies that allow to reach the quorum
 * needed to mark the master in ODOWN state and trigger a failover. */
#define SENTINEL_ASK_FORCED (1 << 0)

void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
    dictIterator *di;
    dictEntry *de;

    di = dictGetIterator(master->sentinels);
    while ((de = dictNext(di)) != NULL) {
        sentinelRedisInstance *ri = dictGetVal(de);
        ...
        /* Only ask if master is down to other sentinels if:
         *
         * 1) We believe it is down, or there is a failover in progress.
         * 2) Sentinel is connected.
         * 3) We did not receive the info within SENTINEL_ASK_PERIOD ms. */
        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;

        /* 当 sentinel 检测到 master 主观下线,那么参数发送 "*",等待确认客观下线,
         * 当确认客观下线后,再进入选举环节。sentinel 再向其它 sentinel 发送自己的 runid,去拉票。*/
        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++;
    }
    dictReleaseIterator(di);
}
  • 其它 sentinel 接收命令。
void sentinelCommand(client *c) {
    ...
    else if (!strcasecmp(c->argv[1]->ptr, "is-master-down-by-addr")) {
        ...
        /* 其它 sentinel 接收到询问命令,根据 ip 和 端口查找对应的 master。 */
        ri = getSentinelRedisInstanceByAddrAndRunID(
            sentinel.masters, c->argv[2]->ptr, port, NULL);

        /* 当前 sentinel 如果没有处于异常保护状态,而且也检测到询问的 master 已经主观下线了。 */
        if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) && (ri->flags & SRI_MASTER))
            isdown = 1;

        /* 询问 master 主观下线命令参数是 *,选举投票参数是请求的 sentinel 的 runid。*/
        if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr, "*")) {
            leader = sentinelVoteLeader(ri, (uint64_t)req_epoch, c->argv[5]->ptr, &leader_epoch);
        }

        /* 根据询问主观下线或投票选举业务确定回复的内容参数。 */
        addReplyArrayLen(c, 3);
        addReply(c, isdown ? shared.cone : shared.czero);
        addReplyBulkCString(c, leader ? leader : "*");
        addReplyLongLong(c, (long long)leader_epoch);
        if (leader) sdsfree(leader);
    }
    ...
}
  • 当前 sentinel 接收命令回复。

当前 sentinel 接收到询问的回复,如果确认该 master 已经主观下线,那么将其标识为 SRI_MASTER_DOWN

/* Receive the SENTINEL is-master-down-by-addr reply, see the
 * sentinelAskMasterStateToOtherSentinels() function for more information. */
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
    ...
    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 sentinel 回复,也检测到该 master 节点已经主观下线。 */
            ri->flags |= SRI_MASTER_DOWN;
        } else {
            ri->flags &= ~SRI_MASTER_DOWN;
        }
        ...
    }
}

  • 确认客观下线

当 >= 法定个数(quorum)的 sentinel 节点确认该 master 主观下线,那么标识当前主观下线的 master 被标识为客观下线。

void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
    dictIterator *di;
    dictEntry *de;
    unsigned int quorum = 0, odown = 0;

    if (master->flags & SRI_S_DOWN) {
        /* Is down for enough sentinels? */
        quorum = 1; /* the current sentinel. *
        /* Count all the other sentinels. */
        di = dictGetIterator(master->sentinels);
        while ((de = dictNext(di)) != NULL) {
            sentinelRedisInstance *ri = dictGetVal(de);
            /* 该 ri 检测到 master 主观掉线。 */
            if (ri->flags & SRI_MASTER_DOWN) {
                quorum++;
            }
        }
        dictReleaseIterator(di);
        /* 是否满足当前 sentinel 配置的法定个数:quorum。 */
        if (quorum >= master->quorum) odown = 1;
    }

    /* Set the flag accordingly to the outcome. */
    if (odown) {
        if ((master->flags & SRI_O_DOWN) == 0) {
            sentinelEvent(LL_WARNING, "+odown", master, "%@ #quorum %d/%d",
                          quorum, master->quorum);
            master->flags |= SRI_O_DOWN;
            master->o_down_since_time = mstime();
        }
    } else {
        if (master->flags & SRI_O_DOWN) {
            sentinelEvent(LL_WARNING, "-odown", master, "%@");
            master->flags &= ~SRI_O_DOWN;
        }
    }
}

3. 开启故障转移

当 sentinel 检测到某个 master 客观下线,可以进入开启故障转移流程了。

/* 定时检查 master 故障情况情况。 */
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    ...
    if (ri->flags & SRI_MASTER) {
        /* 检查 master 是否客观下线。 */
        sentinelCheckObjectivelyDown(ri);
        /* 是否满足故障转移条件,开启故障转移。 */
        if (sentinelStartFailoverIfNeeded(ri))
            /* 满足条件,进入故障转移环节,马上向其它 sentinel 节点选举拉票。 */
            sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_ASK_FORCED);
        /* 通过状态机,处理故障转移对应各个环节。 */
        sentinelFailoverStateMachine(ri);
        /* 定时向其它 sentinel 节点询问 master 主观下线状况或选举拉票。 */
        sentinelAskMasterStateToOtherSentinels(ri, SENTINEL_NO_FLAGS);
    }
}

/* 是否满足故障转移条件,开启故障转移。 */
int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
    /* master 客观下线。 */
    if (!(master->flags & SRI_O_DOWN)) return 0;

    /* 当前 master 没有处在故障转移过程中。 */
    if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;

    /* 两次故障转移,需要有一定的时间间隔。
     * 1. 当前 sentinel 满足了故障转移条件。
     * 2. 当前 sentinel 接收到其它 sentinel 的拉票,也设置了 failover_start_time,说明
     *    其它 sentinel 先开启了故障转移,为了避免冲突,需要等待一段时间。*/
    if (mstime() - master->failover_start_time < master->failover_timeout * 2) {
        ...
        return 0;
    }

    sentinelStartFailover(master);
    return 1;
}

/* 开启故障转移,进入投票环节。 */
void sentinelStartFailover(sentinelRedisInstance *master) {
    ...
    /* 当前 master 开启故障转移。 */
    master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
    /* 当前 master 故障转移正在进行中。 */
    master->flags |= SRI_FAILOVER_IN_PROGRESS;
    /* 开始一轮选举,选举纪元(计数器 + 1)。*/
    master->failover_epoch = ++sentinel.current_epoch;
    ...
    /* 记录故障转移开启时间。 */
    master->failover_start_time = mstime() + rand() % SENTINEL_MAX_DESYNC;
    master->failover_state_change_time = mstime();
}

4. 参考


如果文章不错,给个点赞呗 ~ 谢谢。👍

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容