剖析Redis集群

Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能

1.节点

一个Redis集群通常由多个节点组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建真正可工作的集群,必须将各个独立的节点连接起来。

连接各个节点的工作可以使用CLUSTER MEET命令来完成,可以让node节点与ip和port所指定的节点进行握手,当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。
  1. 启动节点:
    1、一个节点就是一个运行在集群模式下的Redis服务器,服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。
    2、节点会继续使用所有在单机模式中使用的服务器组件。PS:serverCron函数又会调用集群模式特有的clusterCron函数,该函数负责执行在集群模式下需要执行的常规操作。至于那些只有在集群模式下才会用到的数据,节点将它们保存到了clusterNode结构、clusterLink结构。

  2. 集群数据结构:
    1、clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、名字、配置纪元、IP地址、端口号等。
    2、每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点都创建一个相应的clusterNode结构,以此来记录其他节点的状态。
    3、clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息。
    4、redisClient结构和clusterLink结构的相同和不同之处:
    相同:都有自己的套接字描述符和输入、输出缓冲区。
    不同:redisClient是用于连接客户端的,而clusterLink是用于连接节点的。
    5、每个节点都保存着一个clusterState结构,记录了在当前节点的视角下,集群目前所处的状态。

  3. CLUSTER MEET命令的实现:
    收到命令的节点A将与节点B进行握手,以此来确认彼此的存在,并为将来的进一步通信打好基础:
    1、节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里
    2、节点A将根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息
    3、节点B将接收到MEET消息,节点B会为节点A创建一个clusterNode,并将该结构添加到自己的clusterState.nodes字典里
    4、节点B向节点A返回一条PONG消息
    5、节点A收到PONG消息,从而知道B已经成功接收到了MEET消息
    6、A向B返回一条PING消息
    7、B接收到A返回的PING消息,通过这条PING消息B知道A接收到了自己返回的PONG消息,握手完成

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

2.槽指派

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

当数据库中的16384个槽都有节点在处理时,集群处于上线状态;相反,处于下线状态。

通过向节点发送CLUSTER ADDSLOTS命令,可以将一个或多个槽指派给节点负责
  1. 记录节点的槽指派信息:
    1、clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽:
    slots属性是一个二进制位数组,这个数组的长度为16384/8=2048个字节,共包含16384个二进制位
    Redis对slots数组中的16384个二进制位进行编号,并根据索引i上的二进制位的值来判断节点是否负责处理槽i。如果未1,表示节点负责处理槽i。
    numslots属性则记录节点负责处理的槽的数量,也即是slots数组中值为1的数量。
  2. 传播节点的槽指派信息:
    1、一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots和numslots属性之外,还会将自己的slots数组通过消息发送给集群中的其他节点。
    2、当节点A通过消息从B那里接收到节点B的slots数组时,A会在自己的clusterState.nodes字典中查找B对于的clusterNode结构,并对结构中的slots数组进行保存或更新。
  3. 记录集群所有槽的指派信息
    1、clusterState结构中的slots数组记录了集群中所有16384个槽的值派信息:
    slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:
    如果slots[i]指针指向NULL,那么表示槽i尚未指派给任何节点;
    如果指向一个clusterNode结构,表示槽i已经指派给了clusterNode结构所代表的节点
    2、如果只将槽指派信息保存在各个节点的clusterNode.slots数组里,会出现一些无法高效解决的问题,而clusterState.slots数组的存在解决了这些问题:
    如果只使用clusterNode.slots数组来记录槽的指派信息,那么为了知道槽i是否已经被指派,或者槽i被指派给了哪个节点,程序需要遍历字典中的所有clusterNode结构,复杂度为O(N).
    3、clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息,这是2个slots数组的关键区别所在。
  4. CLUSTER ADDSLOTS命令的实现:
    1、该命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责,哪怕有一个槽已经被指派给了某个节点,就向客户端返回错误,并终止命令执行。

3.在集群中执行命令

在对数据库中的16384个槽都进行了指派后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。

当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己。如果指派给了当前节点,就直接执行;如果并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并再次发送。
  1. 计算键属于哪个槽:
    1、节点使用CRC16(key)&16383来计算给定键key属于哪个槽,其中CRC16(key)语句用于计算键key的CRC-16校验和,而&16383语句则用于计算出一个整数作为键的槽号。
    2、使用CLUSTER KEYSLOT命令可以查看一个给定键属于哪个槽。
  2. 判断槽是否由当前节点负责处理:
    1、当节点计算出键所属的槽i后,节点就会检查自己在clusterState.slots数组中的项i,判断键所在的槽是否由自己负责,如果clusterState.slots[i]等于clusterState.myself,那么说明槽i并非由当前节点负责,节点会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点。
  3. MOVED错误:
    1、MOVED错误的格式为:MOVED错误的格式为:MOVED:
    其中slot为键所在的槽,而ip和port则是负责处理槽slot的节点的IP地址和端口号
    2、一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是换一个套接字来发送命令
    3、被隐藏的MOVED错误:
    集群模式的redis-cli客户端在接收到MOVED错误时,并不会打印出MOVED错误,而是根据MOVED错误自动进行节点转向,并打印出转向信息。
  4. 节点数据库的实现:
    1、集群节点保存键值对以及键值对过期时间的方式与单机版完全相同。
    2、节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库,而单机服务器则没有这一限制。
    3、除了将键值对保存在数据库里之外,节点还会用clusterState结构中的slots_to_keys跳跃表来保存槽和键之间的关系。
    跳跃表每个节点的分值都是一个槽号,而每个节点的成员都是一个数据库键。
    每当节点往数据库中添加一个新的键值对时,节点就会将这个键以及键的槽号关联到跳跃表。
    当节点删除数据库中某个键值对时,就会在跳跃表解除被删除键和槽号的关联。
    4、通过在slots_to_keys跳跃表中记录各个数据库键所属的槽,节点可以很方便地对属于某个或某些槽的所有数据库键进行批量操作。例如CLUSTER GETKEYSINSLOT命令可以返回最多count个属于槽slot的数据库键。而这个命令就是通过遍历跳跃表来实现的。

4.重新分片

  1. 集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。
  2. 重新分片操作可以在线进行,在重新分片的过程中,集群不需要下线。
  3. 重新分片的实现原理:
    1、集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
    2、redis-trib对集群的单个槽slot进行重新分片的步骤如下:
    (1)对目标节点发送CLUSTER SETSLOT IMPORTING <source_id>命令,让目标节点准备好从源节点导入属于槽slot的键值对。
    (2)对源节点发送CLUSTER SETSLOT MIGRATING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点。
    (3)向源节点发送CLUSTER GETKEYSINSLOT 命令,获得最多count个属于槽slot的键值对的键名。
    (4)对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE<target_ip><target_port><key_name>0命令,将被选中的键原子地从源节点迁移至目标节点。
    (5)重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。
    (6)redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT NODE <target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群。

5.ASK错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里,而另一部分键值对则保存在目标节点里。

和接到MOVED错误时的情况类似,集群模式的redis也不会打印错误。集群模式下的redis-cli并未支持ASK在自动转向。

  1. CLUSTER SETSLOT IMPORTING命令的实现:
    1、clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽:
    如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在从clusterNode所代表的节点导入槽i。
    在对集群进行重新分片的时候,向目标节点发送命令:
    CLUSTER SETSLOT IMPORTING <source_id>
    可以将目标节点importing_slots_from[i]的值设置为source_id所代表节点的clusterNode结构。
  2. CLUSTER SETSLOT IMPORTING命令的实现:
    1、clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽:
    在对集群进行重新分片的时候,向源节点发送命令:
    CLUSTER SETSLOT IMPORTING <target_id>
    可以将源节点clusterState.migrating_slots_to[i]的值设置为target_id所代表节点的clusterNode结构。
  3. ASK错误:
    1、如果节点收到一个关于键key的请求,并且键key所属的槽i正好就指派给了这个节点,那么节点会尝试在自己的数据库里查找键key。相反,如果没有找到,那么节点会检查自己clusterState.migrating_slots_to[i],看键key所属的槽是否正在进行迁移,如果的确在进行迁移的话,那么会向客户端发送一个ASK错误,引导客户端到正在导入槽i的节点去查找。
    2、转向后,会首先发送命令ASKING,再发送命令请求。
  4. ASKING命令
    1、唯一要做的就是打开发送该命令的客户端的REDIS_ASKING标识。
    2、
    3、当客户端接收到ASK错误并转向至正在导入槽的节点时,客户端会先向节点发送一个ASKING命令,然后才重新发送想要执行的命令,这是因为如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,该命令将被节点拒绝执行,并返回MOVED错误。
    4、客户端的REDIS_ASKING标识是一个一次性标识,当节点执行了一个带有该标识的客户端发送的命令后,该标识就会被移除。
  5. ASK错误和MOVED错误与的区别:
    它们都会导致客户端转向,区别在于:
    MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
    ASK错误只是2个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令发送至目前负责处理槽i的节点,除非ASK错误再次出现。

6.复制与故障转移

Redis集群中的节点分为主节点和从节点,其中主节点用于处理槽,从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
  1. 设置从节点:
    1、向一个节点发送命令:CLUSTER REPLICATE<node_id>
    可以让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制:
    (1)接收到该命令的节点首先会在自己的clusterState.nodes字典中找到node_id所对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点。
    (2)然后节点会修改自己在clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经变成了从节点。
    (3)最后,节点会调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP地址和端口号,对主节点进行复制。
    2、一个节点称为从节点,并开始复制某个主节点这一信息会通过消息发送给集群中的其他节点。
  2. 故障检测:
    1、集群中的每个节点都会定期向集群中的其他节点发送PING消息,以此来检测对方是否在线。如果在规定时间内,没有返回PONG消息,那么就会被标记为疑似下线。即在自己的clusterState.nodes字典中找到节点对应的clusterNode结构,并在结构的flags属性中打开REDIS_NODE_PFAIL标识。
    2、当一个主节点通过消息通知主节点B认为主节点C进入了疑似下线状态时,A会在自己的clusterState.nodes字典中找到C所对应的clusterNode结构,并将B的下线报告添加到clusterNode结构的fail_reports链表里。 每个下线报告由一个clusterNodeFailReport结构表示。
    3、如果在一个集群里,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线,将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条消息的节点都会立即将主节点x标记为已下线。
  3. 故障转移:
    1、当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是执行步骤;
    (1)复制下线主节点的所有从节点里,会有一个从节点被选中。
    (2)被选中的从节点会执行SLAVE no one命令,成为新的主节点。
    (3)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
    (4)新的主节点向集群广播一条PONG消息,这条消息可以让集群中的其他节点立即知道这个节点已经变成了主节点。
    (5)新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
  4. 选举新的主节点:
    1、新的主节点时通过选举产生的:
    (1)集群的配置纪元是一个自增计数器,它的初始值为0.
    (2)当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被+1.
    (3)对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
    (4)当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票。
    (5)如果一个主节点具有投票权,并且尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTER_TYPE_FAILOVER_AUTH_ACK消息,表示支持。
    (6)每个参与选举的从节点都会接收消息,并根据自己受到了多少条这种消息来统计自己获得了多少主节点的支持。
    (7)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
    (8)在每一个配置纪元中,只能投一次票。
    (9)如果在一个配置纪元里没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点位置。

7.消息

集群中的各个节点通过发送和接收消息来进行通信。

节点发送的消息主要有以下5种:

1.MEET消息,当发送者接收到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里。

2.PING消息,集群里的每个节点默认每隔1秒钟就会从已知节点列表中随机选出5个节点,然后对这5个节点中最长时间没有发送过PING消息的节点发送,以此来检测是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了Acluster-node-timeout选项设置时长的一半,也会发送,防止A因为长时间没有随机选中B作为发送对象而导致B的信息更新滞后。

3.PONG消息,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行后,新的主节点会向集群广播一条PONG消息。

4.FAIL消息,当一个主节点A判断另一个节点B已经进入FAIL状态时,节点A会向集群广播一条关于B的FAIL消息,所有收到这条消息的节点都会立即将B标记为已下线。

5.PUBLISH消息,当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。

一条消息由消息头和消息正文组成。
  1. 消息头:
    1、节点发送的所有消息都由一个消息头包裹,消息头除了包含消息正文之外,还记录了消息发送者自身的一些信息,因为这些信息也会被消息接收者用到。
    2、每个消息头都由一个clusterMsg结构表示。
    3、clusterMsg.data属性指向联合clusterMsgData,这个联合就是消息的正文。
    4、clusterMsg结构的currentEpoch、sender、myslots等属性记录了发送者自身的节点信息,接受者会根据这些信息,在自己的clusterState.nodes字典里找到发送者对应的clusterNode结构,并对结构进行更新。
  2. MEET、PING、PONG消息的实现
    1、集群中的各个节点通过Gossip协议来交换各种关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种消息实现,这三种消息的正文都由2个clusterMsgDataGossip结构组成
    2、因为MEET、PING、PONG三种消息都使用相同的消息正文,所以节点通过消息头的type属性来判断一条消息是什么类型。
    3、每次发送MEET、PING、PONG消息时,发送者都从自己的已知节点列表中随机选出2个节点,并将这2个被选中节点的信息分别保存到2个clusterMsgDataGossip结构里。该结构记录了被选中节点的名字,发送者与被选中节点最后一次发送发送和接收PING消息和PONG消息的时间戳,被选中节点的IP地址和端口号,以及被选中节点的标识值。
    4、当接收者收到MEET、PING、PONG消息时,接收者会访问消息正文中的2个clusterMsgDataGossip结构,并根据自己是否认识clusterMsgDataGossip结构中记录的被选中节点来选择进行哪种操作:
    如果被选中节点不存在与接收者的已知节点列表,那么说明接收者是第一次接触到被选中节点,接收者将与被选中节点握手。
    如果已经存在,说明之前进行过接触,将根据记录的信息,对被选中节点所对应的clusterNode结构进行更新。
  3. FAIL消息的实现:
    1、当集群里A将B标记为已下线时,A将向集群广播一条关于B的FAIl消息,所有接收到这条消息的节点都会将B标记为已下线。
    2、在集群的节点数量比较大的情况下,单纯使用Gossip协议来传播节点的已下线信息会给节点的信息更新带来一定延迟,因为Gossip协议消息通常需要一段时间才能传播至整个集群,而发送FAIl消息可以立即让集群知道。
    3、FAIL消息的正文由clusterMsgDataFail结构表示,这个结构只包含一个nodename属性,该属性记录了已下线节点的名字
  4. PUBLISH消息的实现:
    1、当客户端向集群中的某个节点发送命令PUBLISH的时候,接收到PUBLISH命令的节点不仅会向channel频道发送message,还会向集群广播一条PUBLISH消息,所有接收到这条消息的节点都会向channel频道发送message消息。
    2、PUBLISH消息的正文由clusterMagDataPublish结构表示:
    bulk_data属性是一个字节数组,保存了客户端通过PUBLISH命令发送给节点的channel和message参数,而channel_len和message_len则保存了长度。
    3、为什么不直接向节点广播PUBLISH命令:
    实际上,要让集群的所有节点都执行相同的PUBLISH命令,最简单的方法就是向所有节点广播相同的PUBLISH命令,这也是在复制PUBLISH命令时所使用的方法,不过因为这种做法不符合Redis集群的各个节点通过发送和接收消息来进行通信这一规则,所有节点没有采取广播PUBLISH命令的做法。

8.重点回顾

  1. 节点通过握手来将其他节点添加到自己所处的集群当中。
  2. 集群中的16384个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,而哪些槽又被指派给了其他节点。
  3. 节点在接到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责,如果不是的话,节点将向客户端返回一个MOVED错误,MOVED错误携带的信息可以指引客户端转向至正在负责相关槽的节点。
  4. 对Redis集群的重新分片工作是由redis-trib负责执行的,关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。
  5. 如果节点A正在迁移槽i至节点B,那么当A没能在自己的数据库中找到命令指定的数据库键时,A会向客户端返回一个ASK错误,指引客户端到节点B继续查找。
  6. MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点,而ASK错误只是2个节点在迁移槽的过程中使用的一种临时措施。
  7. 集群里的从节点用于复制主节点,并在主节点下线时,代替主节点继续处理命令。
  8. 集群中的节点通过发送和接收信息来进行通信,常见的消息包括MEET、PING、PONG、PUBLISH、FAIl5种。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值