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文档

Redis 5.0.1 source code