Redis源码 - Sentinel Q&A
一篇文章很难把所有Sentinel的问题谈完
本文以Q&A形式对前文进行补充
源码中大量的sentinelEvent是做什么的?
函数最终是通过调用 pubsubPublishMessage 函数,来实现向某一个频道发布消息的
频道创建及发布订阅是如何实现的?
- initServer初始化channels for pubsub: server.pubsub_channels = dictCreate(&keylistDictType,NULL); 所有发布订阅频道都保存在pubsub_channels,每个频道
- 根据master配置和master->slaves列表,建立pc连接
- 再master/slaves创建sentinel:hello频道,所以sentinel订阅这个频道,彼此发现对方建立连接
- publishCommand 命令最终都会使用pubsubPublishMessage发布消息
- subscribeCommand 命令使用 pubsubSubscribeChannel 传递消息
failover之后,旧的master又上线了,如何处理?(假设没有网络分区)
原 master 恢复正常,重新与 sentinel连接,这时候已经产生新的 master 了,所以旧 master,需要被 sentinel 降级为 slave
sentinel通过sentinelInfoReplyCallback 函数处理,调用sentinelRefreshInstanceInfo
sentinel 将旧 master 记录为 slave 了,旧 master 通过 info 还上报 master 角色,此时需要发送 "slaveof" 命令将它降级为 slave
由于redis是异步复制,可能有部分数据丢失,见前面的relication文章
切换新master,slave如何更新?
应用场景:failover成功后,更新master信息
sentinelFailoverReconfNextSlave 函数处理
sentinel leader,根据parallel_syncs 配置决定一次更新多少了个Slave(保证时刻有slave可用)
如何判定slave更新连接至新master?
sentinelRefreshInstanceInfo函数处理
发送给slave info命令再此查询,而不是等待slave回复--异步操作贯穿redis生命周期
如果slave一直更新不成功呢?简单直接终止,最后sentinelFailoverDetectEnd还会再尝试一次
if (elapsed > master->failover_timeout) {
not_reconfigured = 0;
timeout = 1;
sentinelEvent(LL_WARNING, "+failover-end-for-timeout", master, "%@");
}
sentinel接点之间是如何发现的?
通过Auto-discover特性
这一特性是通过向 sentinel:hello 的频道发送 hello 消息来实现的,利用👆的pubsub channels和link->pc链接
类似的,配置master的slaves列表,sentinel 将通过查询INFO命令, redis 自动发现此列表
每个 Sentinel 定期向每个受监视的master和slave的频道 sentinel:hello 发布一条消息,以 ip、端口、runid 的形式
每个 Sentinel 订阅每个master和slave的频道 sentinel:hello,以查找未知的 Sentinel。当检测到新的 Sentinel 时,它们会被添加为该master的 Sentinel
sentinel HA如何做的?
-
Redis Sentinel 一个集群至少三个节点,独立部署
-
Redis Sentinel 之间通过auto-discover 机制发现彼此(下一篇文章会介绍)
-
Redis Sentinel 保证了liveness属性,即如果大多数 sentinels 能够通信,最终将有一个 sentinel 被授权进行故障转移,如果主服务器宕机
-
Redis Sentinel 还保证了safely属性,即每个 Sentinel 将使用不同的配置时期对同一个主服务器进行故障转移,总会有一个执行failover
-
Sentinel state 持久化,sentinel 的状态被持久化在 sentinel 配置文件中。例如,每当为一个master收到或创建一个新的配置时,该配置与配置时期一起被持久化到磁盘上。这意味着停止和重新启动 Sentinel 进程是安全的
-
TILT 模式
-
Redis Sentinel 严重依赖于计算机时间:例如,为了确定一个实例是否可用,它会记住对 PING 命令的最新成功回复的时间,并将其与当前时间进行比较
-
如果计算机时间以意外的方式更改,或者计算机非常繁忙,或者进程由于某种原因被阻塞(process blocked),sentinel 可能会有意外的行为
-
TILT 模式是 Sentinel 可以进入的特殊“保护”模式,用于检测到某些异常情况可能降低系统可靠性时。sentinel 定时器中断默认每秒调用 10 次,因此我们预计在两次定时器中断调用之间会经过大约 100 毫秒
-
Sentinel 的操作是记录前一次定时器中断被调用的时间,并将其与当前调用进行比较:如果时间差为负数或异常大(2 秒或更长时间),则进入 TILT 模式(或者如果已进入 TILT 模式,则退出 TILT 模式的延迟)
-
TILT模式下sentinel依然完成monitor工作
- 暂停其他工作
- 恢复SENTINEL is-master-down-by-addr命令为负,告诉其他节点自己的detect a failure 不能被信任
- 如果一切正常持续 30 秒,TILT 模式将退出
-
-
-
在 Sentinel 的 TILT 模式下,如果我们发送 INFO 命令,我们可能会得到以下响应:
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_tilt_since_seconds:-1
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=0,sentinels=1
字段 "sentinel_tilt_since_seconds" 表示 Sentinel 已经处于 TILT 模式多少秒了。如果不处于 TILT 模式,则该值为 -1。
请注意,在某些情况下,可以使用许多内核提供的单调时钟 API 替代 TILT 模式
在Linux下,单调时钟API通常是由clock_gettime()函数提供的,使用时需要指定时钟ID为CLOCK_MONOTONIC。这个函数可以获取一个单调递增的时间值,不受系统时间的影响,用于计算时间间隔和测量时间段
sentinel scaling 有什么影响?
scaling out时 根据新增的sentinel runid,删除已有节点,再添加新节点
scaling in 时,当节点<3之后影响failure detect & failover, 大于3无影响
sentinel既然可以提供配置更新,如何做的版本控制?
uint64_t config_epoch; / Configuration epoch. /
Sentinel 需要从大多数成员那里获取授权,以便启动故障转移:
当 Sentinel 被授权时(成为leader),它会为正在执行故障转移的master获取一个唯一的config_epoch。
将在故障转移完成后用于对新配置进行版本控制
这意味着每个故障转移的每个配置都使用唯一的版本进行版本控制
/* Update master info if received configuration is newer. */
if (si && 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(LL_WARNING,"+config-update-from",si,"%@");
sentinelEvent(LL_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);
}
}
configuration 如何更新?
Configuration propagation 机制
一旦一个 sentinel 成功执行了master的故障转移,它将开始广播新的配置,以便其他 sentinel 更新它们对于给定主服务器的信息
需要 sentinel 能够向所选的副本发送 REPLICAOF NO ONE 命令,并且稍后在master的 INFO 输出中观察到slave切换到master,此时设置故障转移为成功
在此时,即使slave的重新配置正在进行中,故障转移也被视为成功,并且所有 sentinel 都需要开始报告新的配置。
新配置传播的方式是我们需要每个 sentinel 故障转移都使用不同版本号(config_epoch)来授权的原因。
每个 sentinel 都会持续广播其对于一个master的配置版本,使用 redis 发布/订阅消息。同时,所有 Sentinel 都等待消息,以查看其他 sentinel 广播的配置是什么。
最后,拥有最大version的是winner,其他sentinel依据winner更新
sentinel 网络分区下的一致性,如何处理?
sentinel 的状态信息保存在config里,而sentinel 配置是最终一致。
因此每个分区将收敛到可用的较高配置。然而,在使用 Sentinel 的真实系统中,有三种不同的参与者:
- Redis 实例
- Sentinel 实例
- 客户端
为了定义系统的行为,我们必须考虑所有三种角色。
以下是一个简单的网络示例,其中有 3 个节点,每个节点都运行一个 Redis 实例和一个 Sentinel 实例:
+-------------+
| Sentinel 1 |----- Client A
| Redis 1 (M) |
+-------------+
|
|
+-------------+ | +------------+
| Sentinel 2 |-----+-- // ----| Sentinel 3 |----- Client B
| Redis 2 (S) | | Redis 3 (M)|
+-------------+ +------------+
Sentinel 属性保证了 Sentinel 1 和 Sentinel 2 现在具有主节点的新配置。然而,Sentinel 3 仍然具有旧配置,因为它位于不同的分区中
我们知道,当网络分区恢复时,Sentinel 3 将会更新其配置,但在分区期间,如果有客户端与旧主节点分隔开来,会发生什么呢?
客户端仍然可以写入 Redis 3,即旧的主节点。当分区恢复时,Redis 3 将成为 Redis 1 的副本,并且在分区期间写入的所有数据都将丢失
不同的应用场景,会有不同的策略
-
如果将 redis 用作缓存,即使数据将丢失,client B 仍然能够写入旧的主节点,这可能很方便。 保证了应用端的HA
-
如果将 Redis 用作存储,您需要配置系统以部分防止此问题:
-
由于 Redis 是异步复制的,所以在这种情况下无法完全防止数据丢失,但是您可以使用以下 Redis 配置选项来限制 Redis 3 和 Redis 1 之间的差异:
- min-replicas-to-write 1
- min-replicas-max-lag 10
-
master根据👆的配置决定自身是否可写
- 无法写入至少 1 个副本,则会停止接受写入
- 由于复制是异步的,不能写入实际上意味着副本要么断开连接,要么在超过指定的最大延迟秒数lag未向master发送异步确认
-
使用此配置,以上示例中的 Redis 3 将在 10 秒后变为不可用。当分区恢复时,Sentinel 3 的配置将收敛到新配置,并且客户端 B 将能够获取有效的配置并继续操作
-
总之,Redis + Sentinel 作为一个整体是一个最终一致性系统,它的机制使得last failover wins
旧master的数据被丢弃以复制当前主节点的数据,因此始终存在丢失已确认写入的可能性窗口
避免丢失已确认写入的方法只有两种:
- 使用同步复制,并使用适当的共识算法来运行复制的状态机-raft
- 使用一个可以合并相同对象的不同版本的最终一致性系统-dynamodb
Redis 当前无法使用上述任何系统,并且目前超出了开发目标。然而,有一些代理在 Redis 存储之上实现了解决方案“2”,例如 SoundCloud Roshi 或 Netflix Dynomite
参考
Redis 5.0.1 source code