集群

集群通过分片来进行数据共享,并提供复制和故障转移

节点
  • 1.一个集群有多个节点
  • 2.节点代表的是master或者slave
  • 3.当节点A发生CLUSTER MEET <IP> <PORT>给节点B(ip和port都是B节点的地址),两个节点就会进行握手当握手成功后。节点B就会被加入到节点A所在的集群。


    image.png

启动节点

  • 1.redis服务器在启动的时候会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式


    image.png

集群数据结构

  • 1.clusterNode结构保存了一个节点的当前状态,比如节点的创建时间,节点名字,节点当前的配置纪元,节点的ip地址和端口号
  • 2.每个节点都会使用一个clusterNode来记录自己的状态,并为集群中的所有其他节点(包括主从节点)
    创建一个相应的clusterNode结构(这就意味着每个节点都保存了集群中所有的clusterNode,这些节点都被存放在clusterstate中的nodes属性中)


    image.png
  • 3.其中link属性是一个clusterLink结构,该结构保存了连接点所需要的有关信息,比如套字接描述符,输入缓冲区和输出缓冲区(应该保存的是A节点到B节点之间的链接)


    image.png
  • 4.redisclient和cluster都有自己的套字接描述符合输入,输出缓冲区。前者是用于连接客户端的,后者是用于连接节点的。
  • 5.clusterState记录了在当前节点的视角下,集群所处于的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元


    image.png

    image.png

cluster meet 命令的实现

  • 1.我们像A节点发生cluster meet命令,A节点根据ip和port去寻找B节点并进行握手。握手的过程如下:
  • 2.A节点为B节点创建一个clusterNode结构,并放入A节点中的clusterState的nodes属性(字典)
  • 3.之后A节点像B节点发生一个MEET消息(前面只是握手成功,这边是发送消息),如果一切顺利,则B节点在接收到A节点发生的MEET消息后,节点B会为节点A创建一个clusterNode,并加入到B自己的clusterState的nodes属性(字典)
  • 4.之后B向A返回一个PONG消息
  • 5.通过PONG消息,A知道了B已经接收到自己的MEET消息,之后A将向节点B返回一条PING消息
  • 6.B收到PING消息后就是知道A已经成功收到自己返回的PONG消息,握手完成


    image.png
  • 7.之后节点A会将节点B的信息通过GOSSIP协议传播给集群中的其他节点,并让其他节点与B进行握手。
槽指派
  • 1.集群的整个数据库被分为16384个槽(slot),这意味着假如你有10个节点,每个节点有10个数据库,那么就相当于每个数据库只能分到20分之一的slot
  • 2.集群中的每个节点可以处理0个或者最多16384个slot
  • 3.如果16384个槽点都有节点在处理,则集群处于上线状态(ok),否则只要有一个槽没有被集群处理,那么集群处于下线状态
  • 4.通过向节点发生cluster addslots命令,我们可以将一个或多个槽指派给节点负责

记录节点的槽指派信息

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


    image.png
  • 2.slots属性一个二进制位数组,这个数组的长度为16384/8=2048字节(因为是位数组所以需要除8得到字节,1字节=8位),也就是说是16384位
  • 3.redis以0为起始索引,16383为终止索引,对slots数组中的16384个二进制位进行编号,并根据索引位上的值来判断节点是否负责处理槽i
  • 4.如果slots数组在索引i上的二进制位的值为1,那么标识该节点负责处理槽i,如果为0代表该节点不负责处理槽i
  • 5.取出和设置slots数组中的任意一个二进制位的值的复杂度都是o(1)
  • 6.numslots 是记录该节点负责处理的slot的数量

传播节点的槽指派信息

  • 1.一个节点除了会将自己处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会在将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。
  • 2.当节点收到其他节点发送来的slots属性后,其会在custerState.nodes字典中查找对应节点的ClusterNode结构,并对结构中的slots数组进行更新

记录集群中所有槽的指派信息

  • 1.clusterState中的slots是clusterNode类型的


    image.png
  • 如果slots[i[指针指向Null代表槽未指向任何节点
  • 如果指向一个clusterNode,标识该槽已经指派给了clusterNode结构所代表的节点
  • 之所以还需要clusterState的slots是因为我们可以不需要遍历就知道哪个槽分配给了哪个节点,否则我们还需要遍历clusterState中的nodes字典中的所有clusterNode结构才能知道


    image.png

CLUSTER ADDSLOTS命令的实现

  • 该命令接受一个或多个槽作为参数,并将这些槽指派给接受该命令的节点负责
  • 具体步骤如下图


    image.png
  • 下面图先展示了 一个未指派的槽和以及如何指派槽1,2


    image.png

    image.png
在集群中执行命令
  • 1.如果该槽正好就在该节点则直接指向,否则会返回MOVED错误
  • 2.客户端根据,MOVED错误会指引客户转向redirect至正确的节点,并再次执行想要执行的命令


    image.png

计算键属于哪个槽(一共16384)

  • 计算的命令是CRC(16) &16383,用于计算CRC-16校验和,而&16383语句则用于计算出一个介于0到16383直接的整数作为键KEY的槽号
  • 使用CLUSTER KEYSLOT <KEY> 可以查看一个给的的键属于哪个槽


    image.png

判断槽是否由当前节点负责处理

  • 当节点计算出键所属的槽i之后,节点就会检查自己在clusterState.slots数组中的项i,判断所在的槽是否由自己负责。
  • 如果槽i是当前节点负责,那么直接执行并返回给客户端
  • 如果不属于该节点,那么就会根据clusterstate.nodes[i]指向的节点的ip和port,然后像客户端MOVED错误,指引客户端转向至正在处理槽i节点


    image.png

MOVED错误

  • MOVED <slot> <ip>:<port> 其中ip和port是正确的节点地址。
  • MOVED命令是由节点转向客户端
  • 一个集群的通常会与多个节点创建socket,所谓的节点转向实际上就是换一个socket发送命令
  • 如果客户端尚未与想要链接的节点创建socket,那么客户端会现根据MOVED的指令 提供的IP和port来链接节点,然后再进行转向。

节点数据库的实现

  • 节点中 只能是用0号数据库,而单机redis则没有这个限制
  • 还使用clusterstate结构中的slots_to_keys跳跃表保存槽和键之间的关系


    image.png
  • 每当节点往数据库新天一个键值时,节点就会对这个键以及键的槽号做关联,比如lst键 所在的跳跃表分值为3347 嗲表所关联的slot为3347


    image.png
  • 该跳跃表可以很表的对属于某个或者某些槽的数据库键进行批量操作
重新分片
  • 指将任意数量的某个节点的slots分配给其他节点
  • 重新分片可以在线进行,并且源节点和目标节点都可以继续处理命令请求
  • redis-trib是负责执行分片的具体步骤如下:


    image.png
image.png
ASK错误
  • 即当要处理的键已经被迁移到目标节点,那么源节点返回ASK错误,指引客户端找到真正的键所在的位置


    image.png
  • 一个槽可能包含多个键

cluster setslot importing命令的实现

  • clusterState结构的importing_slots_from数组记录了当前节点正在从其其他节点导入的槽


    image.png
  • 如果importing_slots_from[i]的值不为null,而是指向一个clusterNode结构,那么标识当前节点正在从clusterNode所代表的节点导入槽i
  • cluster setslot<i> importing <source_id> 可以将目标节点的importing_slots_from[i]的值设置为source_id所代表节点的ClusterNode,具体如下图


    image.png

cluster setslot migranting 命令的实现

  • clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽


    image.png
  • 如果migrating_slots_to[i]不为null 而是指向一个clusterNode结构,那么标识当前节点正在将槽i迁移至clusternode所代表的节点
  • 命令cluster setSLOT<i> migrating <target_id>,代表将migrating_slots_to[i]设置为其即将迁移的节点的ClusterNode


    image.png

ASK错误
比如下面的例子


image.png
image.png

ASKING命令

  • 该命令唯一要做的就是打开发送该命令的客户端的flags,即REDIS_ASKING标识
  • 节点判断是否指向客户端命令的过程如下


    image.png
  • 当节点按照最终的逻辑执行槽i命令,但是槽i已经迁移了 则返回ASK错误。
  • 当客户端转向真正的节点的时候需要先发送ASKING命令 以此来区分自己是因为ASK才来这个节点执行命令,否则会因为计算槽的算法返回MOVED错误
  • REDIS_ASKING是一次性标识,当节点执行了有REDIS_ASKING标识的客户端命令后,客户端的该标识就会被清除

ASK错误和MOVED错误的区别

  • 主要就是前者代表槽正在转移,后者代表槽不属于自己管辖


    image.png
复制与故障转移
  • redis集群中主节点用于处理槽,而从节点则用于复制某个主节点


    image.png

    image.png
  • 如果7000节点进入下线,那么集群中仍在正在运作的一个主节点将在原7000节点的2个从节点选择一个节点作为新的主节点


    image.png
  • 当7000重新上线后 只能成为7004的从节点


    image.png
设置从节点
  • cluster replicate <node_id> 可以让接收到命令的节点成为所指定节点的从节点,并开始进行复制
  • 接受到该命令的节点会在自己的clustestate.nodes中找到该node_id的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点:
  • 节点也会修改在自己的clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,并打开REDIS_NODE_SLAVE标识
  • 最后节点会调用复制方法


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


    image.png
image.png
故障检测
  • 集群中的每个节点都会定期的像集群中的其他节点发送ping消息,如果接受节点在规定时间回复PONG则ok,否则发送方会标记为接受方为疑似下线
  • 集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息。
  • 当主节点A通过消息得知主节点B认为主节点C进入疑似下线,那么主节点A会在自己的clusterState.nodes中找到主节点C所对应的ClusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports


    image.png
image.png
  • 如果一个集群半数疑似负责处理槽的主节点都将某个主节点X报告为疑似下线,那么主节点x将被标记下线,然后将消息广播到其他节点


    image.png
故障转移
image.png
选举新的主节点
image.png

image.png
  • 与选举sentinel选举相似 都是基于raft算法选举的
消息

节点发送的消息有五种

  • MEET(GOSSIP)消息:请求接受者加入到发送者所在的集群

  • PING(GOSSIP)消息:检测是否在线

  • PONG(GOSSIP)消息:对MEET和PING消息的回复,也可以广播向其他节点,刷新其他节点对自己的认识(比如故障转移操作执行成功之后)

  • FAIL消息:当节点A判断节点B fail之后,A会像集群广播一条关于节点B的Fail消息,其他收到消息这会立即将节点B标记为已下线

  • PUBLISH消息:节点接收到一条个该命令后节点会执行该命令,并向集群中广播一条PUBLISH消息


    image.png

    image.png
  • 一条消息由消息头(header)和正文(data)组成

消息头

消息头包含消息正文和记录了消息发送者的一些信息
对应着clusterMsg结构


image.png

image.png
  • 其中clusterMsg.data 属性指向了联合ClusterMsgData 也就是消息的正文


    image.png
  • 节点可以根据clusterMsg结构的currentEpoch,sender,myslots等属性找到发生者clusterNode结构并进行更新

MEET ,PING,PONG消息的实现

  • redis集群的各个节点通过gossip协议来交换各自关于不同节点的状态信息,其中MEET ,PING,PONG是gossip协议的三种消息实现,他们的正文都是clusterMsgDataGossip


    image.png
  • 因为三种消息正文相同,所以我们通过消息头的type属性来判断一条消息具体是什么消息

  • 每次发送这三种类型的消息的时候,发送这都从自己的已知节点列表随机选择两个节点(主从都可以),并将这两个被选择节点的消息分别保存到两个clusterMsgDataGossip结构中


    image.png
  • 接受者根据接受到信息去自己的已知列表中查看是否有被随机选择的节点信息,有就更新,没有就进行握手


    image.png

FAIL消息的实现

  • 主节点A将主节点B标记为已下线的FAIL时,节点A会像集群广播一条关于主节点B的Fail消息,所有收到该消息的节点都会将节点B标记为已经下线
  • 如果我们使用gossip协议,那么可能需要多次通信才能被集群所有节点知道B下线,而使用Fail只需要一次
  • 对应的数据结构是clusterMsgDataFail


    image.png
  • 只包含一个nodename,其记录了已下线节点的名字,因为集群中名字独一无二

PUBLISH消息的实现

  • 比如客户端使用publish channel message发向集群中的某个节点
  • 接收到publish消息的及诶按不仅会向channel频道发送message,还会像集群广播一条publish消息,进而导致其他节点也会向该channel发送message
  • 数据结构是clusterMsgDataPublish


    image.png
image.png
  • bulk_data 是一个字节数组,这个字节数组保存了客户端通过publish命令发给节点的channel参数和message参数
  • 其中bulk_data中的0-channel_len-1保存的是channel参数,channel_len至channel_len+message_len_1字节则是保存的message参数
  • 这个channel类似于mq中的channel


    image.png

推荐阅读更多精彩内容