写点什么

Redis 集群架构剖析 (4):槽位迁移,重新分配

作者:非晓为骁
  • 2022 年 4 月 02 日
  • 本文字数:3203 字

    阅读完需:约 11 分钟

在前一篇 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,这个在后面会有用处。



  1. 首先 redis-trib 对目标节点发送指令,让目标节点准备好从源节点导入属于槽 slot 的键值对,指令如下:

  2. CLUSTER SETSLOT <slot> IMPORTING <source_id>

  3. 然后 redis-trib 对源节点发送指令,让源节点准备好将属于 slot 的键值对迁移到目标节点,指令如下:

  4. CLUSTER SETSLOT <slot> MIGRATING <target_id>

  5. 这时候因为源节点收到了命令,要准备将 slot 的键值对迁移给目标节点。但不是所有要迁移的 slot 上都已经存储了键值对,所以接着

  6. 如果这个 slot 上面有存储键值对的话,redis-trib 会向源节点发送指令,获得最多 count 个属于槽 slot 的键值对的键名,指令如下:CLUSTER GETKEYSINSLOT <slot> <count> 。接着 redis-trib 对每个键名,向源节点发送MIGRATE <target_ip> <target_port> <key_name> 0 <timeout> 命令,将被选中的键值对,从源节点迁移到目标节点

  7. 如果这个 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 数组记录了当前节点正在从其他节点导入的槽:


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



CLUSTER SETSLOT MIGRATING

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 会变成如下图所示:



ASKING

在前面了解到如果请求的命令对应的键不在源节点上,在迁移的目标节点上,源节点就会返回一个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 标识就移除了。

ASK 错误和 MOVED 错误

这两个错误都会导致客户端转向,那他们区别如下:


  • MOVED错误代表槽的所属节点已经从一个节点转移到另一个节点,客户端每次收到MOVED时都会直接将请求发送给指向的节点

  • ASKING错误只是两个节点在迁移槽的过程中使用的一种零时措施。




这篇文档,了解到节点发生槽转移时,集群是如何处理重新分配的,数据结构又是如何存储的。这个是针对数据的一种异常情况,还有一个是针对节点的异常,比如说我部署的 redis 节点挂掉了,原本存的槽即使知道导向这个节点,但这个节点也没有回复的能力了。那我们该怎么做呢?是不是该备份一下这个数据呢?似乎就是我挂了,你顶我。针对这个一个异常行为,我们下节分析。




系列文章:

  1. Redis 集群架构剖析 (1):认识 cluster

  2. Redis 集群架构剖析 (2):槽位

  3. Redis集群架构剖析(3):集群处理redis-cli指令


    用户头像

    非晓为骁

    关注

    no pain no gain 2019.04.10 加入

    用我的勤奋,一点一点地努力,提升自己的能力,拓展自己的视野,提高自己的认知。 我的知乎:https://www.zhihu.com/people/zhengfke

    评论

    发布
    暂无评论
    Redis集群架构剖析(4):槽位迁移,重新分配_redis_非晓为骁_InfoQ写作平台