在前一篇Redis集群架构剖析中,我们了解了一个集群如何处理一个由redis-cli发来的指令,但是都是在cluster槽位不变的情况下。那为什么槽位会变呢?集群有可能增删节点,在第二篇的时候,我们知道只有所有节点都分配到槽位的时候,redis cluster在是online状态。在开始之前,依旧可以先思考下面的问题:
先不卖关子,集群在重新分配的过程中,不需要下线,并且源节点和目标节点都可以继续处理命令请求。下面我们来看下redis是如何实现的。
重新分配的操作就是将任意数量已经指派给某个节点(源节点)的槽位改指派给另一个节点(目标节点),并且相关槽位所属的键值对也会从源节点移动到目标节点。
举个例子,下图原本由6370,6371和6372组成的集群,现在加入一个新的节点6373。那么原本分配给6372的槽位1000116383,就将其中的1500116383槽位重新分配给节点6373。重新分配的动作在CLUSTER MEET这个6373节点的时候就做完了。
Redis cluster的重新分配操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供进行重新分配所需的命令,redis-trib则通过向源节点和目标节点发送指令来进行重新分配的操作。
下图是对一个槽位重新分配的一个流程,值得注意的是里面的第三和第四步,先迁移value再迁移key,这个在后面会有用处。
首先redis-trib对目标节点发送指令,让目标节点准备好从源节点导入属于槽slot的键值对,指令如下:
CLUSTER SETSLOT
然后redis-trib对源节点发送指令,让源节点准备好将属于slot的键值对迁移到目标节点,指令如下:
CLUSTER SETSLOT
这时候因为源节点收到了命令,要准备将slot的键值对迁移给目标节点。但不是所有要迁移的slot上都已经存储了键值对,所以接着
如果这个slot上面有存储键值对的话,redis-trib会向源节点发送指令,获得最多count个属于槽slot的键值对的键名,指令如下:CLUSTER GETKEYSINSLOT
。接着redis-trib对每个键名,向源节点发送MIGRATE
命令,将被选中的键值对,从源节点迁移到目标节点
如果这个slot不存在键值对,或者经过了步骤4,那么redis-trib会向集群中的任意一个节点发送CLUSTER SETSLOT
的命令。将槽slot指派给目标节点的信息,发送至整个集群,最终集群终端中的所有节点都会知道槽slot已经指派给了目标节点。
如果这个slot有存储多个键值对,就会重复执行步骤4里面的第二个指令和步骤5。
在迁移过程中,很有可能有redis-cli发请求过来请求数据,这个时候应该怎么做呢?可以联想一下上一篇,如果请求到不是本节点的槽位,节点会告诉redis-cli应该去哪个节点找到对应的槽位,这个思路是否也可以借鉴呢?其实这个问题,在我们设计分布式系统的时候还是很重要的,要想到这种特殊的情况,要嘛是直接禁止访问,要不然就是设计一个机制,可以让迁移和请求同时存在。显然,redis选择了后者。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
MOVED
很像) 下图就是节点收到请求后是否要发送ASK
的流程图
这个ASK
和MOVED一样的返回,也是返回Redirected
到某个节点,如果需要看到ASK
错误的话,得用单机redis请求。
在细究ASK的实现细节前,我们先看下cluster是用什么数据结构来记录,那些槽位在源节点,哪些又正在迁移到目标节点。
在重新分配的实现过程中,我们知道最开始有两个动作,分别是目标节点准备导入槽,源节点准备将槽导出,这设计到两个指令,分别也对应着两个数据结构
clusterState结构的importing_slots_from数组记录了当前节点正在从其他节点导入的槽:
typedef struct clusterState {
//...
clusterNode *importing_slots_from[16384];
//...
} clusterState;
如果importing_slots_from[i]不为NULL,而是指向一个clusterNode结构,那么表示正在从这个clusterNode节点导入槽i。
举个例子,加入6373加入集群,然后将6372上的15002重新分配给6373,会执行CLUSTER SETSLOT 15002 IMPORTING 6372的节点ID
那么6373的importing_slots_from就会变成下图这样,也就是重新分配实现过程的第一步,6373的importing_slots_from[15002]会指向节点6372
clusterState结构的migrating_slots_to数组记录了点前节点正在迁移至其他节点的槽:
typedef struct clusterState{
//...
clusterNode *migrating_slots_to[16384];
//...
} clusterState;
如果migrating_slots_to[i]不为NULL,而是指向一个clusterNode结构,那么表示正在导入到这个clusterNode节点。
举个例子,接着上面的importing,到了重新分配实现过程的第二步,给6372发送指令CLUSTER SETSLOT 15002 MIGRATING 6373的节点ID
,那么6372的migrating_slots_to会变成如下图所示:
在前面了解到如果请求的命令对应的键不在源节点上,在迁移的目标节点上,源节点就会返回一个ASK
错误。接到ASK
错误的客户端就会根据错误提供的IP地址和端口号,转向正在导入槽的目标节点,然后首先会想目标节点发送一个ASKING
命令,之后才会再重新发送原本想要执行的命令。下图是一个简单的转向后,请求ASKING
的示意图。
ASKING
命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING
标识,以下是这个命令实现的伪代码:
def ASKING();
// 打开标识
client.flags != REDIS_ASKING
// 向客户端返回OK
reply("OK")
回想一下,之前槽位不存在请求节点的时候,节点会向客户端返回一个MOVED
错误。但是,如果节点的clusterState.importing_slots_from[i]显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING
标识,那么节点将魄力执行这个关于槽i的命令一次,看一下流程图:
当客户端接收到ASK错误并转向到正在导入槽的节点时,客户端会先向节点发送一个ASKING
命令,然后才重新发送想要执行的命令,这是因为如果客户端不发送ASKING
命令,而直接发送想要执行的命令的话,客户端发送的命令将被节点拒绝执行,并返回MOVED
错误。
举个例子,在上面的例子中,我们向6373节点请求15002槽,因为15002是在导入槽,所以如果我们没有发送一个ASKING
的命令,6373会返回一个MOVED
的错误,并转到6372,因为槽15002还分配在6372上。如果在请求之前,发送了ASKING
命令,那么6373就会执行这个命令。
注意:REDIS_ASKING标识是一次性标识,当节点执行了一个带有REDIS_ASKING标识的客户端发送的命令之后,客户端的REDIS_ASKING标识就移除了。
这两个错误都会导致客户端转向,那他们区别如下:
MOVED
错误代表槽的所属节点已经从一个节点转移到另一个节点,客户端每次收到MOVED
时都会直接将请求发送给指向的节点ASKING
错误只是两个节点在迁移槽的过程中使用的一种零时措施。这篇文档,了解到节点发生槽转移时,集群是如何处理重新分配的,数据结构又是如何存储的。这个是针对数据的一种异常情况,还有一个是针对节点的异常,比如说我部署的redis节点挂掉了,原本存的槽即使知道导向这个节点,但这个节点也没有回复的能力了。那我们该怎么做呢?是不是该备份一下这个数据呢?似乎就是我挂了,你顶我。针对这个一个异常行为,我们下节分析。
系列文章