【Redis实现系列】集群实现与故障迁移

集群

节点与槽指派

启动集群节点

一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式

在这里插入图片描述

节点(运行在集群模式下的Redis服务器)会继续使用所有在单机模式中使用的服务器组件,比如说:

  • 节点会继续使用文件事件处理器来处理命令请求和返回命令回复。

  • 节点会继续使用时间事件处理器来执行serverCron函数,而serverCron函数又会调用集群模式特有的clusterCron函数。clusterCron函数负责执行在集群模式下需要执行的常规操作,例如向集群中的其他节点发送Gossip消息,检查节点是否断线,或者检查是否需要对下线节点进行自动故障转移等。

  • 节点会继续使用数据库来保存键值对数据,键值对依然会是各种不同类型的对象。

  • 节点会继续使用RDB持久化模块和AOF持久化模块来执行持久化工作。

  • 节点会继续使用发布与订阅模块来执行PUBLISH、SUBSCRIBE等命令。

  • 节点会继续使用复制模块来进行节点的复制工作。

  • 节点会继续使用Lua脚本环境来执行客户端输入的Lua脚本。

除此之外,节点会继续使用redisServer结构来保存服务器的状态,使用redisClient结构来保存客户端的状态,至于那些只有在集群模式下才会用到的数据,节点将它们保存到了cluster.h/clusterNode结构、cluster.h/clusterLink结构,以及cluster.h/clusterState结构里面。

集群数据结构

clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元、节点的IP地址和端口号等等。

  • 不必纠结下面说这些数据结构存在哪个结构里,只要知道数据结构里什么即可,在使用的时候可以获取到需要的数据就行了。至于这个数据结构保存在哪里只是使用时是否优雅还有效率的问题,但是只要存下来就可以满足需求。

clusterNode

  • 每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的clusterNode结构,以此来记录其他节点的状态:

    struct clusterNode {
      // 创建节点的时间
      mstime_t ctime;
      // 节点的名字,由40个十六进制字符组成
      // 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
      char name[REDIS_CLUSTER_NAMELEN];
      // 节点标识
      // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
      // 以及节点目前所处的状态(比如在线或者下线)。
      int flags;
      // 节点当前的配置纪元,用于实现故障转移
      uint64_t configEpoch;
      // 节点的IP地址
      char ip[REDIS_IP_STR_LEN];
      // 节点的端口号
      int port;
      // 保存连接节点所需的有关信息
      clusterLink *link;
      // ...
    };
    

clusterLink

  • clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区:

    typedef struct clusterLink {
      // 连接的创建时间
      mstime_t ctime;
      // TCP 套接字描述符
      int fd;
      // 输出缓冲区,保存着等待发送给其他节点的消息(message)。 
      sds sndbuf;
      // 输入缓冲区,保存着从其他节点接收到的消息。
      sds rcvbuf;
      // 与这个连接相关联的节点,如果没有的话就为NULL 
      struct clusterNode *node;
    } clusterLink;
    
  • redisClient结构和clusterLink结构的相同和不同之处

    • redisClient结构和clusterLink结构都有自己的套接字描述符和输入、输出缓冲区
    • 这两个结构的区别在于,redisClient结构中的套接字和缓冲区是用于连接客户端的,而clusterLink结构中的套接字和缓冲区则是用于连接节点的。

clusterState

  • 每个节点都保存着一个clusterState结构,这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元,诸如此类:

    typedef struct clusterState {
      // 指向当前节点的指针
      clusterNode *myself;
      // 集群当前的配置纪元,用于实现故障转移
      uint64_t currentEpoch;
      // 集群当前的状态:是在线还是下线
      int state;
      // 集群中至少处理着一个槽的节点的数量
      int size;
      // 集群节点名单(包括myself节点)
      // 字典的键为节点的名字,字典的值为节点对应的clusterNode结构
      dict *nodes;
      // ...
    } clusterState;
    

在这里插入图片描述

集群状态增加节点

通过向节点 A 发送CLUSTER MEET命令,客户端可以让接收命令的节点 A 将另一个节点 B 添加到节点 A 当前所在的集群里面:

CLUSTER MEET <ip> <port>

收到命令的节点A将与节点B进行握手(handshake),以此来确认彼此的存在,并为将来的进一步通信打好基础:

  • 节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。

  • 之后,节点A将根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息(message)。

  • 如果一切顺利,节点B将接收到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。

  • 之后,节点B将向节点A返回一条PONG消息。

  • 如果一切顺利,节点A将接收到节点B返回的PONG消息,通过这条PONG消息节点A可以知道节点B已经成功地接收到了自己发送的MEET消息。

  • 之后,节点A将向节点B返回一条PING消息。

  • 如果一切顺利,节点B将接收到节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功地接收到了自己返回的PONG消息,握手完成。

在这里插入图片描述

之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,经过一段时间之后,节点B会被集群中的所有节点认识。

节点记录槽指派

clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽:

struct clusterNode {
  // ...
  unsigned char slots[16384/8];
  int numslots;
  // ...
};
  • slots属性是一个二进制位数组(bit array),这个数组的长度为16384/8=2048个字节,共包含16384个二进制位。

    示例:若该节点控制槽位 0~7

    在这里插入图片描述

  • numslots属性则记录节点负责处理的槽的数量,也即是slots数组中值为1的二进制位的数量。

传播槽指派

  • 一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。

在这里插入图片描述

  • 当节点A通过消息从节点B那里接收到节点B的slots数组时,节点A会在自己的clusterState.nodes字典中查找节点B对应的clusterNode结构,并对结构中的slots数组进行保存或者更新。

  • 因为集群中的每个节点都会将自己的slots数组通过消息发送给集群中的其他节点,并且每个接收到slots数组的节点都会将数组保存到相应节点的clusterNode结构里面,因此,集群中的每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。

集群状态记录槽指派

clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:

typedef struct clusterState {
  // ...
  clusterNode *slots[16384];
  // ...
} clusterState;

slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:

  • 如果slots[i]指针指向NULL,那么表示槽i尚未指派给任何节点。

  • 如果slots[i]指针指向一个clusterNode结构,那么表示槽i已经指派给了clusterNode结构所代表的节点。

在这里插入图片描述

虽然clusterState.slots数组记录了集群中所有槽的指派信息,但使用clusterNode结构的slots数组来记录单个节点的槽指派信息仍然是有必要的:

  • 因为当程序需要将某个节点的槽指派信息通过消息发送给其他节点时,程序只需要将相应节点的clusterNode.slots数组整个发送出去就可以了。

  • 另一方面,如果Redis不使用clusterNode.slots数组,而单独使用clusterState.slots数组的话,那么每次要将节点A的槽指派信息传播给其他节点时,程序必须先遍历整个clusterState.slots数组,记录节点A负责处理哪些槽,然后才能发送节点A的槽指派信息,这比直接发送clusterNode.slots数组要麻烦和低效得多。

clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息,这是两个slots数组的关键区别所在。

槽指派实现

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。

  • 当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。

    127.0.0.1:7000> CLUSTER INFO
    cluster_state:fail
    cluster_slots_assigned:0
    cluster_slots_ok:0
    cluster_slots_pfail:0
    cluster_slots_fail:0
    cluster_known_nodes:3
    cluster_size:0
    cluster_current_epoch:0
    cluster_stats_messages_sent:110
    cluster_stats_messages_received:28
    
  • 通过 CLUSTER ADDSLOTS 指派槽位

    # 给 7000 的服务器指派槽位
    127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
    OK
    # 给 7001 的服务器指派槽位
    127.0.0.1:7001> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000
    OK
    # 给 7002 的服务器指派槽位
    127.0.0.1:7002> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383
    OK
    
    # 槽位都分配后查看集群状态
    127.0.0.1:7000> CLUSTER INFO
    cluster_state:ok
    cluster_slots_assigned:16384
    cluster_slots_ok:16384
    cluster_slots_pfail:0
    cluster_slots_fail:0
    cluster_known_nodes:3
    cluster_size:3
    cluster_current_epoch:0
    cluster_stats_messages_sent:2699
    cluster_stats_messages_received:2617
    
    # 查看集群节点的情况
    127.0.0.1:7000> CLUSTER NODES
    9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388317426165 0 connected 10001-16383
    68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388317427167 0 connected 5001-10000
    51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected 0-5000
    

CLUSTER ADDSLOTS 实现

  • 伪代码表示:

    def CLUSTER_ADDSLOTS(*all_input_slots):
        # 遍历所有输入槽,检查它们是否都是未指派槽
        for i in all_input_slots:
            # 如果有哪怕一个槽已经被指派给了某个节点
            # 那么向客户端返回错误,并终止命令执行
            if clusterState.slots[i] != NULL:
                reply_error()
                return
        # 如果所有输入槽都是未指派槽
        # 那么再次遍历所有输入槽,将这些槽指派给当前节点
        for i in all_input_slots:
            # 设置clusterState结构的slots数组
            # 将slots[i]的指针指向代表当前节点的clusterNode结构
            clusterState.slots[i] = clusterState.myself
            # 访问代表当前节点的clusterNode结构的slots数组
            # 将数组在索引i上的二进制位设置为1
            setSlotBit(clusterState.myself.slots, i)
    

故障转移

Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。

设置从节点

向一个节点发送命令:

# 让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制
CLUSTER REPLICATE <node_id>
  • 接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点:

    struct clusterNode {
         // ...
         // 如果这是一个从节点,那么指向主节点
         struct clusterNode *slaveof;
         // ...
    };
    
  • 然后节点会修改自己在clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经由原来的主节点变成了从节点。

  • 最后,节点会调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制。因为节点的复制功能和单机Redis服务器的复制功能使用了相同的代码,所以让从节点复制主节点相当于向从节点发送命令SLAVE OF。

    在这里插入图片描述

一个节点成为从节点,并开始复制某个主节点后

  • 会通过消息发送给集群中的其他节点,最终集群中的所有节点都会知道某个从节点正在复制某个主节点

  • 集群中的所有节点都会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单:

    struct clusterNode {
        // ...
        // 正在复制这个主节点的从节点数量
        int numslaves;
        // 一个数组
        // 每个数组项指向一个正在复制这个主节点的从节点的clusterNode结构
        struct clusterNode **slaves;
        // ...
    };
    

    在这里插入图片描述

故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail,PFAIL)。

  • 然后会在自己的clusterState.nodes字典中找到下线节点所对应的clusterNode结构,并在结构的flags属性中打开REDIS_NODE_PFAIL标识,以此表示该节点进入了疑似下线状态

在这里插入图片描述

集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,例如某个节点是处于在线状态、疑似下线状态(PFAIL),还是已下线状态(FAIL)。

  • 当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点C所对应的clusterNode结构,并将主节点B的下线报告(failure report)添加到下线节点对应clusterNode结构的fail_reports链表里面:

    struct clusterNode {
      // ...
      // 一个链表,记录了所有其他节点对该节点的下线报告
      list *fail_reports;
      // ...
    };
    
  • 每个下线报告由一个clusterNodeFailReport结构表示:

    struct clusterNodeFailReport {
      // 报告目标节点已经下线的节点
      struct clusterNode *node;
      // 最后一次从node节点收到下线报告的时间
      // 程序使用这个时间戳来检查下线报告是否过期
      // (与当前时间相差太久的下线报告会被删除)
      mstime_t time;
    } typedef clusterNodeFailReport;
    
  • 比如 7000 节点下线,其他节点保存的结构

    在这里插入图片描述

如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

  • 和 Sentinel 不同的是,Sentinel 是询问,然后统计回复,Cluster 是发送报告,接收报告的其他主节点来统计收到的报告数
故障转移步骤

当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:

  • 复制下线主节点的所有从节点里面,会有一个从节点被选中。
  • 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
  • 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  • 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
  • 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
选举新的主节点

cluster 来执行故障迁移的是下线节点的从节点来执行的。

  • 集群的配置纪元是一个自增计数器,它的初始值为0。

  • 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。

  • 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。

  • 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。

  • 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。

  • 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。

  • 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。

  • 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。

  • 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

这个选举新主节点的方法和选举领头Sentinel的方法非常相似,因为两者都是基于Raft算法的领头选举(leader election)方法来实现的。

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:C马雯娟 返回首页