redis设计与实现读书笔记(五)

2021-01-22  本文已影响0人  peareaden

本文内容为《redis设计与实现》一书学习笔记。本文主要概述十七章内容。

第十七章 集群

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

17.1 节点

一个Redis集群通常由多个节点(node)组成,连接各个节点的工作可以使用CLUSTER MEET命令来完成:

CLUSTER MEET [ip] [port]

向一个节点node发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。

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

集群模式的Redis服务器依然会继续使用单机模式下的Redis服务器组件,比如复制等功能。至于那些只有在集群模式下才会用到的数据,节点将它们保存到了clusterNode结构、clusterLink结构、clusterState结构。

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

clusterNode结构的link属性如下图所示:

每个节点都保存着一个clusterState,记录着当前节点的视角下集群目前所处的状态。

以包含三个节点127.0.0.1:7000、127.0.0.1:7001、127.0.0.1:7002的集群为例,节点127.0.0.1:7000创建的clusterState结构如下:

CLUSTER MEET命令的实现
通过向节点A发送CLUSTER MEET命令,可以让节点A将另外一个节点B添加到节点A当前所在的集群里面,收到命令的节点A将于节点B进行握手,以此来确认彼此的存在。握手过程如下:

  1. 节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面;
  2. 节点A将给节点B发送一条MEET消息;
  3. 节点B接收到节点A发送的MEET消息,B会为A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面;
  4. 节点B将给节点A返回一条PONG消息;
  5. 节点A收到B的PONG消息,A就知道B已经成功的接收到自己的消息了;
  6. 节点A将给B发送一条PING消息;
  7. 节点B收到A的PING消息,B就知道A已经接收到自返回的PONG消息,握手结束;

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

17.2 槽指派

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384(2^14)个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。 当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。

通过向节点发送CLUSTER ADDSLOTS命令,可以将一个或多个槽指派(assign)给节点负责。 还是上面的例子,可以执行以下命令将槽0至槽5000指派给节点7000负责:

127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
OK

将槽5001至槽10000指派给节点7001负责:

127.0.0.1:7001> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000
OK

将槽10001至槽16383指派给7002负责:

127.0.0.1:7002> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383
OK

当以上三个CLUSTER ADDSLOTS命令都执行完毕之后,数据库中的16384个槽都已经被指派给了相应的节点,集群进入上线状态:

127.0.0.1:7000> CLUSTER INFO
cluster_state:ok
...

17.2.1 记录节点的槽指派信息

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

slots属性是一个二进制位数组,这个数组的长度是16384 / 8 = 2048个字节,一共包含16384个二进制位。以0为起始索引,16383为终止索引,并根据第i位上的二进制值来判断槽是否是该节点负责,以下图为例,该节点负责槽0至槽7。

因为取出和设置slots数组中任意二进制位的复杂度仅为O(1),所以对于给定的slots数组来说,程序检查节点是否负责处理某个槽,或者将某个槽指派给节点负责,这两个动作的复杂度都是O(1)

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

17.2.2 传播节点的槽指派信息

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

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

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

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

clusterState.slots数组记录集群中所有槽指派信息,clusterNode.slots数组记录clusterNode结构所代表的节点的槽指派信息。

为什么不可以只将槽指派信息保存在各个节点的clusterNode.slots里,而需要存在clusterState.slots数组里呢 ?

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

17.3 在集群中执行命令

在对集群中的16384个槽都指派完成之后,集群就进入上线状态,这时客户端就可以向集群发送数据命令了。
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:

判断客户端是否需要转向的流程如下:

17.3.1 计算槽属于哪个键

节点首先需要计算出键所对应的槽,Redis使用CRC16(key) & 16383来计算这个值

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

节点计算出键所属的槽i后,检查clusterState.slots[i] 是否等于 clusterState.myself,从而判断键所在槽是否由自己处理,等于则自己处理,不等则根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点。

17.3.3 MOVED错误

当客户端接收到节点返回的MOVED错误时,客户端会根据MOVED错误中提供的IP地址和端口号,转向至负责处理槽slot的节点,并向该节点重新发送之前想要执行的命令。

集群模式的redis-cli客户端在接收到MOVED错误时,并不会打印出MOVED错误,而是根据MOVED错误自动进行节点转向,并打印出转向信息,所以看不见节点返回的MOVED错误。而单机(stand alone)模式的redis-cli客户端,会打印MOVED错误。这是因为单机模式的redis-cli客户端不清楚MOVED错误的作用,只会直接将MOVED错误打印出来,不会自动转向。

17.3.4 节点数据库的实现

需要注意的是,节点只能使用0号数据库,而单机则没有这个限制。

17.4 重新分片

Redis集群的重新分片操作可以将任意已经分配给某个节点的槽改为指派给其他的另外一个节点,且相关槽所属的键值对也会从源节点移动到目标节点。
重新分片操作可以在线进行,集群不需要下线,源节点和目标节点都可以继续处理命令请求;
Redis集群的重新分片操作由Redis的集群管理软件redis-trib执行,redis-trib对集群的单个槽slot进行重新分片的步骤如下:

  1. redis-trib向目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,让目标节点准备好从源节点导入属于槽slot的键值对;
  2. redis-trib对源节点发送CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移到目标节点;
  3. redis-trib向源节点发送CLUSTER GETKEYSINSLOT <slot> <count>命令,获取最多count个属于槽slot的键值对的键名字;
  4. 对于步骤3得到的每个键,redis-trib都将向源节点发送命令,原子的将这些键迁移到目标节点;
  5. 重复步骤3和4,直到源节点保存的所有属于槽slot的键值对都被迁移到目标节点为止;
  6. redis-trib会向集群中的任意一个节点发送CLUSTER SETSLOT <slot> NODE <taregt_id>命令,将槽slot指派给目标节点,这一信息会通过消息发送给集群中的所有节点,最终集群中的所有节点都将知道槽slot已经指派给了目标节点;

整个过程如下图所示:

17.5 ASK错误

在进行重新分片的过程中,可能属于被迁移槽的一部分键在源节点中,而另一部分已经迁移到目标节点。这时候如果客户端向源节点发送一个数据库操作命令,而这个被操作的键刚好在被迁移的键中时:

当客户端接收到ASK错误并转向至正在导入槽的节点时,客户端会先向节点发送一个ASKING命令,然后才重新发送想要执行的命令。原因:如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,客户端发送的命令将被节点拒绝执行,并返回MOVED错误。以将节点7002中的槽16198导入节点7003为例,虽然节点7003正在导入槽16198,但槽16198目前还是指派给节点7002的,所以如果直接发送GET命令,节点7003会向客户端返回MOVED错误,并指引客户端转向至节点7002;但如果在发送GET命令前,先向节点7003发送一个ASKING命令,GET命令就会被节点7003执行。

ASK错误和MOVED错误的区别:

17.6 复制和故障转移

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

17.6.1 设置从节点

向一个节点发送命令:

CLUSTER REPLICATE <node_id>

可以让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制。

17.6.2 故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail,PFAIL)。
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

17.6.3 故障转移

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

17.6.4 选举新的主节点

与16章选举领头Sentinel的方法类似,两者都是基于Raft算法的领头选举(leader election)方法实现的。

17.7 消息

集群中的各个节点通过发送和接收消息(message)来进行通信。节点发送的消息主要有以下五种:

17.7.2 MEET、PING、PONG命令的实现

Redis集群的各个节点通过Gossip协议来交换各自关于不同结点的状态信息,Gossip协议由 MEET、PING、PONG三种消息实现。这三种消息使用相同的消息正文(正文由两个clusterMsgDataGossip结构组成),通过消息头的type属性进行区分。
每次发送MEET、PING、PONG命令时,发送者都从自己的已知节点列表里随机选出两个节点(可以是主节点也可以是从节点),并将这两个被选中的节点的信息分别保存到两个clusterMsgDataGossip结构里。
当接收者收到MEET、PING、PONG命令时,接收者会访问消息正文中的两个clusterMsgDataGossip结构,并根据自己是否认识这两个节点选择操作:

17.7.3 FAIL消息的实现

FAIL消息可见17.6.2
在集群节点数量较大时,使用Gossip协议来传播节点的已下线信息会给节点的信息更新带来一定延迟,因为Gossip协议消息通常需要一段时间才能传播至整个集群,而发送FAIL信息可以让集群里的所有节点立即知道某个主节点已下线,从而尽快判断是否需要将集群标记为下线或者对下线主节点进行故障转移。

17.7.4 PUBLISH消息的实现

上一篇 下一篇

猜你喜欢

热点阅读