阅读 905

Redis专题:深入解读哨兵模式

公众号搜素“码路印记”,点关注不迷路!

Sentinel是Redis在主从复制基础上构建的一套高可用解决方案,本文将从下图几个方面来介绍Redis的Sentinel模式。

写在前面

上一篇文章《Redis专题:一文搞懂主从复制原理》我们了解到在主从模式下,主从复制机制使得slave成为与master完全一致的副本,一旦master宕机,我们可以选择一个正常的slave成为新的主节点,实现手动的故障恢复。

但是,人工干预效率低、易出错,并且故障感知滞后,不具备生产实用性。一个能够自动感知系统故障、自动故障转移的可靠组件,肯定是我们必须的。为此,Redis官方提供一个Redis的高可用方案——哨兵(Sentinel),使用它可以搭建一个即使无人干预也能抵抗某些类型失败的高可用的Redis分布式系统。这篇文章将对Redis的哨兵模式进行详细介绍,现在开始吧。

哨兵是Redis的一种运行模式,它专注于对Redis实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障转移,确保整个Redis系统的可用性。结合Redis的官方文档,可以知道Redis哨兵具备的能力有如下几个:

  • 监控(Monitoring):持续监控Redis主节点、从节点是否处于预期的工作状态。
  • 通知(Notification):哨兵可以把Redis实例的运行故障信息通过API通知监控系统或者其他应用程序。
  • 自动故障恢复(Automatic failover):当主节点运行故障时,哨兵会启动自动故障恢复流程:某个从节点会升级为主节点,其他从节点会使用新的主节点进行主从复制,通知客户端使用新的主节点进行。
  • 配置中心(Configuration provider):哨兵可以作为客户端服务发现的授权源,客户端连接到哨兵请求给定服务的Redis主节点地址。如果发生故障转移,哨兵会通知新的地址。这里要注意:哨兵并不是Redis代理,只是为客户端提供了Redis主从节点的地址信息。

哨兵模式是天然的分布式系统,它被设计为基于一套配置,并在多个哨兵实例的配合下工作。多实例共同协作有以下优势:

  • 主节点的系统故障是在多个实例共同认可的情况下完成的,大大降低了误报的概率。
  • 即使不是所有的哨兵实例都正常运行哨兵集群也能正常工作,这大大增加了系统的鲁棒性。

哨兵集群演示

说了这么多,不如实践看的直观,先通过一个例子看下Sentinel的高可用表现。由于Sentinel工作原理中需要对其他Sentinel节点和主从节点建立网络连接,其中要使用IP和端口,而Docker下采用的是端口映射或虚拟IP,导致在Docker运行需要一定的配置工作。所以,我这次直接在本地多实例运行。

搭建集群

本次实例采用三个哨兵节点、三个数据节点(一主二从)方式搭建,结构下图所示:

image.png 准备三个redis配置文件,对应Redis的一主二从,文件名称及内容如下:

# redis-6379.conf
port 6379
daemonize yes
pidfile /var/run/redis-6379.pid
logfile "6379.log"
dir "/Users/eleme/raysonxin/docker/redis-docker/log"
dbfilename dump-6379.rdb

# redis-6380.conf
port 6380
daemonize yes
pidfile /var/run/redis-6380.pid
logfile "6380.log"
dir "/Users/eleme/raysonxin/docker/redis-docker/log"
dbfilename dump-6380.rdb
slaveof 127.0.0.1 6379

# redis-6381.conf
port 6381
daemonize yes
pidfile /var/run/redis-6381.pid
logfile "6381.log"
dir "/Users/eleme/raysonxin/docker/redis-docker/log"
dbfilename dump-6381.rdb
slaveof 127.0.0.1 6379
复制代码

准备三个Redis Sentinel配置文件,作为三个监控节点,文件及内容如下:

# sentinel-26379.conf
port 26379
daemonize yes
dir "/Users/eleme/raysonxin/docker/redis-docker/log"
logfile "26379.log"
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 127.0.0.1 6379 2   
# 判断主节点时间
sentinel down-after-milliseconds mymaster 10000
sentinel parallel-syncs mymaster 1    
sentinel failover-timeout mymaster 180000

# sentinel-26380.conf
port 26380
daemonize yes
dir "/Users/eleme/raysonxin/docker/redis-docker/log"
logfile "26380.log"
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 127.0.0.1 6379 2   
# 判断主节点时间
sentinel down-after-milliseconds mymaster 10000 
sentinel parallel-syncs mymaster 1    
sentinel failover-timeout mymaster 180000

# sentinel-26381.conf
port 26381
daemonize yes
dir "/Users/eleme/raysonxin/docker/redis-docker/log"
logfile "26381.log"
# 配置监听的主服务器,这里sentinel monitor代表监控,mymaster代表服务器的名称,可以自定义,192.168.11.128代表监控的主服务器,6379代表端口,2代表只有两个或两个以上的哨兵认为主服务器不可用的时候,才会进行failover操作。
sentinel monitor mymaster 127.0.0.1 6379 2   
# 判断主节点时间
sentinel down-after-milliseconds mymaster 10000 
sentinel parallel-syncs mymaster 1    
sentinel failover-timeout mymaster 180000
复制代码

依次启动三个Redis Server,命令如下:

redis-server conf/redis-6379.conf
redis-server conf/redis-6380.conf
redis-server conf/redis-6381.conf
复制代码

通过客户端连接至127.0.0.1:6379,输入命令info replication,可以看到主从复制信息,说明一主二从工作正常。 image.png 依次启动三个Redis Sentinel,命令如下:

redis-sentinel conf/redis-26379.conf
redis-sentinel conf/redis-26380.conf
redis-sentinel conf/redis-26381.conf
复制代码

通过客户端连接至任意Sentinel节点,如127.0.0.1:26379,输入命令info sentinel,可以看到哨兵及主节点信息(如下图所示),主节点为6379数据节点,有两个从节点,3个哨兵节点。 image.png 查询当前的主节点,在哨兵客户端输入命令SENTINEL get-master-addr-by-name mymaster查询主节点,可以发现当前返回的是端口为6379的节点。 image.png

模拟主节点宕机

客户端连接至主节点,向主节点发送命令debug sleep 30,它会是的主节点阻塞30秒。然后再次执行查询主节点命令,可以看到主节点切换为端口为6381的节点。 image.png 向6381发送命令info replication可以看到6381切换为主节点,6379、6389变为6381的从节点。 image.png 在这个过程中,哨兵节点26380输出了以下日志内容,以下内容我们暂时不解释,等后面讲解了哨兵工作原理,谜底自然就会揭开。 image.png

Sentinel工作原理

哨兵是Redis的一种工作模式,以监控节点状态及执行故障转移为主要工作,哨兵总是以固定的频率去发现节点、故障检测,然后在检测到主节点故障时以安全的方式执行故障转移,确保集群的高可用性。

以下为哨兵周期性检查的核心逻辑,哨兵模式的原理部分也将以下面的代码为主线进行说明。

/* Perform scheduled operations for the specified Redis instance. */
/* Sentinel模式下,对节点执行的定时操作。属于最核心的函数了,包含节点发现、信息更新、故障检测、故障转移等操作 */
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
    /* ========== MONITORING HALF ============ */
    /* Every kind of instance */
    sentinelReconnectInstance(ri);
    /* 定时向主从节点、其他Sentinel节点发送PING、INFO、Hello(仅Sentinel)消息;
     * 以此实现了节点发现、节点信息更新、收集健康检测信息*/
    sentinelSendPeriodicCommands(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. */
    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节点生效 */
    sentinelCheckSubjectivelyDown(ri);

    /* Masters and slaves */
    if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
        /* Nothing so far. */
    }

    /* 以下操作仅针对主节点 */
    if (ri->flags & SRI_MASTER) {
        /* 判断主节点是否满足客观宕机条件 */
        sentinelCheckObjectivelyDown(ri);
        /* 检查是否可以开始故障转移操作 */
        if (sentinelStartFailoverIfNeeded(ri)) {
            /* 监控该主节点的Sentinel执行选主操作 */
            sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
        }
        /* 根据failover_state状态机,逐步执行故障转移操作 */
        sentinelFailoverStateMachine(ri);
        
        /* 这一步是向监控该主节点的其他Sentinel询问该主节点是否主观宕机,为主观宕机收集选票 */
        sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
    }
}
复制代码

节点自动发现

首先说明一下,这里的节点指的是其他哨兵节点及从节点。

回过头看下上面哨兵的配置文件,会发现我们仅配置了主节点的地址信息(host和port),并没有配置从节点及其他哨兵节点的信息。但是,我们在哨兵节点和主节点客户端通过info replicationinfo sentinel命令时却能够发现它们的存在。这一功能是由哨兵的自动发现机制实现的,我们来了解下。

先看数据节点

一般情况下,哨兵节点每隔10秒(故障转移时每隔1秒)向主从节点发送INFO命令,以此获取主从节点的信息。第一次执行时,哨兵仅知道我们给出的主节点信息,通过对主节点执行INFO命令就可以获取其从节点列表。如此周期性执行,就可以不断发现新加入的节点。 image.png

  • 如果INFO命令目标是从节点:哨兵从返回信息中获取从节点所属的最新主节点ip和port,如果与历史记录不一致,则执行更新;获取从节点的优先级、复制偏移量以及与主节点的链接状态并更新。
  • 如果INFO命令目标是主节点:哨兵从返回信息中获取主节点的从机列表,如果从节点是新增的,则将其加入监控列表。
  • 无论目标是主节点还是从节点,都会记录其runId。
  • 如果节点的角色发生变化,哨兵会记录节点新的角色及上报时间。若此时哨兵运行在TILT模式下,则什么都不做。否则,会执行主从切换相关的逻辑,我们后面再细说。

INFO命令是Redis的管理命令,通过它我们可以了解服务运行的信息。关于INFO命令的作用及返回值信息,建议大家实操看下,可参考官方文档了解每个信息的含义。

再来看哨兵节点

为了相互检查可用性及信息交互,哨兵之间是一直保持连接的,但是我们并没有显示的告知它们彼此的存在,它们之间是怎么发现对方并交互的呢?

是这样的:通过刚才的介绍,我们了解到哨兵通过INFO命令发现了主节点及从节点的地址信息,而Redis提供了一种发布订阅的消息通信模式,即Pub/Sub,哨兵们就是通过一个约定好的通道(channel)发布/订阅hello信息进行通信。结合图示说明一下: image.png 如上图所示:

  • 每隔2秒,每个哨兵会通过它所监控的主节点、从节点向__sentinel__:hello通道发布一条hello消息。
  • 每个哨兵会通过它所监控的主节点、从节点订阅__sentinel__:hello通道的消息,以此接收其他哨兵发布的信息。

我们可以通过Redis客户端直接订阅该channel:连接到其中一个节点,然后输入命令:SUBSCRIBE __sentinel__:hello,如下图所示: image.png 如上图所示,hello信息中包含8个信息,依次是:哨兵ip、哨兵端口、哨兵runId、当前纪元、主节点名称、主节点ip、主节点端口、主节点纪元,除了纪元应该都可以理解。这样每个哨兵发布的消息都会被其他哨兵接收到,从而达到信息交换的目的。

  • 每个哨兵都会维护其监控的主节点信息,如果它接收到其他哨兵消息后,发现自己维护的信息已经过时,则立即执行更行过程。
  • 如果哨兵接受到的信息没有在已有的监控列表中,就意味着发现了一个新的哨兵实例,此时会创建一个新的哨兵实例加入监控列表。在处理新增哨兵实例时,如果它与已存在的哨兵实例runId或者ip、port一致,将只保存最新的实例信息。

关于其他哨兵节点及从节点的发现过程就介绍到这里了,整体还是比较容易理解的。简单总结一下:对于数据节点采用INFO命令询问,从一个主节点得到从节点,再通过从节点校验主节点,实现节点发现;对于其他哨兵节点,借助正在被监控的数据节点以类似广播的方式,实现节点的发现。

故障检测

故障检测是哨兵执行故障转移的前提,在知晓需要监控的目标(主从节点)后,哨兵通过PING命令实现对主从节点的故障检测。

哨兵以集群方式工作,官方建议至少要有三个节点,每个节点都以相同的方式对主从节点进行监控与故障检测。由于网络抖动或者网络分区,单个哨兵对节点的故障检测可能无法代表其真实的状态,为了降低误判,哨兵之间还需要对节点的故障状态进行协商。所以这里需要引入两个概念:

  • 主观宕机(Subjective Down, SDOWN):是指一个哨兵实例通过检测发现某个主节点发生故障的一种状态。
  • 客观宕机(Objective Down, ODOWN):是指哨兵检测到某个主节点发生故障,通过命令SENTINEL is-master-down-by-addr与其他哨兵节点协商,并且在指定时间内接收到指定数量的其他哨兵的确认反馈时的一种状态。

简单来说,SDOWN是哨兵自己认为节点宕机,而ODOWN是不但哨兵自己认为节点宕机,而且该哨兵与其他节点沟通后,达到一定数量的哨兵都认为节点宕机了。

这里的“一定数量”是一个法定数量(Quorum),是由哨兵监控配置决定的,解释一下该配置:

# sentinel monitor <master-name> <master-host> <master-port> <quorum>
# 举例如下:
sentinel monitor mymaster 127.0.0.1 6379 2
复制代码

这条配置项用于告知哨兵需要监听的主节点:

  • sentinel monitor:代表监控。
  • mymaster:代表主节点的名称,可以自定义。
  • 192.168.11.128:代表监控的主节点ip,6379代表端口。
  • 2:法定数量,代表只有两个或两个以上的哨兵认为主节点不可用的时候,才会把主节点设置为ODOWN状态,然后进行failover操作。

例子中quorum=2,也就是说:哨兵检测到主节点故障并设置其状态为SDOWN,然后至少得到一个其他哨兵的认可,即标记该主节点为ODWN状态。

还有一个概念:config-epoch,配置纪元,它是维护集群内主从关系信息版本的配置。每次执行故障转移都会加1,用于表明一个新的集群主从关系版本。数值越大,版本越新。它由哨兵节点维护,并在哨兵节点之间相互传播。

认识以上几个概念后,我们看下Redis是如何实现的。

主观宕机

Redis以类似心跳检测的PING命令对节点进行健康检查,然后根据节点的回复情况进行状态管理。

哨兵以字段act_ping_time维护对节点执行PING命令的时间,并把它作为超时未回复的依据,通过下面的过程了解它的变化: image.png

  • 默认情况下,每隔1秒哨兵向节点发送一次PING命令;发送成功后,设置last_ping_time为当前时间,按如下规则修改act_ping_time
    • act_ping_time为0,则设置为当前时间。
    • act_ping_time不是0,则不做任何修改。
  • 若收到节点回复:修改last_pong_time为当前时间,并检查是否为有效回复,哨兵仅认为+PONG、-LOADING-MASTERDOWN`是有效的,其他回复或未回复都是无效的。
    • 若为有效回复,则修改last_avail_time为当前时间,修改act_ping_time为0;
    • 若为无效回复,不做任何修改
  • 每隔100毫秒,哨兵逐个检查节点是否达到SDOWN状态,具体方法在sentinelCheckSubjectivelyDown,当满足以下条件(两者满足其一)时,哨兵会把节点状态置为SDOWN:
    • 在预设时间范围内哨兵未收到节点对PING命令的有效回复。这个预设时间是由配置项down-after-milliseconds决定的,默认值是30秒。
    • 哨兵认为它是主节点,而节点上报它正在切换为从节点,但是在指定时间范围内它没有完成角色切换。这个时间计算公式为:down-after-milliseconds + 2 * SENTINEL_INFO_PERIOD,默认值是50秒(30 + 2 * 10)。

SDOWN状态是指在down-after-milliseconds未收到节点的PING命令回复,如果该配置项为30秒,但是哨兵在29秒时收到节点的回复,哨兵也会认为节点是正常工作的。

SDOWN无法触发故障转移,仅仅说明是一个哨兵认为节点发生故障(不可用)了,若要触发故障转移,必须达到ODOWN状态。

客观宕机

当Sentinel将一个主节点判断为主观下线之后,为了确认这个主服务器是否真的下线 了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。

发送Sentinel is-master-down-by-addr命令。

Sentinel使用Sentinel is-master-down-by-addr命令询问其他Sentinel节点是否已经下线。命令及参数说明如下:

Sentinel is-master-down-by-addr <ip> <port> <epoch> <runId>

# 既可用于询问其他哨兵主节点是否下线,也可以用于后续故障转移的投票,这里先说第一种情况:
# ip:被Sentinel判断为主观下线主节点的ip;
# port:被Sentinel判断为主观下线主节点的port;
# epoch:Sentinel的配置纪元;
# runId:询问场景中它始终为`*`。
复制代码

如果被Sentinel判断为主观下线的主节点IP为127.0.0.1,端口号为6379,Sentinel的配置纪元为0,那么Sentinel将向其他Sentinel发送以下命令:

SENTINEL is-master-down-by-addr 127.0.0.1 6379 0 *
复制代码

接收Sentinel is-master-down-by-addr命令。

当Sentinel接收到其他Sentinel发过来的Sentinel is-master-down-by-addr命令后,会解析命令中的主节点的ip和port,检查本地缓存中主节点是否为SDOWN,然后以下三个参数进行回复:

# 返回目标Sentinel对主节点的检查结果,1说明为SDOWN,0说明正常。
1) <down_state>
# 在询问主节点是否下线的场景下,始终为 *
2) <leader_runid>
# 在询问主节点是否下线的场景下,始终为 0
3) <leader_epoch>
复制代码

如果接收Sentinel is-master-down-by-addr命令的Sentinel也认为主节点已经为SDOWN,则回复命令如下:

1) 1
2) *
3) 0
复制代码

处理Sentinel is-master-down-by-addr回复。

当Sentinel接收到其他Sentinel对Sentinel is-master-down-by-addr的回复后,会解析其返回的内容,然后设置该Sentinel对询问主节点的在线状态。

这样,Sentinel就完成了通过其他Sentinel对主节点在线状态的询问过程。

在Sentinel的周期性函数中,会检查主节点是否满足客观宕机的条件。判断的过程比较简单:

  • 从字典中取出监控当前主节点的所有Sentinel节点,然后遍历这些Sentinel的主节点状态是否为SDOWN,是就累加。
  • 遍历完成后,检查累加结果是否大于等于quorum(法定数量)。成立则修改主节点状态为客观宕机(ODOWN),并设置宕机时间;不成立则设置为非客观宕机。

ODOWN状态仅应用在主节点上,不对从节点及其他哨兵节点应用,但是SDOWN状态对他们都是有效的。

故障转移

Sentinel判定主节点客观宕机(ODOWN)后,将进入故障转移过程。

进入故障转移过程有几个前提:主节点为客观宕机状态、当前没有故障转移在执行、上次故障转移已经超时。Sentinel确认可以执行故障转移后,会进行以下几项准备工作:

  • 设置failover_state:SENTINEL_FAILOVER_STATE_WAIT_START(故障转移等待开始);
  • 设置当前主节点标识位:SRI_FAILOVER_IN_PROGRESS(主节点处于故障转移过程中);
  • 配置纪元加1,并以此作为故障转移的纪元;
  • 记录故障转移开始时间及failover_state状态修改时间;

整个故障转移过程是依靠Sentinel周期性函数及failover_state状态机来驱动的(具体函数是sentinelFailoverStateMachine),通过图示来说明: image.png

如图所示,一个状态对应一个操作步骤。每次sentinelFailoverStateMachine执行时首先判断failover_state,然后选择对应的过程执行,接下来按照上图把核心过程介绍一下。

Sentinel Leader选举

当一个主节点被判断为客观下线时,监控这个主节点的所有Sentinel会进行协商,选举一个Leader对下线的主节点执行故障转移操作。怎么选呢?

思考一下,我们可以知道:故障检测是多个Sentinel同时执行的,也就是说可能多个Sentinel在相近的时间内都判定主节点客观宕机了,因此Leader的选举过程在Sentinel集群内可能是同步进行的。所以,Sentinel需要在集群内进行“拉票”,“拉票”的依据就是配置纪元及“拉票”的时间。配置纪元越大,优先级越高;“拉票”请求越早,优先级越高。我们来看下:

  • 当Sentinel判断主节点客观下线后,会把自己的配置纪元加1,未检测到主节点ODOWN或检测慢的,自然落后于当前纪元;
  • Sentinel会使用Sentinel is-master-down-by-addr命令向其他所有Sentinel发起投票请求,与故障检测过程中的“询问“不同,这里的runId将被设置为当前Sentinel的runId,epoch为最新的纪元。
  • 其他Sentinel接收到“投票”请求后,执行以下过程:
    • 若请求纪元大于自身配置纪元,则更新替换;若监控主节点的配置纪元小于请求纪元,则更新替换,并“投票”给发起请求的Sentinel。这个过程是抢占式的,同一纪元,先到先得。(Redis命令处理是单线程,无并发冲突)。
    • 根据判断结果,回复“投票”请求:回复内容为该Sentinel选举的Leader的runId。
  • Sentinel接收并处理Sentinel is-master-down-by-addr回复:把投票结果(runId)更新到该Sentinel的节点信息中。

“投票”完成后就到了“唱票”环节,该过程是在SENTINEL_FAILOVER_STATE_WAIT_START状态下执行的。Sentinel会遍历当前主节点下所有的Sentinel节点,把它们的投票信息进行统计;然后判断是否有Sentinel胜出。这里胜出的条件是:

  • Sentinel必须获取集群内大多数Sentinel的选票,即票数大于50%(防止“脑裂“);
  • Sentinel所获票数必须大于等于法定人数(quorum);

举例:监控主节点的Sentinel有5个,quorum为4,投票情况:

  • 如果某个Sentinel的获得3票,但是3<4,该Sentinel不能被选为Leader;
  • 如果某个Sentinel的获得4票,但是4>=4,该Sentinel可以被选为Leader;

因为Sentinel Leader的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次Leader,所以在一个配置纪元立main,只会出现一个Leader。

如果在给定时限内,没有一个Sentinel被选举为Leader,那么各个Sentinel将在一段时间后再次进行选择,直到选出Leader为止。

Sentinel Leader选举完成,设置failover_state为SENTINEL_FAILOVER_STATE_SELECT_SLAVE

新主节点选举

主节点已经客观宕机,Sentinel Leader会从该主节点存活的从节点中选出一个新的主节点。

首先,Sentinel Leader会按照以下条件剔除从节点:

  • 主观宕机(SDOWN)或与处于断线状态的从节点;
  • 最近5秒内未回复过Sentinel Leader INFO命令的从节点;
  • 从节点的优先级为0的从节点,由配置项replica-priority决定;
  • 与主节点断开连接超过10倍down-after-milliseconds的从节点;

筛选过后,剩下的从节点都是数据比较新、与Sentinel Leader通信正常的,可以保证故障转移后最小的数据丢失。

然后,按照以下规则选择新的主节点:

  • 选择replica-priority最低的节点。如果存在相同,则继续;
  • 选择复制偏移量最大的的从节点。如果存在相同,则继续;
  • 选择runId最小的从节点;

如果新主节点选举失败,将等待重试。选举成功,则将此从节点提升,并设置failover_state为SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE

配置新主节点&新主节点角色提升

选出新的主节点之后,Sentinel Leader会向它发送slaveof NO ONE,把这个从节点转为主节点(这是在从节点自身来看的角色转换)。

从节点接收slaveof NO ONE命令后,会重置其主节点信息,断开与其主节点、从节点的网络连接,重置其复制ID,并执行持久化重写操作。

发送命令后,Sentinel Leader会设置failover_state为SENTINEL_FAILOVER_STATE_WAIT_PROMOTION,等待从节点角色提升。

Sentinel Leader会向它发送slaveof NO ONE命令后,每隔一秒发送一次INFO命令(正常是10秒一次),并观察命令回复中的角色信息。当被升级的从节点的角色从原来的slave变为master时,Sentinel Leader就直到该从节点已经升级主节点了。

从节点角色提升成功,设置failover_state状态为SENTINEL_FAILOVER_STATE_RECONF_SLAVES。

配置其他从节点

新的主节点已经配置完成,接下来就是要让其他存活的从节点以该节点为主节点,然后向该节点发起主从复制。

该过程原理比较简单:遍历原主节点的从节点,向这些从节点发送slaveof <ip> <port>命令,该部分在上一篇《Redis专题:一文搞懂主从复制原理》讲过,大家可以自行翻阅。

所有从节点配置完成后,就会修改failover_state为SENTINEL_FAILOVER_STATE_UPDATE_CONFIG

不过,这一过程受配置项parallel_syncs(同时执行主从复制的节点数量)的影响。由于主从复制过程中从节点数据加载阶段无法对外提供服务,所以,如果同时进行主从复制的从节点数量较多,可能会导致短时间内系统不可用。

该配置越小,从节点完成配置的时间越长;反之,时间越短。实际环境中,我们需要根据从节点的数量,系统压力,按照比例合理设置。

更新配置

故障转移过程中,新主节点是以“储君”的身份在工作,其他所有从节点切换至新的主节点后,就要正式把新主节点“立”起来了。简单来说有三步(实现方法在sentinelFailoverSwitchToPromotedSlave,由周期函数触发):

  • 重置新主节点的信息状态、清空从节点、Sentinel节点等,failover_state修改为SENTINEL_FAILOVER_STATE_NONE
  • 从旧主节点中迁移Sentinel节点、从节点,迁移至新的主节点中。
  • 释放就主节点配置信息。

至此,故障转移工作完成。

其他问题

主从节点移除

Sentinel从不移除从节点,即使很长时间都不可用。这一点是非常有用的,因为当发生网络分区或者故障后,Sentinel需要正确的对恢复节点进行重新配置。经过故障转移,旧主节点将以从节点的角色加入集群,Sentinel会对他进行重新配置,让它从新的主节点执行主从复制。

如果需要移除故障节点,需要依次向Sentinel节点发送Sentinel Reset命令,经过10秒,Sentinel会重新刷新它们的从节点列表,仅保存主节点INFO命令回复内容中的从节点。

脑裂问题

redis的主从模式下脑裂是指因为网络问题,导致redis 主节点跟从节点和Sentinel集群处于不同的网络分区,此时因为Sentinel集群无法感知到 主节点的存在,就会将某一个从节点提升为主节点。此时就存在两个不同的主节点,就像一个大脑分裂成了两个。

集群脑裂问题中,如果客户端还在基于原来的主节点继续写入数据,那么新的主节点将无法同步这些数据,当网络问题解决之后,Sentinel 集群将原先的主节点降为从节点,此时再从新的主中同步数据,将会造成大量的数据丢失。

如下图所示:Redis 3原来是主节点,Redis 1和2是它的从节点。由于网络分区,Sentinel 1和2提升Redis 1作为新的主节点,而Redis 3在自己所处的网络分区中仍然是主节点。Client B还会持续写入数据,但是当网络恢复后,Redis 3将被重新配置为Redis 1的从节点,Client B写入的数据将会全部丢失。

            +-------------+
            | Sentinel 1  |----- Client A
            | Redis 1 (M) |
            +-------------+
                    |
                    |
+-------------+     |          +------------+
| Sentinel 2  |-----+-- // ----| Sentinel 3 |----- Client B
| Redis 2 (S) |                | Redis 3 (M)|
+-------------+                +------------+
复制代码

由于Redis使用异步复制,在这个场景中无法完全解决数据丢失的问题,但是可以通过以下参数把数据损失降低:

min-replicas-to-write 1
min-replicas-max-lag 10
复制代码

min-replica选项的作用在上一篇《Redis专题:一文搞懂主从复制原理》我们也说过了,这一配置要求主节点必须包含一个从节点并且主从之间的最大延迟不能超过10秒,否则主节点会拒绝客户端的写入请求。

但是从故障发生到min-replica选项生效也需要一定的时间,这个过程中的数据丢失是无法避免的,依赖“Redis+Sentinel”的系统必须对数据的丢失有一定的容忍性,否则就需要采用支持强一致的系统了。

总结

本文详细介绍了Redis Sentinel模式的工作原理,从节点发现、故障检测、故障转移等几个方面深入展开。对我个人来讲,Redis主从模式的运行原理有了更加深入的认识。

希望也能给你带来帮助!码字不易,感谢转发!

参考文档

文章分类
后端
文章标签