Redis高可用方案Sentinel
操作
Master
TODOSlave
Sentinel
获取集群信息
1
redis-cli -p 26379 info Sentinel
获取 master 节点地址
1
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
客户端如何连接Sentinel集群
在 Sentinel 模式下,客户端不是直接连接服务器的,而是先访问 Sentinel 拿到集群信息再尝试连接 Master。当 Master 发生故障时,客户端会重新向 Sentinel 要地址,并自动完成节点切换。
- Master 和 Slave 的配置和之前并无区别;
- Sentinel 相当于对 Master 的代理,Sentinel 可以通过发布订阅功能获取到 Slave 和其他 Sentinel 的信息。
其实 Sentinel 的内核与其他形式的 Redis 服务器基本一致,只是支持的命令不同、负责的任务也不同。
同理,客户端也可以通过pubsub功能来订阅集群中的其他信息,关键事件如下:
Sentinel 执行原理
在Sentinel的主事件循环中可以看到它每100毫秒执行的定时任务:
1 | int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { |
实例状态探测
- 每个 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
120void 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 要以每秒一次的频率确认主服务器的确进入了主观下线状态。如果有足够数量的 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);
}
}
}
这个数量是可以配置的,即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
37void 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 | void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { |
状态感知(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 判断, 并且通过 SENTINELis-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:
选出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顶替上来,但是这个顶替过程大概需要多长时间呢?具体又是哪些步骤会比较耗时?
判断Master下线
Sentinel会PING Master,如果距离上次PING成功的时间超过了master-down-after-milliseconds
时间,则表示主观下线了。
将Master标记为SDOWN后,这个Sentinel会通过发事件消息来通知其他Sentinel。Cluster中是通过gossip协议来通知其他节点的。
重新选主
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选举不会发生脑裂,但是在一些极端的情况下还是有可能会发生脑裂的,比如:
- 原Master不能提供服务了,但是它本身并没有挂掉;
- Sentinel发现连不上Master,于是判定客观下线,并发起主从切换;
- 原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的可用性。