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

2020-06-15

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

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

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


1. 故障转移流程

  1. sentinel 时钟定时检查监控的各个 redis 实例角色,是否通信异常。
  2. 发现 master 主观下线。
  3. 向其它 sentinel 节点询问它们是否也检测到该 master 主观下线。
  4. sentinel 通过询问,确认 master 客观下线。
  5. 进入选举环节,sentinel 向其它 sentinel 节点拉票,希望它们选自己为代表进行故障转移。
  6. 少数服从多数,当超过法定 sentinel 个数选择某个 sentinel 为代表。
  7. sentinel 代表执行故障转移。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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. 主观下线

主要检查节点间的 心跳 通信是否正常。

  • 检测异步链接是否超时,超时则关闭链接。
  • 检测心跳是否超时,超时则标识主观下线,否则恢复正常。
  • master 角色误报,超时标识主观下线。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 发现了 master 主观下线,所以先进入询问环节,再进行选举拉票。


1
2
# is-master-down-by-addr 命令格式。
SENTINEL is-master-down-by-addr <masterip> <masterport> <sentinel.current_epoch> <*>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* 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 接收命令。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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 被标识为客观下线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 客观下线,可以进入开启故障转移流程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* 定时检查 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. 参考