前言
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
实际上,切片集群是一种保存大量数据的通用机制,这个机制可以有不同的实现方案。在 Redis 3.0 之前,官方并没有针对切片集群提供具体的方案。从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群。Redis Cluster 方案中就规定了数据和实例的对应规则。本文以Redis 3.0 之后官方提供Redis Cluster为基础。
Redis集群作用
- 解决单个节点数据量大、写入量大产生的性能瓶颈问题。
- 通过分片可以让更多的服务器承担写请求的压力。
知识点汇总
Redis集群整体架构图
实例
一个Redis集群通常由多个实例组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个实例的集群。
构建集群的命令
CLUSTER MEET <ip> <port>
例如: 实例1将实例2加入到集群
CLUSTER MEET 127.0.0.1 7002
CLUSTER MEET命令的实现
以实例1将实例2加入到集群为例说明,收到命令的实例A将与实例B进行握手(handshake),以此来确认彼此的存在,并为将来的进一步通信打好基础:
- 实例1会为实例2创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
- 之后,实例1将根据CLUSTER MEET命令给定的IP地址和端口号,向实例2发送一条MEET消息(message)。
- 实例2将接收到节点A发送的MEET消息,实例2会为实例1创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
- 实例2将向实例1返回一条PONG消息。
- 实例1将接收到实例2返回的PONG消息,通过这条PONG消息实例1可以知道实例2已经成功地接收到了自己发送的MEET消息。
- 实例1将向实例2返回一条PING消息。
- 实例2将接收到实例1返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功地接收到了自己返回的PONG消息,握手完成。
握手过程如下图
说明:clusterNode结构保存了一个实例的当前状态,比如实例的创建时间、实例的名字、实例的IP地址和端口号等等。每个实例都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他实例(包括主实例和从实例)都创建一个相应的clusterNode结构,以此来记录其他实例的状态
槽指派
给实例分配哈希槽
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。
通过向节点发送CLUSTER ADDSLOTS命令,可以将一个或多个槽指派(assign)给节点负责, 命令如下:
CLUSTER ADDSLOTS <slot> [slot ...]
例如:将槽0至槽5000指派给实例1负责, 在实例1上执行以下命令:
CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
同理:将槽5001至槽10000指派给实例2负责, 在实例2上执行以下命令:
CLUSTER ADDSLOTS 5001 5002 ... 10000
同理:将槽10001至槽16383指派给实例3负责, 在实例3上执行以下命令:
CLUSTER ADDSLOTS 10001 10002 ... 16383
当以上三个CLUSTER ADDSLOTS命令都执行完毕之后,数据库中的16384个槽都已经被指派给了相应的节点,集群进入上线状态。
槽指派信息的传播
一般来说,客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的。那么,客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?
答案:这是因为Redis实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散(Gossip协议)。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。
客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
集群中命令的执行
客户端发送数据库键命令流程
- 当客户端向实例发送与数据库键有关的命令时,接收命令的实例会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己。
- 如果键所在的槽正好就指派给了当前实例,那么实例直接执行这个命令。
- 如果键所在的槽并没有指派给当前实例,那么实例会向客户端返回一个MOVED错误,指引客户端转向(redirect)至正确的实例,并再次发送之前想要执行的命令。
- 客户端发送数据库键命令流程图如下:
重定向
重定向是指客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,实例就会向客
户端返回一个MOVED错误,指引客户端转向至正在负责槽的实例。
MOVED的格式为:
MOVED <slot> <ip>:<port>
其中slot为键所在的槽,而ip和port则是负责处理槽slot的节点的IP地址和端口号。
示例:
MOVED 10086 127.0.0.1:7003
表示槽10086正由IP地址为127.0.0.1,端口号为7003的节点负责。
重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
例如:对于之前的包含7001、7002、7003三个实例的集群来说,向这个集群添加一个IP为127.0.0.1,端口号为7004的实例,然后通过重新分片操作,将原本指派给节点7003的槽15001至16383改为指派给节点7004。
重新分片的实现原理
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib对集群的单个槽slot进行重新分片的步骤如下:
- redis-trib对目标节点发送CLUSTERSETSLOTIMPORTING<source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
- redis-trib对源节点发送CLUSTERSETSLOTMIGRATING<target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点。
- redis-trib向源节点发送CLUSTER GETKEYSINSLOT命令,获得最多count个属于槽slot的键值对的键名(keyname)。
- 对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE<target_ip><target_port><key_name>0命令,将被选中的键原子地从源节点迁移至目标节点。
- 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。每次迁移的过程如下图
- redis-trib向集群中的任意一个节点发送CLUSTERSETSLOTNODE<target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。
步骤5,每次迁移过程图:
重新分片流程图:
ASK错误
ASK的执行
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
- 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
- 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
- 客户端会先向目标节点发送一个ASKING命令,然后才重新发送想要执行的命令。这是因为如果客户端不发送ASKING命令,而直接发送想要执行的命令的话,那么客户端发送的命令将被节点拒绝执行,并返回MOVED错误。
说明:ASKING 命令让这个实例允许执行客户端接下来发送的命令
判断是否发送ASK错误的过程图
MOVED和ASK区别
MOVED | ASK | |
---|---|---|
是否更新客户端缓存的哈希槽分配信息 | 是 | 否 |
槽的负责权是否已经从一个节点转移到了另一个节点 | 是 | 否 |
消息
集群中的各个节点通过发送和接收消息(message)来进行通信,我们称发送消息的节点为发送者(sender),接收消息的节点为接收者。
节点发送的消息主要有以下五种:
- MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
- PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
- PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
- FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
- PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。