暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Redis专题:深入Redis Cluster集群容错机制(3/3)

码路印记 2021-04-05
791

哨兵模式的自动故障转移能力为其提供高可用保障,同样的,为了提供集群的可用性,Redis Cluster提供了自动故障检测及故障转移能力。两者在设计思想上有很大的相似之处,本节将围绕这个话题进行剖析。

心跳机制

Redis Cluster作为无中心的分布式系统,集群容错机制依靠各个节点共同协作,在节点检测到某个节点故障时,通过传播节点故障并达成共识,然后触发一系列的从节点选举及故障转移工作。这一工作完成的基础是节点之间通过心跳机制对集群状态的维护。

下图是从节点A视角来看集群的状态示意图(仅绘制与集群容错有关的内容),myself指向A节点本身,它是节点A对自身状态的描述;nodes[B]指向节点B,它是从A节点来看B节点的状态;还有集群当前纪元、哈希槽与节点映射关系等。在集群中,每两个节点之间通过PING
PONG
两种类型的消息保持心跳,由前文可知这两种消息采用完全相同的结构(消息头和消息体都相同),仅消息头中的type
字段不同,我们称这个消息对为心跳消息。

  • PING
    /PONG
    消息头包含了消息源节点的配置纪元(configEpoch)、复制偏移量(offset)、节点名称(sender)、负责的哈希槽(myslots)、节点状态(flags)等,这些内容与目标节点所维护的nodes中节点信息一一对应;另外还包含在源节点看来集群纪元(currentEpoch)、集群状态(state)。
  • PING
    /PONG
    消息体包含若干clusterMsgDataGossip
    ,每个clusterMsgDataGossip
    对应一个集群节点状态,它描述了源节点与之的心跳状态及源节点对它运行状态的判断。

心跳消息在集群节点两两之间以“我知道的给你,你知道的给我”这样“瘟疫传播”的方式传播、交换信息,可以保证在短时间内节点状态达成一致。我们从心跳触发的时机、消息体的构成、应用几个方面深入理解心跳机制。

触发时机

在集群模式下,心跳动作是由周期性函数clusterCron()
触发的,该函数每个100毫秒执行一次。为了控制集群内消息的规模,同时兼顾与节点之间心跳的时效性,Redis Cluster采取了不同的处理策略。

正常情况下,clusterCron()
每隔一秒(该函数每执行10次)向一个节点发送PING
消息。这个节点的选择是随机的,随机方式为:

  • 随机5次,每次从节点列表nodes中随机选择一个节点,如果节点满足条件(集群总线链接存在、非等待PONG
    回复、非握手状态、非本节点),则作为备选节点;
  • 如果备选节点不为空,则从备选节点中选择PONG
    回复最早的节点;

补充说明Redis Cluster心跳消息发送与接收的检查依据,这对后续故障检测也是非常重要的:

  • 当源节点向目标节点发送PING
    命令后,将设置目标节点的ping_sent
    为当前时间。
  • 当源节点接收到目标节点的PONG
    回复后,将设置目标节点的ping_sent
    为0,同时更新pong_received
    为当前时间。

也就是说, ping_sent
 为0,说明已收到 PONG
 回复并等待下次发送; ping_sent
 不为0,说明正在等待 PONG
 回复。

我们在集群配置文件中设置了超时参数cluster-node-timeout
,对应变量NODE_TIMEOUT
,节点将以此参数作为目标节点心跳超时的依据。为了确保PING-PONG消息不超时并保留重试余地,Redis Cluster将以NODE_TIMEOUT/2
为界限进行心跳补偿。

clusterCron()
每次执行时(100毫秒)会依次检查每个节点:

  • 如果在收到目标节点PONG
    消息NODE_TIMEOUT/2
    还未发送PING
    ,源节点会立刻向目标节点发送一次PING
  • 如果已经向目标节点发送PING
    消息,但是在NODE_TIMEOUT/2
    内未收到目标节点的PONG回复,源节点会尝试断开网络链接,通过重连排除网络链接故障对心跳的影响。

总体来讲,集群内每秒的心跳消息收发数量是稳定的,即使集群有很多节点也不会导致瞬时网络I/O过大,给集群带来负担。集群中每两个节点之间都在保持心跳,按照N个节点的有向完全图,整个集群会有N*(N-1)
个链接,每个链接都需要保持心跳,心跳消息成对出现。

假如集群有100个节点,NODE_TIMEOUT
为60秒,那就意味着每个节点在30秒内要发送99条PING消息,平均每秒发送3.3条。100个节点每秒发送总计330条消息,这个数量级的网络流量压力还是可以接受的。

不过,我们需要注意节点数确定的情况下,需要合理设置NODE_TIMEOUT
参数。如果过小,会导致心跳消息对网络带来较大压力;如果太大,可能会影响及时发现节点故障。

消息构成

PING
/PONG
消息采用一致的数据结构。其中,消息头的内容来自集群状态的myself
,这点很容易理解;而消息体需要追加若干节点的状态,但是集群中有很多节点,到底应该添加哪几个节点呢?

按照Redis Cluster的设计,每个消息体将会包含正常节点和PFAIL
状态节点,具体获取方式如下(该部分源码位于cluster.c
函数clusterSendPing
中):

  • 确定心跳消息需要包含正常节点的理论最大值(之所以是理论值,是因为接下来的流程还需要考虑节点状态,会移除在握手中或宕机的节点):
    • 条件1:最大数量=集群节点数 - 2,“减2”是指去掉源节点与目标节点;
    • 条件2:最大数量为节点数量的10分之一且不小于3;
    • 以上结果两者取最小值,得到所需的理论数量wanted
  • 确定心跳信息需要包含PFAIL
    状态节点的数量pfail_wanted
    :集群状态中获取所有PFAIL
    节点的数量(server.cluster->stats_pfail_nodes
    )。
  • 添加正常节点:从集群节点列表随机获取wanted
    个节点,创建gossip消息片段,加入消息体。其中节点需要满足以下条件:
    • 不是源节点自身;
    • 不是PFAIL
      状态,因为后面单独添加PFAIL
      节点;
    • 节点不是握手状态及无地址状态,或者节点集群总线链接存在且负责的哈希槽数量不为0;
  • 添加PFAIL
    状态节点:遍历获取PFAIL
    状态节点,创建gossip片段,加入消息体,此时节点需要满足处于PFAIL
    状态、不是握手状态、不是无地址状态。

消息应用

节点接收到PING或PONG消息后,将按照消息头及消息体中的内容对本地维护的节点状态进行更新。顺着源码说明的话,其中涉及的字段和逻辑还是比较复杂的,我将从应用场景角度来说明消息的处理过程(结合源码函数clusterProcessPacket
)。

集群纪元和配置纪元

心跳消息头包含了源节点的配置纪元(configEpoch)及在他看来的集群当前纪元(currentEpoch),目标节点接收后将检查自身维护的源节点的配置纪元和集群当前纪元。具体方式为:

  • 若目标节点缓存的源节点配置纪元(缓存配置纪元)小于源节点心跳消息中声明的配置纪元(声明配置纪元),则修改缓存配置纪元为声明配置纪元;
  • 若目标节点缓存的集群当前纪元(缓存当前纪元)小于源节点心跳消息中声明的集群当前纪元(声明当前纪元),则修改缓存当前纪元为声明当前纪元;

哈希槽变更检测

消息头包含了源节点当前负责的哈希槽列表,目标节点会检查本地缓存的哈希槽与节点的映射关系,看是否存在与映射关系不一致的哈希槽。当发现不一致的映射关系时,将按照以下情况进行处理:

  • 如果源节点为主节点,并且其声明的配置纪元大于目标节点缓存的配置纪元,则按照声明的哈希槽修改本地哈希槽与节点的映射关系;
  • 如果本地缓存的节点配置纪元大于源节点声明的配置纪元,则通过UPDATE
    消息告知源节点更新本地的哈希槽配置;
  • 如果本地缓存的节点配置纪元与源节点声明的配置纪元相同,并且源节点和目标节点都是主节点,则处理配置纪元冲突:目标节点升级本地集群当前纪元和自身的配置纪元(与集群当前纪元保持一致)。这样在后续心跳中,将会再次调整哈希槽冲突,最终达到一致。

新节点发现

在节点握手过程中,我们知道,新节点加入集群仅需与集群中任意一个节点通过握手加入集群,但是其他节点并不知道有新节点加入,新节点也不知道其他节点的存在;对于两者而言,都是新节点的发现过程。

在心跳过程中,源节点会把对方未知的新节点信息加入消息体,通知目标节点。目标节点将执行以下流程:

  • 目标节点发现消息体中存在本地缓存不存在的节点,将会为其创建clusterNode
    对象,并加入集群节点列表nodes。
  • 目标节点在clusterCron
    函数中创建与其的网络链接,两者通过两次心跳交互完成新节点的发现。

节点故障发现

节点故障发现是心跳的核心功能,该部分在下一节单独介绍。

故障发现与转移

PFAIL与FAIL概念

Redis Cluster使用两个概念PFAIL
FAIL
来描述节点的运行状态,这与哨兵模式中的SDOWN
ODOWN
类似。

  • PFAIL:可能宕机

当一个节点在超过 NODE_TIMEOUT
时间后仍无法访问某个节点,那么它会用 PFAIL
来标识这个不可达的节点。无论节点类型是什么,主节点和从节点都能标识其他的节点为 PFAIL

Redis集群节点的不可达性是指:源节点向目标节点发送PING命令后,超过 NODE_TIMEOUT
时间仍未得到它的PONG
回复,那么就认为目标节点具有不可达性。

这是由心跳引出的一个概念。为了让PFAIL
尽可能,NODE_TIMEOUT
必须比两节点间的网络往返时间大;为了确保可靠性,当在经过一半 NODE_TIMEOUT
时间还没收到目标节点对于 PONG
命令的回复时,源节点就会马上尝试重连接该目标节点。

所以,PFAIL
是从源节点对目标节点心跳检测的结果,具有一定的主观性。

  • FAIL:宕机

PFAIL
状态具有一定的主观性,此时不代表目标节点真正的宕机,只有达到FAIL
状态,才意味着节点真正宕机。

不过我们已经知道,在心跳过程中,每个节点都会把检测到PFAIL的节点告知其他节点。所以,如果某个节点宕机是客观存在的,那其他节点也必然会检测到PFAIL状态。

在一定的时间窗口内,当集群中一定数量的节点都认为目标节点为PFAIL
状态时,节点就会将该节点的状态提升为FAIL
(宕机)状态。

节点故障检测

本节将详细说明节点故障检测的实现原理,还是以下图为例(A、B、C节点为主节点,以B节点宕机为例),重点关注集群状态节点列表(nodes)的ping_sent
pong_received
fail_reports
几个字段。节点如何达到PFAIL状态?

每个节点维护的集群状态中包含节点列表,节点信息如上图节点B所示,其中字段ping_sent
代表了B节点对A节点的心跳状态:如果值为0,说明A节点与B节点心跳正常;如果值不是0,说明A节点已经向B节点发送了PING
,正在等待B节点回复PONG

集群节点每隔100毫秒执行一次clusterCron()
函数,其中会检查与每个节点的心跳及数据交互状态,若A节点在NODE_TIMEOUT
时间内未收到B节点的任何数据,则视为B节点发生故障,A节点设置节点状态为PFAIL
。具体代码如下所示:

void clusterCron(void) {
    /* 省略…… */
    
    // ping消息已经发送的时间
    mstime_t ping_delay = now - node->ping_sent;
    // 已经多久没有收到节点的数据了
    mstime_t data_delay = now - node->data_received;
    
    // 两者取较早的
    mstime_t node_delay = (ping_delay < data_delay) ? ping_delay : data_delay;

    // 判断超时
    if (node_delay > server.cluster_node_timeout) {
        /* 节点超时,如果当前节点不是PFAIL或FAIL状态,则设置为PFAIL状态 */
        if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) {
            serverLog(LL_DEBUG,"*** NODE %.40s possibly failing", node->name);
            node->flags |= CLUSTER_NODE_PFAIL;
            update_state = 1;
        }
    }
    /* 省略…… */
}

复制

PFAIL状态传播

由“心跳机制——消息构成”可知,PFAIL
状态的节点将会随着心跳传播至集群内所有可达节点,不再赘述。

PFAIL状态切换至FAIL状态

PFAIL
FAIL
的状态切换需要集群内过半数主节点的认可,集群节点通过心跳消息收集节点的PFAIL
的标志。如果B节点发生故障,A、C节点都将检测到B节点故障并标记B节点为PFAIL
;那么A、C节点之间的心跳消息都会包含B节点已经PFAIL
的状态。以A节点来看,Redis Cluster是如何处理。

由于其他节点的状态在心跳消息的消息体内,消息接收方通过clusterProcessGossipSection
函数进行处理,C节点是主节点,并且声明B节点为PFAIL
状态。从源码可知,将执行以下流程:

  • 为B节点添加故障报告节点,即把C节点添加到B节点的fail_reports
    内。

fail_reports
clusterNodeFailReport
列表,保存了所有认为B节点故障的节点列表。结构如下所示,其中time
字段代表其被加入的时间,即声明该节点故障的最新时间,当再次报告该节点状态时,仅刷新time
字段。

typedef struct clusterNodeFailReport {
    /* 报告节点故障的节点 */
    struct clusterNode *node;  /* Node reporting the failure condition. */
    /* 故障报告的时间 */
    mstime_t time;             /* Time of the last report from this node. */
} clusterNodeFailReport;

复制
  • 检查是否达到FAIL
    的条件:Redis Cluster规定,若超过半数的主节点认为某个节点为PFAIL
    状态,则设置节点状态为FAIL
    状态。接着上面的例子,具体过程为:
    • 计算达到FAIL
      的法定节点数:此时集群中包含3个主节点,则至少需要2个节点认可;
    • 在A节点集群状态中,C节点已经被加入B节点的fail_reports
      列表,并且A节点已经标记B节点故障。即2两个节点确认B节点发生故障,所以可以设置B节点为FAIL
      状态。
    • A节点取消B节点的PFAIL
      状态,设置其FAIL
      状态,然后向所有可达节点发送关于B节点的FAIL
      消息。

详细的代码过程如下函数所示:

void markNodeAsFailingIfNeeded(clusterNode *node) {
    int failures;
    // 计算判定节点宕机的法定数量
    int needed_quorum = (server.cluster->size / 2) + 1;
    // 判断当前节点是否认为该节点已经超时
    if (!nodeTimedOut(node)) return/* We can reach it. */
    if (nodeFailed(node)) return/* Already FAILing. */

    failures = clusterNodeFailureReportsCount(node);
    /* 当前节点也认可该节点宕机 */
    if (nodeIsMaster(myself)) failures++;
    if (failures < needed_quorum) return/* No weak agreement from masters. */

    serverLog(LL_NOTICE, "Marking node %.40s as failing (quorum reached).", node->name);

    /* 设置节点为FAIL状态 */
    node->flags &= ~CLUSTER_NODE_PFAIL;
    node->flags |= CLUSTER_NODE_FAIL;
    node->fail_time = mstime();

    /* 向所有可达节点广播节点的FAIL状态,所有节点接收后将被强制接收认可 */
    clusterSendFail(node->name);
    clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
}

复制

需要注意的是:fail_reports
中的记录是有有效期的,默认是2倍的NODE_TIMEOUT
,超过该时间限制记录会被移除。也就是说,必须在一定的时间窗口内收集足够的记录才能完成PFAIL
FAIL
的状态转移。如果某个主节点对该节点的心跳恢复正常,会立刻从fail_reports
移除。

节点A把节点B设置为FAIL
状态后,将向所有可达节点发送关于B节点FAIL
的消息,对应的消息类型为CLUSTERMSG_TYPE_FAIL
。其他节点一旦收到FAIL消息,将立即设置节点B为FAIL
状态,无论在他们看来节点B是否处于PFAIL
状态。

主节点故障后,关于它的FAIL
消息被传播至集群内的所有可达节点,这些节点标记其为FAIL
状态。为了保证集群的可用性,该主节点的从节点们将启动故障转移动作,选择最优的从节点提升为主节点,Redis Cluster的故障转移包含两个关键过程:从节点选举和从节点提升。

从节点选举

若主节点故障,该主节点的所有从节点都会启动一个选举流程,在其他主节点的投票表决下,只有投票胜出的从节点才有机会提升为主节点。从节点选举的准备与执行过程是在clusterCron中进行的。

发起选举的条件与时机

从节点发起选举流程必须满足以下条件(选举流程发起前的检查工作位于函数clusterHandleSlaveFailover(void)
):

  • 从节点的主节点处于FAIL
    状态;
  • 从节点主节点负责的哈希槽不为空;
  • 为了保证从节点数据的时效性,从节点与主节点之间断联的时间必须小于指定时间。关于这个指定的时间,我从代码中提取了出来,如下所示:
/* 数据有效时间 */
mstime_t data_age;

/* 取从节点与主节点断开的时间间隔 */
if (server.repl_state == REPL_STATE_CONNECTED) {
    data_age = (mstime_t)(server.unixtime - server.master->lastinteraction) * 1000;
else {
    data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
}

/*  */
if (data_age > server.cluster_node_timeout)
    data_age -= server.cluster_node_timeout;

data_age > 
    (((mstime_t)server.repl_ping_slave_period * 1000
     + (server.cluster_node_timeout * server.cluster_slave_validity_factor)

复制

如果FAIL
状态的主节点拥有多个从节点,Redis Cluster总是希望数据最完整的从节点被提升为新的主节点。然而,假如所有从节点同时启动选举流程,所有从节点公平竞争,无法保证数据最完整的节点优先被提升。为了提高该节点的优先级,Redis Cluster在启动选举流程时引入了延迟启动机制。结合源码,每个从节点会计算一个延迟值并据此计算该节点选举流程启动的时间,计算公式如下:

/* 选举流程启动延迟值 */
DELAY = 500 + random(0,500) + SLAVE_RANK * 1000

/* 计算得出从节点启动选举流程的时间 */
server.cluster->failover_auth_time = DELAY + mstime()

复制

解释一下这个公式:

  • 固定的延迟值500,是为了保证主节点的FAIL状态在集群内传播完成,防止发起选举时主节点拒绝;
  • 随机延迟值random(0,500):为了确保从节点不再同一时间启动选举流程;
  • SLAVE_RANK:该值取决于从节点数据的完整程度。当主节点变为FAIL
    状态后,从节点之间会通过PONG
    命令交换状态,以便建立最优的rank排序;从节点rank排序规则为:从节点repl_offset
    最大的从节点,rank = 0;其次,rank = 1,以此类推。

所以,数据完整程度最高的节点将最先启动选举流程,如果后续一切顺利,它将被提升为新的主节点。

设置failover_auth_time
后,当clusterCron()
再次运行时,如果系统时间达到这个预设值(并且failover_auth_sent=0
)就会进入选举流程。

从节点选举流程

从节点启动选举流程,先把自身维护的集群当前纪元(currentEpoch
)加1,并设置failover_auth_sent=1
以表示已经启动选举流程;然后通过FAILOVER_AUTH_REQUEST
类型的消息向集群内的所有主节点发起选举请求,并在2倍NODE_TIMEOUT
(至少2秒)时间内等待主节点的投票回复。

集群内的其他主节点是从节点选举的决策者,投票前需要做出严格的检查。为了避免多个从节点在选举中同时胜出,并且保证选举过程合法性,主节点接收到FAILOVER_AUTH_REQUEST
命令消息后,将会做以下条件校验:

  • 发起选举流程的从节点所属主节点必须处于FAIL
    状态(前面DELAY中的固定值500毫秒,就是为了保证FAIL消息在集群内传播充分);
  • 针对一个给定的currentEpoch
    ,主节点只会投票一次,并且保存在lastVoteEpoch中,其他较老的纪元选取申请都会拒绝。另外,如果从节点投票请求中的currentEpoch小于当前主节点的currentEpoch
    ,投票请求会被拒绝。
  • 主节点一旦通过FAILOVER_AUTH_ACK
    类型的消息投赞成票给指定的从节点,该主节点在2倍NODE_TIMEOUT
    时间内将不再投票给同主节点下的其他从节点。

主节点投票完成将记录信息,并安全持久化保存到配置文件:

  • 保存上次投票的集群当前纪元:lastVoteEpoch
  • 保存投票时间,存储在集群节点列表的voted_time
    中。

为了避免把上一轮投票计入本轮投票,从节点会检查FAILOVER_AUTH_ACK
消息所声明的currentEpoch
,若该值小于从节点的集群当前纪元,该选票会被丢弃。确认投票有效,从节点将通过cluster->failover_auth_count
进行计数。

在得到大多数主节点的投票认可后,从节点将从选举中胜出。如果在2倍NODE_TIMEOUT
(至少2秒)时间内未得到大多数节点的投票认可,选举流程将会终止,并且在4倍NODE_TIMEOUT
(至少4秒)时间后启动新的选举流程。

从节点提升

从节点获得有效选票后,将把投票计数器failover_auth_count
加1,并通过从节点选举与提升处理函数clusterHandleSlaveFailover
进行周期性检查,如果从节点得到大多数(法定数量)主节点的认可,将触发从节点提升流程。

这里选举通过法定数量与触发FAIL
状态的法定数量一致,即(server.cluster->size / 2) + 1
,半数以上的主节点。

从节点启动提升流程,将会对自身的状态信息进行一系列的修改,最终把自己提升为主节点,具体内容如下:

  • 从节点把节点配置纪元configEpoch
    加1;
  • 切换自己的角色为主节点,并且重置主从复制关系(这个与哨兵模式从节点提升类似);
  • 复制原主节点所负责的哈希槽,改为自己负责;
  • 保存并持久化以上配置变更信息;

从节点把自己设置为主节点以后,就会通过PONG
命令向集群所有节点广播自己状态的变化,以便其他节点及时修改状态,接受该从节点的角色提升。

从节点在选举中获胜后,自身的角色提升过程是比较简单的,更为关键的是被集群内其他所有节点认可。结合其他节点在集群中角色,需要考虑三种情况:

  • 其他节点:在从节点提升前,集群内与当前节点无主从关系、非同一主节点从节点的其他节点。
  • 兄弟从节点:在从节点提升前,同一主节点的从节点。
  • 旧主节点:在故障恢复后如何重新加入集群。

通用处理逻辑

新晋主节点被提升后,向集群内所有可达节点发送了PONG消息。其他节点收到该PONG
消息,除了进行通用的处理逻辑(如提升配置纪元等)外,会检测到该节点的角色变化(从节点提升为主节点),从而进行本地集群状态cluterState
更新。具体的更新内容为:

  • 更新配置纪元和集群当前纪元,因为从节点提升时升级了;
  • 把原主节点的从节点列表中移除该从节点;
  • 更新节点角色:设置从节点为主节点,取消从节点标志;
  • 哈希槽冲突处理:新晋主节点接管了旧主节点的全部哈希槽,把原主节点负责的哈希槽变更为新的主节点;

兄弟从节点切换主从复制

以上过程是“其他节点”、“兄弟从节点”通用的处理过程,“旧主节点”暂时失联,无法被通知到。基于此PONG
消息,“其他节点”已经认可新晋主节点的角色变更信息。但是“兄弟节点”仍然是把旧的主节点作为自己的主节点,按照故障迁移的思想,它应该以新晋主节点作为自己的主从复制对象,怎么实现呢?

在哈希槽冲突处理过程中,“兄弟从节点”会发现,冲突的哈希槽是原来它的主节点负责的,“兄弟从节点”检测到这一变化,就会把新晋主节点作为自己的主节点,并以它为新的主节点进行主从复制。

旧主节点重新加入

旧主节点恢复后,将以宕机前的配置信息(集群当前纪元、配置纪元、哈希槽等等)与其他节点保持心跳。

当集群内任一节点收到它的PING
消息后,会发现它的配置信息已经过时(节点配置纪元),并且哈希槽的分配情况存在冲突,此时节点将通过UPDATE
消息通知它更新配置。

UPDATE消息包含了冲突哈希槽的负责权节点信息,旧主节点接收后会发现自身的节点配置纪元已经过时,从而把UPDATE消息的节点作为自己的主节点,并切换自己的身份为从节点,然后更新本地的哈希槽映射关系。

在后续的心跳中,其他节点将把旧主节点作为新晋主节点的从节点进行更新。

至此,故障转移完成。

容错有关的其他话题

从节点迁移

为了提供集群系统的可用性,Redis Cluster实现了从节点迁移机制:集群建立时,每个主节点都有若干从节点,如果在运行过程中因为几次独立的节点故障事件,导致某个主节点没有正常状态的从节点(被孤立),那么该主节点一旦宕机,集群将无法工作。Redis Cluster会及时发现被孤立的主节点和从节点数量最大的主节点,然后挑选合适的从节点迁移至被孤立的主节点,使得其能够再抵御一次宕机事件,从而提高整个系统的可用性。

以下图为例进行说明:初始状态时,集群有7个节点,其中A、B、C为主节点,A有两个从节点A1、A2,B、C各有一个从节点,分别时B1、C1。通过以下过程,阐述节点故障时,从节点迁移的作用:

  • 集群在运行过程中,由于B节点发生故障,B1通过选举被提升为新的主节点,导致B1成为无从节点的孤立状态,此时如果B1再发生故障,集群将不可用。
  • 但是此时A有两个从节点,Redis Cluster将启动从节点迁移机制,把A1转移至B1的从节点,使得B1不再被孤立。
  • 此时即使B1再发生故障,那么A1可以提升为新的主节点,集群可以继续工作。

集群脑裂

作为分布式系统,必须解决网络分区带来的各种复杂问题。在Redis Cluster中,由于网络分区问题,导致集群节点分布在两个分区,使得集群发生“脑裂”。此时从节点的选举与提升在两个网络分区是如何工作的呢?如上图所示,节点A及其从节点A1,由于网络分区与其他节点失联。我们来看下两个分区内的节点是如何工作的?

  • 多数节点网络分区

该分区内的节点将检测到节点A的PFAIL
状态,然后经过传播确认节点A达到FAIL
状态;A2节点将触发选举流程并胜出,提升为新的主节点,继续工作。经过故障转移,含有大部分节点的网络分区可以继续工作。

  • 少数节点分区

位于少数节点分区的节点A、A1,会检测到其他节点B、C的PFAIL
状态,但是由于无法得到大多数主节点的确认,B、C无法达到FAIL
状态,进而导致不能发生后续的故障转移工作。

Redis Cluster总结

本文从三个主要部分介绍了Redis Cluster的工作原理:集群结构、数据分片、容错机制,差不多覆盖了Redis Cluster的所有内容,希望能够给大家学习Redis Cluster带来帮助。

在研究官方文档、系统源码的过程中,确实遇到了好多不解的内容,通过反复梳理代码流程,逐个揭开各个谜底,最终建立起了整个知识体系。

终于写到这里了,这篇文章从起笔到完成历时一个月,每天写一点,还好完成了。

参考资料

  • 《Redis Cluster Specification》:https://redis.io/topics/cluster-spec
  • 《Redis cluster tutorial》:https://redis.io/topics/cluster-tutorial
  • 《Redis 源码 6.0.10》

推荐阅读



文章转载自码路印记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论