今天看啥  ›  专栏  ›  linuxzw

10.6、故障转移

linuxzw  · 简书  ·  · 2019-05-13 23:08

故障转移

Redis集群自身实现了高可用。高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。本节介绍故障转移的细节,分析故障发现和替换故障节点的过程。

  1. 故障发现

    当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点状态等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线(pfail)和客观下线(fail)。

    • 主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。

    • 客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。

    1. 主观下线

      集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接受节点标记为主观下线(pfail)状态。

      流程说明:

      1)节点a发送ping消息给节点b,如果通信正常接收到pong消息,节点a更新最近一次与节点b的通信时间。

      2)如果节点a与节点b通信出现问题则断开连接,下次会进行重连。如果一致通信失败,则节点a记录的节点b最后通信时间将无法更新。

      3)节点a内的定时任务检测到与节点b最后通信时间超高cluster-node-timeout时,更新本地对节点b的状态为主观下线(pfail)。

      主观下线简单来讲就是,当cluster-note-timeout时间内某节点无法与另一个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态。每个节点内的clusterState结构都需要保存其他节点信息,用于自身视角判断其他节点的状态。结构关键属性如下:

      typedef struct clusterState {
          clusterNode *myself; /* 自身节点 */
          dict *nodes; /* 当前集群内所有节点的字典集合,key为节点ID,value为对应节点ClusterNode结构 */
          ...
      } clusterState;
      

      字典nodes属性中的clusterNode结构保存了节点的状态,关键属性如下:

      typedef struct clusterNode {
          int flags; /* 当前节点状态,如:主从角色,是否下线等 */
          mstime_t ping_sent; /* 最后一次与该节点发送ping消息的时间 */
          mstime_t pong_received; /* 最后一次接收到该节点pong消息的时间 */
          ...
      } clusterNode;
      

      其中最重要的属性是flags,用于标示该节点对应状态,取值范围如下:

      CLUSTER_NODE_MASTER 1 /* 当前为主节点 */
      CLUSTER_NODE_SLAVE 2 /* 当前为从节点 */
      CLUSTER_NODE_PFAIL 4 /* 主观下线状态 */
      CLUSTER_NODE_FAIL 8 /* 客观下线状态 */
      CLUSTER_NODE_MYSELF 16 /* 表示自身状态 */
      CLUSTER_NODE_HANDSHAKE 32 /* 握手状态,未与其他节点进行消息通信 */
      CLUSTER_NODE_NOADDR 64 /* 无地址节点,用于第一次meet通信未完成或者通信失败 */
      CLUSTER_NODE_MEET 128 /* 需要接受meet消息的节点状态 */
      CLUSTER_NODE_MIGRATE_TO 256 /* 该节点被选中为新的主节点状态 */
      

      Redis集群对于节点最终是否故障判断非常严谨,只有一个节点认为主观下线并不能准确判断是否故障。

    2. 客观下线

      当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态是,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。结构如下:

      struct clusterNode { /* 认为是主观下线的clusterNode结构 */
          list *fail_reports; /* 记录了所有其他节点对该节点的下线报告 */
          ...
      };
      

      通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程。这里有两个问题:

      1)为什么必须是负责槽的主节点参与故障发现决策?因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主机诶单数据和状态信息的复制。

      2)为什么半数以上处理槽的主节点?必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务。

      假设节点a标记节点b为主管下线,一段时间后节点a通过消息把节点b的状态发送到其他节点,当节点c接受到消息并解析出消息体含有节点b的pfail状态是,会触发客观下线流程:

      1)当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略。

      2)找到pfail对应的节点结构,更新clusterNode内部下线报告链表。

      3)根据更新后的下线报告链表尝试进行客观下线。

      这里针对维护下线报告和尝试客户端下线逻辑进行详细说明。

      (1)维护下线报告链表

      每个节点ClusterNode结构中都会存在一个下线链表结构,保存了其他主节点针对当前节点的下线报告,结构如下:

      typedef struct clusterNodeFailReport {
          struct clusterNode *node; /*  */
          mstime_t time; /*  */
      } clusterNodeFailReport;
      

      下线报告中保存了报告故障的节点结构和最近收到下线报告的时间,当接收到fail状态时,会维护对应节点的下线上报链表。每个下线报告都存在有效期,每次在尝试触发客观下线时,都会检测下线报告是否过期,对于过期的下线报告将被删除。如果在cluster-node-time * 2的时间内该下线报告没有得到更新则过期并删除。下线报告的有效期限是server.cluster_node_timeout * 2,主要是针对故障误报的情况。例如节点A在上一个小时报告节点B主观下线,但是之后又恢复正常。现在又有其他节点上报节点B主观下线,根据实际情况之前的属于误报不能被使用。

      运维提示:如果在cluster-node-time * 2时间内无法收集到一半以上槽节点的下线报告,那么之前的下线报告将会过期,也就是说主观下线上报的速度追赶不上下线报告过期的速度,那么故障节点将永远无法被标记为客观下线从而导致故障转移失败。因此不建议将cluster-node-time设置得过小。

      (2)尝试客观下线

      集群中的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线,流程如下:

      1)首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出。

      2)当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。

      3)向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。

      广播fail消息是客观下线的最后一步,它承担着非常重要的职责:

      • 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效。

      • 通知故障节点的从节点触发故障转移流程。

      需要理解的是,尽管存在广播fail消息机制,但是集群所有节点知道故障故障节点进入客观下线状态是不确定的。比如当出现网络分区时有可能集群被分割为一大一小两个独立集群中。大的集群持有半数槽节点可以完成客观下线并广播fail消息,但是小集群无法接收到fail消息。当时当网络恢复后,只要故障节点变为客观下线,最终总会通过Gossip消息传播至集群的所有节点。

      运维提示:网络分区会导致分割后的小集群无法收到大集群的fail消息,因此如果故障节点所有的从节点都在小集群内将导致无法完成后续故障转移,因此部署主从结构时需要根据自身机房/机架拓扑结构,降低主从被分区的可能性。

  2. 故障恢复

    故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程:

    1. 资格检查

      每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。参数cluster-slave-validity-factor用于从节点的有效因子,默认为10.

    2. 准备选举时间

      当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。故障选举时间相关字段如下:

      struct clusterState {
          ...
          mstime_t failover_auth_time; /* 记录之前或者下次将要执行故障选举时间 */
          int failover_auth_rank; /* 记录当前从节点排名 */
      }
      

      这里之所以采用延迟触发机制,主要是通过多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。

    3. 发起选举

      当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:

      (1)更新配置纪元

      配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元(clusterNode.configEpoch)标示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配置纪元(clusterState.currentEpoch),用于记录集群内所有主节点配置纪元的最大版本。执行cluster info命令可以查看配置纪元信息:

      127.0.0.1:6379> cluster info
      ...
      cluster_current_epoch:15    //整个集群最大配置纪元
      cluster_my_epoch:13         //当前主节点配置纪元
      

      配置纪元会跟随ping/pong消息在集群内传播,当发送方与接收方都是主节点且配置纪元相等时代表出现了冲突,nodeId更大的一方会递增全局配置纪元并赋值给当前节点来区分冲突。

      配置纪元的主要作用:

      • 标示集群每个主节点的不同版本和当前集群最大的版本。

      • 每次集群发生重要事件时,这里的重要事件值出现新的主节点(新加入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。

      • 主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong消息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时的消息状态污染集群。

      配置纪元的应用场景有:

      • 新节点加入
      • 槽节点映射冲突检测。
      • 从节点投票选举冲突检测。

      开发提示:之前在通过cluster setslot命令修改槽节点映射时,需要确保执行请求的主节点本地配置纪元(configEpoch)是最大值,否则修改后的槽信息在消息传播中不会被拥有更高的配置纪元的节点采纳。由于Gossip通信机制无法准确知道当前最大的配置纪元在哪个节点,因此在槽迁移任务最后的cluster setslot {slot} node {nodeId}命令需要在全部主节点中执行一遍。

      从节点每次发起投票时都会自增集群的全局配置纪元,并单独保存在clusterState,failover_auth_epoch变量中国用于标识本次从节点发起选举的版本。

      (2)广播选举消息

      在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。消息内容如同ping消息只是将type类型变为FAILOVER_AUTH_REQUEST。

    4. 选举投票

      只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。

      投票过程其实是一个领导者选举的过程,如集群内又N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。

      Redis集群没有直接使用从节点记性领导者选举,主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。

      当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主机诶单操作,例如集群内有5个持有槽的主节点,主节点b故障后还有4个,当其中一个从节点收集到3张投票时代表获得了足够的选票可以进行替换主节点操作。

      运维提示:故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到3/2+1个主机诶单选票将导致故障转移失败。这个问题也是用于故障发现环节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。

      投票作废: 每个配置纪元代表依次选举周期,如果在开始投票之后的cluster-node-timeout * 2时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,知道选举成功为止。

    5. 替换主节点

      当从节点收集到足够的选票之后,触发替换主节点操作:

      1)当前从节点取消复制变为主节点。

      2)执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。

      3)向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。

  3. 故障转移时间

    在介绍完故障发现和恢复的流程后,这时我们可以估算出故障转移时间:

    1)主观下线(pfail)识别时间=cluster-node-timeout

    2)主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。

    3)从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟一秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。

    根据以上分析可以预估出故障转移时间,如下:

    failover-time(毫秒) <= cluster-node-timeout + cluster-node-timeout/2 + 1000
    

    因此,故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。配置时可以根据业务容忍度做出适当调整,但不是越小越好,下一节的带宽消耗部分进一步说明。




原文地址:访问原文地址
快照地址: 访问文章快照