Redis高可用方案Sentinel

操作

  1. Master
    TODO

  2. Slave

  3. Sentinel

  4. 获取集群信息

    1
    redis-cli -p 26379 info Sentinel
  5. 获取 master 节点地址

    1
    redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster

客户端如何连接Sentinel集群

Sentinel
在 Sentinel 模式下,客户端不是直接连接服务器的,而是先访问 Sentinel 拿到集群信息再尝试连接 Master。当 Master 发生故障时,客户端会重新向 Sentinel 要地址,并自动完成节点切换。

  • Master 和 Slave 的配置和之前并无区别;
  • Sentinel 相当于对 Master 的代理,Sentinel 可以通过发布订阅功能获取到 Slave 和其他 Sentinel 的信息。

    其实 Sentinel 的内核与其他形式的 Redis 服务器基本一致,只是支持的命令不同、负责的任务也不同。

同理,客户端也可以通过pubsub功能来订阅集群中的其他信息,关键事件如下:
RedisSentinel事件

Sentinel 执行原理

Sentinel的主要任务
在Sentinel的主事件循环中可以看到它每100毫秒执行的定时任务:

1
2
3
4
5
6
7
8
9
10
11
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...

/* Run the Sentinel timer if we are in sentinel mode. */
// 如果服务器运行在 sentinel 模式下,那么执行 SENTINEL 的主函数
run_with_period(100) {
if (server.sentinel_mode) sentinelTimer();
}

...
}

实例状态探测

  • 每个 Sentinel 以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令
    如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线。 一个有效回复可以是: +PONG 、 -LOADING 或者 -MASTERDOWN。
    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
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    void sentinelHandleDictOfRedisInstances(dict *instances) {
    ...

    // 遍历多个实例,这些实例可以是多个主服务器、多个从服务器或者多个 sentinel
    di = dictGetIterator(instances);
    while((de = dictNext(di)) != NULL) {

    // 取出实例对应的实例结构
    sentinelRedisInstance *ri = dictGetVal(de);

    // 执行调度操作
    sentinelHandleRedisInstance(ri);

    // 如果被遍历的是主服务器,那么递归地遍历该主服务器的所有从服务器
    // 以及所有 sentinel
    if (ri->flags & SRI_MASTER) {

    // 所有从服务器
    sentinelHandleDictOfRedisInstances(ri->slaves);

    // 所有 sentinel
    sentinelHandleDictOfRedisInstances(ri->sentinels);

    // 对已下线主服务器(ri)的故障迁移已经完成
    // ri 的所有从服务器都已经同步到新主服务器
    if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
    // 已选出新的主服务器
    switch_to_promoted = ri;
    }
    }
    }

    ...
    }

    /* Perform scheduled operations for the specified Redis instance. */
    // 对给定的实例执行定期操作
    void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {

    /* ========== MONITORING HALF ============ */
    /* ========== 监控操作 =========*/

    /* Every kind of instance */
    /* 对所有类型实例进行处理 */

    // 如果有需要的话,创建连向实例的网络连接
    sentinelReconnectInstance(ri);

    // 根据情况,向实例发送 PING、 INFO 或者 PUBLISH 命令
    sentinelSendPeriodicCommands(ri);

    ...
    }

    // 根据时间和实例类型等情况,向实例发送命令,比如 INFO 、PING 和 PUBLISH
    // 虽然函数的名字包含 Ping ,但命令并不只发送 PING 命令
    /* Send periodic PING, INFO, and PUBLISH to the Hello channel to
    * the specified master or slave instance. */
    void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
    mstime_t now = mstime();
    mstime_t info_period, ping_period;
    int retval;

    /* Return ASAP if we have already a PING or INFO already pending, or
    * in the case the instance is not properly connected. */
    // 函数不能在网络连接未创建时执行
    if (ri->flags & SRI_DISCONNECTED) return;

    /* For INFO, PING, PUBLISH that are not critical commands to send we
    * also have a limit of SENTINEL_MAX_PENDING_COMMANDS. We don't
    * want to use a lot of memory just because a link is not working
    * properly (note that anyway there is a redundant protection about this,
    * that is, the link will be disconnected and reconnected if a long
    * timeout condition is detected. */
    // 为了避免 sentinel 在实例处于不正常状态时,发送过多命令
    // sentinel 只在待发送命令的数量未超过 SENTINEL_MAX_PENDING_COMMANDS 常量时
    // 才进行命令发送
    if (ri->pending_commands >= SENTINEL_MAX_PENDING_COMMANDS) return;

    /* If this is a slave of a master in O_DOWN condition we start sending
    * it INFO every second, instead of the usual SENTINEL_INFO_PERIOD
    * period. In this state we want to closely monitor slaves in case they
    * are turned into masters by another Sentinel, or by the sysadmin. */
    // 对于从服务器来说, sentinel 默认每 SENTINEL_INFO_PERIOD 秒向它发送一次 INFO 命令
    // 但是,当从服务器的主服务器处于 SDOWN 状态,或者正在执行故障转移时
    // 为了更快速地捕捉从服务器的变动, sentinel 会将发送 INFO 命令的频率该为每秒一次
    if ((ri->flags & SRI_SLAVE) &&
    (ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS))) {
    info_period = 1000;
    } else {
    info_period = SENTINEL_INFO_PERIOD;
    }

    /* We ping instances every time the last received pong is older than
    * the configured 'down-after-milliseconds' time, but every second
    * anyway if 'down-after-milliseconds' is greater than 1 second. */
    ping_period = ri->down_after_period;
    if (ping_period > SENTINEL_PING_PERIOD) ping_period = SENTINEL_PING_PERIOD;

    // 实例不是 Sentinel (主服务器或者从服务器)
    // 并且以下条件的其中一个成立:
    // 1)SENTINEL 未收到过这个服务器的 INFO 命令回复
    // 2)距离上一次该实例回复 INFO 命令已经超过 info_period 间隔
    // 那么向实例发送 INFO 命令
    if ((ri->flags & SRI_SENTINEL) == 0 &&
    (ri->info_refresh == 0 ||
    (now - ri->info_refresh) > info_period))
    {
    /* Send INFO to masters and slaves, not sentinels. */
    retval = redisAsyncCommand(ri->cc,
    sentinelInfoReplyCallback, NULL, "INFO");
    if (retval == REDIS_OK) ri->pending_commands++;
    } else if ((now - ri->last_pong_time) > ping_period) {
    /* Send PING to all the three kinds of instances. */
    sentinelSendPing(ri);
    } else if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
    /* PUBLISH hello messages to all the three kinds of instances. */
    sentinelSendHello(ri);
    }
    }

从主观下线到客观下线

  • 如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有 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
    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
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    /* Perform scheduled operations for the specified Redis instance. */
    // 对给定的实例执行定期操作
    void sentinelHandleRedisInstance(sentinelRedisInstance *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. */
    // 如果 Sentinel 处于 TILT 模式,那么不执行故障检测。
    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);

    ...
    }

    /* Is this instance down from our point of view? */
    // 检查实例是否已下线(从本 Sentinel 的角度来看)
    void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {

    ...

    /* Update the SDOWN flag. We believe the instance is SDOWN if:
    *
    * 更新 SDOWN 标识。如果以下条件被满足,那么 Sentinel 认为实例已下线:
    *
    * 1) It is not replying.
    * 它没有回应命令
    * 2) We believe it is a master, it reports to be a slave for enough time
    * to meet the down_after_period, plus enough time to get two times
    * INFO report from the instance.
    * Sentinel 认为实例是主服务器,这个服务器向 Sentinel 报告它将成为从服务器,
    * 但在超过给定时限之后,服务器仍然没有完成这一角色转换。
    */
    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(REDIS_WARNING,"+sdown",ri,"%@");
    // 记录进入 SDOWN 状态的时间
    ri->s_down_since_time = mstime();
    // 打开 SDOWN 标志
    ri->flags |= SRI_S_DOWN;
    }
    } else {
    // 移除(可能有的) SDOWN 状态
    /* Is subjectively up */
    if (ri->flags & SRI_S_DOWN) {
    // 发送事件
    sentinelEvent(REDIS_WARNING,"-sdown",ri,"%@");
    // 移除相关标志
    ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
    }
    }
    }
    如果有足够数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线
    这个数量是可以配置的,即quorum的数量。
    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
    /* Perform scheduled operations for the specified Redis instance. */
    // 对给定的实例执行定期操作
    void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    ...

    /* ============== ACTING HALF ============= */
    /* ============== 故障检测 ============= */

    ...这里省略SDOWN检测代码

    /* 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 命令
    // 刷新其他 Sentinel 关于主服务器的状态
    // 这一句是对那些没有进入 if(sentinelStartFailoverIfNeeded(ri)) { /* ... */ }
    // 语句的主服务器使用的
    sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
    }
  • 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。 当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
    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 sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {

    ...

    /* If this is a slave of a master in O_DOWN condition we start sending
    * it INFO every second, instead of the usual SENTINEL_INFO_PERIOD
    * period. In this state we want to closely monitor slaves in case they
    * are turned into masters by another Sentinel, or by the sysadmin. */
    // 对于从服务器来说, sentinel 默认每 SENTINEL_INFO_PERIOD 秒向它发送一次 INFO 命令
    // 但是,当从服务器的主服务器处于 SDOWN 状态,或者正在执行故障转移时
    // 为了更快速地捕捉从服务器的变动, sentinel 会将发送 INFO 命令的频率该为每秒一次
    if ((ri->flags & SRI_SLAVE) &&
    (ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS))) {
    info_period = 1000;
    } else {
    info_period = SENTINEL_INFO_PERIOD;
    }

    ...

    // 实例不是 Sentinel (主服务器或者从服务器)
    // 并且以下条件的其中一个成立:
    // 1)SENTINEL 未收到过这个服务器的 INFO 命令回复
    // 2)距离上一次该实例回复 INFO 命令已经超过 info_period 间隔
    // 那么向实例发送 INFO 命令
    if ((ri->flags & SRI_SENTINEL) == 0 &&
    (ri->info_refresh == 0 ||
    (now - ri->info_refresh) > info_period))
    {
    /* Send INFO to masters and slaves, not sentinels. */
    retval = redisAsyncCommand(ri->cc,
    sentinelInfoReplyCallback, NULL, "INFO");
    if (retval == REDIS_OK) ri->pending_commands++;
    } else if
    ...
    }
    }
    注意上边发请求时使用的回调函数sentinelInfoReplyCallback
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 处理 INFO 命令的回复
    void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) {
    sentinelRedisInstance *ri = c->data;
    redisReply *r;

    if (ri) ri->pending_commands--;
    if (!reply || !ri) return;
    r = reply;

    if (r->type == REDIS_REPLY_STRING) {
    // 解析info命令的响应数据
    sentinelRefreshInstanceInfo(ri,r->str);
    }
    }
  • 当没有足够数量的 Sentinel 同意主服务器已经下线,主服务器的客观下线状态就会被移除。
    当主服务器重新向 Sentinel 的 PING 命令返回有效回复时,主服务器的主观下线状态就会被移除。

故障转移 - 选举 Sentinel Leader

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
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {

...

/* ============== ACTING HALF ============= */
/* ============== 故障检测 ============= */

...

/* 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 命令
// 刷新其他 Sentinel 关于主服务器的状态
// 这一句是对那些没有进入 if(sentinelStartFailoverIfNeeded(ri)) { /* ... */ }
// 语句的主服务器使用的
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}

状态感知(info)

Sentinel服务器只需配置Master的地址,其他Slave的信息是通过定时(10秒)向Master发送info命令来获取的,info命令返回的信息中,包含了主从拓扑关系,其中包括每个slave的地址和端口号。有了这些信息后,哨兵就会记住这些节点的拓扑信息,在后续发生故障时,选择合适的slave节点进行故障恢复。
哨兵除了向master发送info之外,还会向每个master节点特殊的pubsub中发送master当前的状态信息和哨兵自身的信息,其他哨兵节点通过订阅这个pubsub,就可以拿到每个哨兵发来的信息。这么做的目的主要有2个:

  • 哨兵节点可以发现其他哨兵的加入,进而方便多个哨兵节点通信,为后续共同协商提供基础
  • 与其他哨兵节点交换master的状态信息,为后续判断master是否故障提供依据

心跳检测(ping)

故障发生时,需要立即启动故障恢复机制,那么Sentinel怎么及时地知道哪些节点发生故障了呢?这主要是通过向所有其他节点发送PING命令来实现的。
每个哨兵节点每隔1秒向master、slave、其他哨兵节点发送ping命令,如果对方能在指定时间内响应,说明节点健康存活。如果未在规定时间内(可配置)响应,那么该哨兵节点认为此节点主观下线

至于Sentinel怎么知道其他节点的地址,其实就是通过前面提到的info命令来感知的。

主观下线和客观下线

  • 主观下线(Subjectively Down, 简称 SDOWN)
    主观下线指的是单个 Sentinel 实例对服务器做出的下线判断。
    如果一个服务器没有在 master-down-after-milliseconds 选项所指定的时间内, 对向它发送 PING 命令的 Sentinel 返回一个有效回复(有效回复只有+PONG、-LOADING 错误或 -MASTERDOWN 错误), 那么 Sentinel 就会将这个服务器标记为主观下线。

    注意是在master-down-after-milliseconds时间内一直返回无效回复。

  • 客观下线(Objectively Down, 简称 ODOWN)
    客观下线指的是多个 Sentinel 实例在对同一个 Master 做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后,得出的服务器下线判断。 (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)
    从主观下线切换到客观下线并不是通过较严格的投票算法,而是采用了流言协议(gossip protocol):只要 Sentinel 在给定时间内从其他 Sentinel 接收到足够数量的 Master 下线通知,那么 Sentinel 就会执行状态的切换;如果之后其他 Sentinel 不再报告 Master 已下线,则客观下线状态就会被移除。
    只要一个 Sentinel 发现某个 Master 进入客观下线状态,之后就会进入故障迁移阶段,选举出一个 Sentinel 对失效的 Master 执行自动故障迁移操作。

    客观下线只适用于 Master,对 Slave 或 Sentinel 则不会达到客观下线状态。

故障迁移(Master 挂掉,Sentinel选举新Master)

单纯的主从架构并不能挽救 Master 挂掉的情况,因此引入了 Sentinel 集群。Sentinel 会不断地检查集群主服务器和从服务器是否运作正常,并在超过 n 个 Sentinel 同意后判断主节点失效(配置sentinel monitor mymaster 127.0.0.1 6379 2表示这个n=2),不过要注意,无论设置多少个 Sentinel 同意才能判断一个服务器失效, 一个 Sentinel 都需要获得系统中多数 Sentinel 的支持, 才能发起一次自动故障迁移。

  • 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作,它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器;
  • 当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器代替失效服务器。

故障转移主要分为Sentinel选举和故障转移(Master替换)两个步骤,Sentinel选主流程如下:

  • Sentinel发现主服务器已经进入客观下线状态。
  • 利用Raft leader election算法选举 Sentinel 中的 Leader,对我们的当前 epoch 进行自增, 并尝试在这个epoch中当选,之后,所有 Sentinel 都以更高的 epoch 为准,并主动用更新的 epoch 代替自己的配置。
  • 如果当选失败, 那么在设定的故障迁移超时时间的两倍之后,重新尝试当选。 如果当选成功, 那么执行Slave的选主。

Slave选举

Slave选举的规则如下:

  • 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被淘汰。
  • 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。
  • 在经历了以上两轮淘汰之后剩下来的从服务器中, 我们选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器; 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID 的那个从服务器成为新的主服务器。

也就是说,多个Slave的优先级按照:slave-priority配置 > 数据完整性 > runid较小者进行选择。

之后所有Sentinel要进行投票选出一个Leader:
RedisSentinel投票

选出Leader后,Leader需要从现有的Slave中选出

故障转移

提升新的Master的流程如下:

  • 向被选中的从服务器发送 SLAVEOF NO ONE 命令,让它转变为主服务器。
  • 通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新。
  • 向已下线主服务器的从服务器发送 SLAVEOF 命令, 让它们去复制新的主服务器。
  • 当所有从服务器都已经开始复制新的主服务器时, 领头 Sentinel 终止这次故障迁移操作。

客户端感知新master流程如下:
哨兵在故障切换完成之后,会向自身节点的指定pubsub中写入一条信息,客户端可以订阅这个pubsub来感知master的变化通知。我们的客户端也可以通过在哨兵节点主动查询当前最新的master,来拿到最新的master地址。
另外,哨兵还提供了“钩子”机制,我们也可以在哨兵配置文件中配置一些脚本逻辑,在故障切换完成时,触发“钩子”逻辑,通知客户端发生了切换,让客户端重新在哨兵上获取最新的master地址。
一般来说,推荐采用第一种方式进行处理,很多客户端SDK中已经集成好了从哨兵节点获取最新master的方法,我们直接使用即可。

Sentinel 选举的安全性

配置安全性:

  • 每当一个 Redis 实例被重新配置(reconfigured) —— 无论是被设置成主服务器、从服务器、又或者被设置成其他主服务器的从服务器 —— Sentinel 都会向被重新配置的实例发送一个 CONFIG REWRITE 命令, 从而确保这些配置会持久化在硬盘里。完成重新配置之后,从服务器会去复制正确的主服务器。
  • Sentinel 的状态会被持久化到 Sentinel 配置文件里,当 Sentinel 接收到新配置或 Leader Sentinel 为 Master 创建一个新配置时,这些配置都会与epoch一起被保存到磁盘;

故障自动迁移的一致性:

  • Raft 算法保证在一个 epoch 里只有一个 Leader Sentinel 产生,减少了脑裂的风险;
  • Sentinel 集群总是以更高的 epoch 为准,因为发生网络分区(network partition)时可能会有 Sentinel 包含老的配置,而当这个 Sentinel 服务器接收到其他 Sentinel 的版本更新配置时就会进行更新。
  • 发生网络分区并且某些 Sentinel 仍在采用老的配置时,如果有客户端连接到这些 Sentinel 上,最终可能就会将请求转发到非 Master 服务器上,造成数据不一致。因此,应该使用 min-slaves-to-write 选项, 让主服务器在连接的从实例少于给定数量时停止执行写操作, 与此同时, 应该在每个运行 Redis 主服务器或从服务器的机器上运行 Redis Sentinel 进程。

Sentinel 故障迁移的实时性

故障迁移虽然能提供主从切换来保证挂掉的Master能被其他Slave顶替上来,但是这个顶替过程大概需要多长时间呢?具体又是哪些步骤会比较耗时?

  1. 判断Master下线
    Sentinel会PING Master,如果距离上次PING成功的时间超过了master-down-after-milliseconds时间,则表示主观下线了。
    将Master标记为SDOWN后,这个Sentinel会通过发事件消息来通知其他Sentinel。

    Cluster中是通过gossip协议来通知其他节点的。

  2. 重新选主

  3. Slave提升

这个实时性的讨论并不是纯粹的极客行为,因为切换要多长时间是评估我们服务可用性的重要指标,并且提供后续优化的指导方向。

TILT 模式

TILT 模式是一种特殊的保护模式,Sentinel 每隔 100ms 会向实例发一次PING命令,并将上一次 PING 成功的时间和当前时间比对,从而知道与该实例有多长时间没有进行任何成功通讯:

  • 如果两次调用时间之间的差距为负值, 或者非常大(超过 2 秒钟), 那么 Sentinel 进入 TILT 模式。
  • 如果 Sentinel 已经进入 TILT 模式, 那么 Sentinel 延迟退出 TILT 模式的时间。

    Sentinel严重依赖计算机的时间功能,一旦计算机的时间功能出现故障, 或者计算机非常忙碌, 又或者进程因为某些原因而被阻塞时, Sentinel 可能也会跟着出现故障。

进入 TILT 模式后,Sentinel 仍然会继续监视所有目标,但是:

  • 它不再执行任何操作,比如故障转移。
  • 当有实例向这个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令时, Sentinel 返回负值: 因为这个 Sentinel 所进行的下线判断已经不再准确。

TILT 相当于降级,如果 Sentinel 可以在 TILT 模式下正常维持 30s,那么 Sentinel 会退出 TILT 模式。

BUSY 状态

当 Lus 脚本执行时间超过阈值,Redis 会返回BUSY错误,当出现这种情况时, Sentinel 在尝试执行故障转移操作之前, 会先向服务器发送一个 SCRIPT KILL 命令, 如果服务器正在执行的是一个只读脚本的话, 那么这个脚本就会被杀死, 服务器就会回到正常状态。

脑裂

虽然Sentinel利用Raft选举不会发生脑裂,但是在一些极端的情况下还是有可能会发生脑裂的,比如:

  1. 原Master不能提供服务了,但是它本身并没有挂掉;
  2. Sentinel发现连不上Master,于是判定客观下线,并发起主从切换;
  3. 原Master和新Master同时给Client提供服务,发生脑裂。

这种脑裂并不会影响可用性,但是却破坏了数据的一致性,甚至会导致数据丢失:在Sentinel重连上原Master后,会将其归入到新Master的Slave,这时脑裂期间的数据就会被从新Master上复制过来的数据覆盖掉了,导致数据的丢失。

脑裂的解决办法主要是以下两个配置参数:

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

QA

5个哨兵的集群,quorum设置为2,在运行过程中,有3个实例都发生了故障,这时主库也发生了故障,还能正确判断主库的客观下线吗?还能执行主从的自动切换吗?

判断客户端下线是可以的,因为判断ODOWN的条件是有不少于quorum数量的Sentinel同意即可。
不可执行主从切换,因为一个哨兵要执行主从切换,得获得半数以上哨兵的投票同意,也就是3个哨兵。

哨兵实例是不是越多越好?

哨兵实例越多,误判率越低,但是判断主库下线和选举Leader时实例要拿到的赞成票也越多,主从切换花费的时间也相对会更多。
如果客户端对Redis的响应时间有要求,则很有可能会报警。

调大down-after-milliseconds对减少误判是不是有好处?

这个值的作用是:判断距离上次PING成功的时间超过了这个值,就标记实例主观下线。
调大的话Sentinel需要更长的时间才能判断集群出问题了,也即影响到Redis的可用性。

参考

  1. Sentinel