Redis集群 - Cluster

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

使用CLUSTER MEET <ip> <port>命令来连接各个节点,组成集群

节点

就是运行在集群模式下的Redis服务器,通过配置参数cluster-enabled,Redis服务器会来决定是否开启集群模式

Cluster meet命令的实现

客户端向节点A发送命令,可以让节点A将命令中的节点B添加到节点A所在的集群,过程如下:

  • 收到命令的节点A将与节点B进行握手,确认彼此的存在
  • 节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典中
  • 节点A根据命令中的ip和port向节点B发送MEET消息
  • 如果节点B成功接收到消息,也会为节点A创建一个clusterNode结构,并将结构添加到自己的clusterState.nodes字典中
  • 然后节点B返回PONG消息
  • 如果节点A成功接收到PONG消息,那么节点A知道节点B已经成功接收到了自己的MEET消息,然后又向节点B发送PING消息
  • 如果节点B接收到了节点A返回的PING消息,那么节点A就知道节点B已经接收到自己的PONG消息,完成握手
  • 之后节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B完成握手

槽指派

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

当数据库中的所有槽都有节点在处理时,集群处于上线状态(ok);反之,如果有槽没有被节点处理(即使只有1个槽没有被处理),集群也处于下线状态(fail)

通过CLUSTER ADDSLOTS命令,我们可以将一个或者多个槽指派(assign)给节点负责

记录节点的槽指派信息

clusterNode结构的slots属性和numslots属性记录了节点负责处理那些槽

slots

slots属性是一个二进制位数组,长度为16384bit / 8 = 2048字节

通过对slots数组索引i上的二进制位的值来判断节点是否负责处理槽i:

  • 如果slots数组索引i上的值为1,那么表示当前节点处理槽i
  • 如果slots数组索引i上的值为0,那么表示当前节点不处理槽i
numslots

记录了节点负责处理的槽的数量

传播节点的槽指派信息

节点会将自己的slots数组发送给集群中的其他节点,来告知自己处理了哪些节点

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

clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息,slots数组的每一个元素都是一个指向clusterNode结构的指针,如果某个元素指向NULL,那么表示该槽没有被节点处理

在集群中执行命令

当客户端向节点发送与数据库键相关的命令时,接收命令的节点会计算出命令要处理的数据库属于哪个槽,并检查这个槽是否指派给了自己

  • 如果是,那么节点就执行这个命令
  • 如果否,那么节点就会向客户端返回MOVED命令,指引客户端redirect至正确的节点,并再次发送之前想要执行的命令

计算键属于哪个槽

通过CRC16(key) & 16383来计算键key的槽号,其中:

  • CRC16(key)用于计算key的CRC-16校验和
  • & 16383用于计算出一个[0,16383]之间的槽号

可以使用命令cluster keyslot <key>查看键key属于哪个槽

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

计算出键所属的槽i之后,节点就会检查自己在clusterState.slots数组中的项i,判断键所在的槽是否由自己负责处理:

  • 如果clusterState.slots[i]等于clusterState.myself,那么表示当前槽由当前节点负责,节点可以执行客户端发送的命令
  • 如果clusterState.slots[i]不等于clusterState.myself,那么说明槽i并非由当前节点负责,那么节点会向客户端返回MOVED错误,并redirect到处理槽i的节点

MOVED错误

MOVED <slot> <ip>:<port>,其中

  • slot:键所在的槽
  • ip:负责该槽的节点的ip
  • port:负责该槽的节点的port

节点数据库的实现

节点只能使用0号数据库,而单机Redis服务器可以使用任何数据库

重新分片

重新分片指的是Redis集群可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),相关槽所属的键值对也会从源节点移动到目标节点

重新分片操作可以在线进行,并且源节点和目标节点都可以正常处理命令请求

重新分片实现原理

重新分片操作由redis-trib负责执行,重新分片的步骤如下:

  • redis-trib对目标节点发送cluster setslot <slot> importing <source_id>命令,让目标节点准备好从源节点导入属于槽slot的键值对
  • redis-trib对源节点发送cluster setslot <slot> migrating <target_id>命令,让源节点准备好属于槽slot的键值对迁移至目标节点
  • redis-trib向源节点发送cluster getkeysinslot <slot> <count>命令,获得最多count个属于槽slot的键值对的键名
  • 对于上一步骤获取到的每个键名,redis-trib都向源节点发送一个migrate <target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键原子的从源节点迁移至目标节点
  • 重复执行前两个步骤,直到槽slot的所有键值对迁移完成
  • redis-trib向集群中的任意一个节点发送cluster setslot <slot> node <target_id>命令,将槽slot指派给目标节点,这个消息会发送给整个集群,其他节点都会知道槽slot已经指派给了目标节点

ASK错误

在重新分片期间,客户端向源节点发送一个数据库键相关的命令,并且该键恰好属于正在迁移的槽时:

  • 源节点会现在自己的数据库里查找指定的键,如果找到的话,就直接执行客户端命令
  • 如果没有找到,那么表示该键已经被迁移到目标节点中了,那么源节点将向客户端返回一个ASK错误,指引客户端redirect到目标节点,并再次发送之前想要执行的命令

cluster setslot importing命令的实现

clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽,如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在从clusterNode所代表的节点导入槽i

在对集群进行重新分片时,向目标节点发送cluster setslot <i> importing <source_id>命令可以将目标节点的clusterState.importing_slots_from[i]设置为source_id所代表的节点的clusterNode结构

cluster setslot migrating命令的实现

clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽,如果migrating_slots_to[i]的值不为NULL,而是指向一个clusterNode结构,那么表示当前节点正在将槽i迁移至clusterNode所代表的节点

在对集群进行重新分片时,向源节点发送cluster setslot <i> migrating <target_id>命令可以将目标节点的clusterState.migrating_slots_to[i]设置为target_id所代表的节点的clusterNode结构

ASK错误

如果节点接收到关于键key的命令请求,同时该键所属的槽i正好指派给了这个节点,那么节点会尝试在自己的数据库中查找键key,如果找到了,那么执行命令;如果没有找到,那么节点会检查自己的clusterState.migrating_slots_to[i],查看键是否在迁移,如果正在迁移,那么就会向客户端发送一个ASK错误,引导客户端到正在导入槽i的节点去查找键key

接收到ASK错误的客户端会根据错误提供的IP和端口号redirect至正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,然后再发送想要执行的命令

ASKING命令

用于打开REDIS_ASKING标识

复制与故障转移

Redis集群中分为主节点和从节点,主节点用于处理槽,从节点用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点处理命令请求

设置从节点

客户端通过命令cluster replicate <node_id>让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制

  • 接收到命令的节点会在自己的clusterState.nodes字典中寻找与node_id相对应的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,用于记录这个节点正在复制的主节点
  • 然后节点会修改自己在clusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示这个节点已经从原来的主节点变成了从节点
  • 最后,节点调用复制代码,并根据clusterState.myself.slaveof指向的clusterNode结构所保存的IP和PORT,对主节点进行复制

当一个节点变为主节点,并开始复制某个主节点的信息会通过消息发送给集群中的其他节点,其他节点会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制的这个主节点的从节点名单

故障检测

集群中的每个节点都会定期的向急群众的其他节点发送PING消息,以此来检测对象是否在线,如果接受PING消息的节点在规定时间内没有向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(在自己的clusterState.nodes中找到对应节点的clusterNode结构,并在结构的flags属性中打开REDIS_NODE_PFAIL标识,表示该节点疑似下线)

集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,包括节点是否处于在线、疑似下线或者下线状态

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

如果一个集群中超过半数以上负责槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL)状态,并向集群广播一条关于主节点x已下线的消息,所有收到这条消息的节点都会将主节点x标记为已下线

故障转移

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

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

如何选举新的主节点

  • 集群中有一个配置纪元,它是一个自增计数器,初始值为0
  • 当集群里的某个节点开始一次故障转移操作时,配置纪元加1
  • 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票
  • 当从节点发现自己正在复制的主节点已经进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有接受到该消息的并且具有投票权的主节点为这个从节点进行投票
  • 如果一个主节点具有投票权(即它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条cluster_type_failover_auth_ack消息,表示这个主节点支持该从节点成为新的主节点
  • 每个参与选举的从节点都会接收cluster_type_failover_auth_ack消息,并根据自己受到了多少条这类消息来统计自己获得了多少主节点的支持
  • 如果集群里半数+1的具有投票权的主节点都将票投给了某个从节点,那么这个从节点就会成为新的主节点
  • 在每个配置纪元中,每个具有投票权的主节点都只能投一次票
  • 如果在当前配置纪元中没有从节点能接收到足够多的票,那么配置纪元加1,重新举行选举,直到选出新的主节点为止

消息

集群中的各个节点通过发送和接收消息来进行通信,我们称发送消息的节点为发送者(sender),接收消息的节点为接收者(receiver)

消息种类主要有5种:

  1. MEET:用于将目标节点(MEET命令后面跟着的IP和port对应的节点)加入当前节点(当前连接的节点)的集群中

  2. PING:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。

    如果节点A最后一次收到节点B发送的PONG消息的时间已经超过了节点A的cluster-node-timeout时长的一半,那么节点A也会向节点B发送PING消息,防止节点B因为长时间没有被选中而导致节点A对节点B的消息滞后

  3. PONG:当接收者收到发送者发来的MEET消息或者PING消息时,会返回一条PONG消息给发送者,表明自己已经接收到消息了

    一个节点也可以通过向集群广播自己的PONG消息来让集群中其他节点立即刷新关于这个节点的认识,例如当故障转移完成后,新的主节点就会向集群广播一条PONG消息,其他收到该消息的节点会知道这个节点已经由从节点变为主节点并且已经接管了已下线主节点负责的槽了

  4. FAIL:当一个节点A判断另一个节点B已经进入FAIL状态时,节点A会向集群广播节点B已经FAIL的消息,那么所有收到这条消息的节点都会将节点B标记为FAIL

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