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