Redis扩缩容演进史与奇思妙想

Redis 的扩缩容方案在 RedisCluster 中发生了很多的改造与优化,其中主要包括对于 Slot 和 Keys 映射关系的优化,从最初的跳表,到基数树,再到最新的柔性数组的相关优化。同时 Redis 的非社区Cluster 模式下的扩缩容在业界在诞生了很多有意思的设计思路,比如 Codis 提供的同步/异步迁移方案,选择性复制以及旁路扩缩容的迁移方案等。这篇文章将简略的描述一下当前业界实现的 Redis 的扩缩容方案。

一、简介

这里主要描述的是 Redis 的横向扩缩容。

二、Redis 扩缩容演进史

2.1、映射关系存储结构演进

由于我们需要能够高效的依据 Slot 来找到对应的 Keys 信息,从而实现数据的迁移,因此我们需要记录 Slot 和 Keys 的映射关系,Redis 也在不断的优化这种存储结构,从而在实现高效遍历的时候又能够节省存储所带来的内存开销。

2.1.1、SkipList 存储结构

  • 版本范围:3.0.0 ~ 3.2.13(以下分析基于 3.2.13 版本)

  • 设计特点:

    • 使用一个全局结构体变量( server.cluster->slots_to_keys )来记录 Slot 和 Keys 的映射关系(集群模式下仅允许 DB-0 );
    • 存储方式:
      • Score : Key 的 SlotID ;
      • Value :Key 的 robj 指针;
  • 数据变更流程:

    • 新增:当数据被写入 DB 之后,就会调用 slotToKeyAdd 函数将数据在额外存储在 slots_to_keys 中 ,时间复杂度 O(logN);
    • 变更:由于 slots_to_keys 中记录的是 Key 的信息,因此如果只是 Key 的 Value 变化了, slots_to_keys 中的信息保持不变;
    • 删除:当数据被从 DB 中删除之后,就会调用 slotToKeyDel 函数将数据也从 slots_to_keys 删除 ,时间复杂度 O(logN);
    • 查找:每次查找的过程相当于是依据 SlotID 在 slots_to_keys 中查找对应的 Keys ,时间复杂度 O(logN);
  • 数据结构:

    // server.cluster 全局结构体
    typedef struct clusterState {
    ...

    zskiplist *slots_to_keys; // 跳表的方式记录Slot和Keys的映射关系

    ...
    } clusterState;

    // 存储映射关系的跳表
    typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
    } zskiplist;

    // 跳表内部的节点结构
    typedef struct zskiplistNode {
    robj *obj;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
    struct zskiplistNode *forward;
    unsigned int span;
    } level[];
    } zskiplistNode;

2.1.2、RadixTree 存储结构

  • 版本范围:4.0.0 ~ 6.2.7(以下分析基于 6.2.7 版本)

  • 代码记录:commit / c4716d33459199a768e0cb40f469671b778471bd

  • 设计特点:

    • 使用一个全局结构体变量( server.cluster->slots_to_keys )来记录 Slot 和 Keys 的映射关系(集群模式下仅允许 DB-0 );
    • 相比于使用跳表的方式更节省内存;
    • 存储方式:
      • Value 由两部分组成:
        • 前两个字节:分别是 SlotID >> 8 和 SlotID & 0xFF 的值;
        • 后部分字节:实际的 Key 的信息;
  • 数据变更流程:

    • 新增:当数据被写入 DB 之后,就会调用 slotToKeyAdd 函数将数据在额外存储在 slots_to_keys 中 ;
    • 变更:由于 slots_to_keys 中记录的是 Key 的信息,因此如果只是 Key 的 Value 变化了, slots_to_keys 中的信息保持不变;
    • 删除:当数据被从 DB 中删除之后,就会调用 slotToKeyDel 函数将数据也从 slots_to_keys 删除;
    • 查找:每次查找的过程相当于是依据 SlotID 在 slots_to_keys 中查找对应的 Keys;
  • 数据结构:

    // server.cluster 全局结构体
    typedef struct clusterState {
    ...

    rax *slots_to_keys; // 基数树的方式记录Slot和Keys的映射关系

    ...
    } clusterState;

    // 存储映射关系的基数树
    typedef struct rax {
    raxNode *head;
    uint64_t numele;
    uint64_t numnodes;
    } rax;

    // 基数树内部的节点结构
    typedef struct raxNode {
    uint32_t iskey:1; /* Does this node contain a key? */
    uint32_t isnull:1; /* Associated value is NULL (don't store it). */
    uint32_t iscompr:1; /* Node is compressed. */
    uint32_t size:29; /* Number of children, or compressed string len. */
    unsigned char data[];
    } raxNode;

2.1.3、DictEntryPtr 存储结构

  • 版本范围:7.0.0 ~ 7.0.5(写这篇文章时最新版本为 7.0.5 ,以下分析基于 7.0.5 版本)

  • 代码记录:pull / 9356

  • 设计特点:

    • 在 redisDb 的结构体中增加一个 16384 大小的数组来记录 Slot 和 Key 的映射关系,每个数组中的节点都记录了对应 Slot 中 Key 的 dictEntry 的指针地址;
    • 相比于使用基数树的方式更节省内存;
    • 写性能提升约 50%,读性能降低约 10% ;
    • 存储方式:
      • Value 由两部分组成:
        • 前两个字节:分别是 SlotID >> 8 和 SlotID & 0xFF 的值;
        • 后部分字节:实际的 Key 的信息;
  • 数据变更流程:

    • 新增:当数据被写入 DB 之后,就会调用 slotToKeyAddEntry 函数将数据所在 Entry 的指针地址插入对应 Slot数据的链表中,并将链表的头部第一个元素 head 设置为新插入的指针;
    • 变更:当由于碎片整理需要变更已有 Key 的 Entry 地址地址时,会调用 slotToKeyReplaceEntry 函数更新对应的链表节点信息,将该 Entry 移动到链表的头部;
    • 删除:当数据被从 DB 中删除之后,就会调用 slotToKeyDelEntry 函数将链表中记录的 Entry 节点删除;
    • 查找:每次查找的过程相当于是依据 SlotID 找到对应的数组索引,然后从链表的头部 head 开始遍历数据;
  • 数据结构:

    // server.db 全局结构体
    typedef struct redisDb {
    ...

    clusterSlotToKeyMapping *slots_to_keys; // Slot和Keys的映射数组
    } redisDb;

    // 存储映射关系的数组结构
    typedef struct clusterSlotToKeyMapping {
    slotToKeys by_slot[16384];
    } clusterSlotToKeyMapping;

    // 每一个Slot和Key的数组项
    typedef struct slotToKeys {
    uint64_t count; // Slot中Key的数量
    dictEntry *head; // 记录的第一个数据项的dictEntry指针地址
    } slotToKeys;


    // 改造后的dictEntry
    typedef struct dictEntry {
    void *key;
    union {
    void *val;
    uint64_t u64;
    int64_t s64;
    double d;
    } v;
    struct dictEntry *next; // 同一个哈希桶中的下一个节点
    void *metadata[]; // 柔性数组记录相同Slot的Key链表
    } dictEntry;

2.2、扩缩容流程演进

2.2.1、Redis 3系

  • 版本范围:3.0.0 ~ 3.2.13(以下分析基于 3.2.13 版本)
  • 版本特点:
    • 首次支持集群模式下的扩缩容;
    • 支持使用 redis-trib.rb 脚本实现集群初始化与扩缩容;
  • 扩容流程(假设从节点 A 中迁移数据到新节点 N):
    • 加入需要扩容的节点 N,命令: cluster meet N_ip N_port
    • 连接目标节点 N ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 A ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 A 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 A 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace] keys $key1 $key2 ... $keyN
    • 将源节点 A 上的所有对应 Slot 的数据全部迁移到目标节点 N 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
  • 缩容流程(假设从节点 N 中迁移数据到节点 A):
    • 连接目标节点 A ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 N ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 N 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 N 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace] keys $key1 $key2 ... $keyN
    • 将源节点 N 上的所有对应 Slot 的数据全部迁移到目标节点 A 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
    • 通知集群所有节点要删除的节点信息,命令: cluster forget $delNodeID

2.2.2、Redis 4系

  • 版本范围:3.0.0 ~ 4.0.14(以下分析基于 4.0.14 版本)
  • 版本特点:
    • 迁移命令支持密码认证;
  • 扩容流程(假设从节点 A 中迁移数据到新节点 N):
    • 加入需要扩容的节点 N,命令: cluster meet N_ip N_port
    • 连接目标节点 N ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 A ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 A 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 A 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password] keys $key1 $key2 ... $keyN
    • 将源节点 A 上的所有对应 Slot 的数据全部迁移到目标节点 N 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
  • 缩容流程(假设从节点 N 中迁移数据到节点 A):
    • 连接目标节点 A ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 N ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 N 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 N 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password] keys $key1 $key2 ... $keyN
    • 将源节点 N 上的所有对应 Slot 的数据全部迁移到目标节点 A 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
    • 通知集群所有节点要删除的节点信息,命令: cluster forget $delNodeID

2.2.3、Redis 5系

  • 版本范围:5.0.0 ~ 5.0.14(以下分析基于 5.0.14 版本)
  • 版本特点:
    • 使用 redis-cli 实现了 redis-trib.rb 脚本的功能,移除了 redis-trib.rb 脚本;
  • 扩容流程(假设从节点 A 中迁移数据到新节点 N):
    • 加入需要扩容的节点 N,命令: cluster meet N_ip N_port
    • 连接目标节点 N ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 A ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 A 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 A 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password] keys $key1 $key2 ... $keyN
    • 将源节点 A 上的所有对应 Slot 的数据全部迁移到目标节点 N 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
  • 缩容流程(假设从节点 N 中迁移数据到节点 A):
    • 连接目标节点 A ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 N ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 N 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 N 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password] keys $key1 $key2 ... $keyN
    • 将源节点 N 上的所有对应 Slot 的数据全部迁移到目标节点 A 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
    • 通知集群所有节点要删除的节点信息,命令: cluster forget $delNodeID

2.2.4、Redis 6系

  • 版本范围:6.0.0 ~ 6.2.7(以下分析基于 6.2.7 版本)
  • 版本特点:
    • 迁移命令支持 ACL 密码认证;
  • 扩容流程(假设从节点 A 中迁移数据到新节点 N):
    • 加入需要扩容的节点 N,命令: cluster meet N_ip N_port
    • 连接目标节点 N ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 A ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 A 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 A 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password|auth2 $username $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password|auth2 $username $password] keys $key1 $key2 ... $keyN
    • 将源节点 A 上的所有对应 Slot 的数据全部迁移到目标节点 N 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
  • 缩容流程(假设从节点 N 中迁移数据到节点 A):
    • 连接目标节点 A ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 N ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 N 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 N 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password|auth2 $username $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password|auth2 $username $password] keys $key1 $key2 ... $keyN
    • 将源节点 N 上的所有对应 Slot 的数据全部迁移到目标节点 A 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
    • 通知集群所有节点要删除的节点信息,命令: cluster forget $delNodeID

2.2.5、Redis 7系

  • 版本范围:7.0.0 ~ 7.0.5(写这篇文章时最新版本为 7.0.5 ,以下分析基于 7.0.5 版本)
  • 版本特点:
  • 扩容流程(假设从节点 A 中迁移数据到新节点 N):
    • 加入需要扩容的节点 N,命令: cluster meet N_ip N_port
    • 连接目标节点 N ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 A ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 A 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 A 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password|auth2 $username $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password|auth2 $username $password] keys $key1 $key2 ... $keyN
    • 将源节点 A 上的所有对应 Slot 的数据全部迁移到目标节点 N 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
  • 缩容流程(假设从节点 N 中迁移数据到节点 A):
    • 连接目标节点 A ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 N ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 N 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 N 上执行同步阻塞的数据迁移,支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|auth $password|auth2 $username $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|auth $password|auth2 $username $password] keys $key1 $key2 ... $keyN
    • 将源节点 N 上的所有对应 Slot 的数据全部迁移到目标节点 A 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID
    • 通知集群所有节点要删除的节点信息,命令: cluster forget $delNodeID

三、奇思妙想

3.1、Redis 异步扩缩容方案

  • Redis版本:基于社区版本 6.0.0 版本改造 ,版本地址

  • 方案特点:

    • Codis 作者(王乃峥)提供的迁移方案;
    • 基于异步线程实现的异步扩缩容方案;
    • 利用 RedisCluster 中提供的 Slot 和 Keys 的映射关系,与 Redis 6系版本中实现一致;
  • 异步扩容流程(假设从节点 A 中迁移数据到新节点 N ):

    • 加入需要扩容的节点 N,命令: cluster meet N_ip N_port
    • 连接目标节点 N ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID
    • 连接源节点 A ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID
    • 从源节点 A 获取待迁移数据,命令: cluster getkeysinslot $slotid $count
    • 在源节点 A 上执行异步的数据迁移(必须带有 async 参数),支持两种不同的命令格式:
      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|async|auth $password|auth2 $username $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|async|auth $password|auth2 $username $password] keys $key1 $key2 ... $keyN
    • 源节点 A 将迁移任务添加到异步队列中,等待迁移线程处理异步任务,同时将当前客户端 Block ;
    • 源节点 A 中的迁移线程将数据异步迁移到目标节点 N 中,等待执行结果;
    • 目标节点 N 接收到迁移命令,将其加入异步队列中,等待迁移线程处理异步任务,同时将当前客户端 Block ;
    • 目标节点 N 处理完成命令后,给源节点 A 回复迁移结果,然后取消 Block 迁移客户端;
    • 源节点 A 接收到迁移结果后,取消 Block 客户端,给发起迁移的客户端回复执行结果,完成迁移;
  • 异步缩容流程(假设从节点 N 中迁移数据到节点 A):

    • 连接目标节点 A ,设置 Slot 状态为 importing ,命令:cluster setslot $slotid importing $sourceNodeID

    • 连接源节点 N ,设置 Slot 状态为 migrating ,命令: cluster setslot $slotid migrating $targetNodeID

    • 从源节点 N 获取待迁移数据,命令: cluster getkeysinslot $slotid $count

    • 在源节点 N 上执行异步的数据迁移(必须带有 async 参数),支持两种不同的命令格式:

      • migrate $targetIP $targetPort $key $dbid $timeout [copy|replace|async|auth $password|auth2 $username $password]
      • migrate $targetIP $targetPort "" $dbid $timeout [copy|replace|async|auth $password|auth2 $username $password] keys $key1 $key2 ... $keyN
    • 源节点 N 将迁移任务添加到异步队列中,等待迁移线程处理异步任务,同时将当前客户端 Block ;

    • 源节点 N 中的迁移线程将数据异步迁移到目标节点 A 中,等待执行结果;

    • 目标节点 A 接收到迁移命令,将其加入异步队列中,等待迁移线程处理异步任务,同时将当前客户端 Block ;

    • 目标节点 A 处理完成命令后,给源节点 N 回复迁移结果,然后取消 Block 迁移客户端;

    • 源节点 A 接收到迁移结果后,取消 Block 客户端,给发起迁移的客户端回复执行结果;

    • 将源节点 N 上的所有对应 Slot 的数据全部迁移到目标节点 A 之后,设置迁移 Slot 的归属节点信息,理论上需要连接源节点和目标节点来设置,命令:cluster setslot $slotid node $targetNodeID

    • 通知集群所有节点要删除的节点信息,命令: cluster forget $delNodeID

基于异步线程的迁移流程图

  • 异步扩/缩容状态机:
    • PROCESS_STATE_NONE :
    • PROCESS_STATE_DONE :
    • PROCESS_STATE_QUEUED :

3.2、Codis 扩缩容方案

Codis 是豌豆荚推出的比较早期的一款 Redis 的集群方案,它不同于 RedisCluster 的去中心化的部署架构,它包含了元信息管控组件,Proxy ,以及 Redis 等。因此 Codis 实现的 Redis 扩缩容的方案业余社区的方案不同,这里主要介绍了 Codis 在扩缩容上实现的两种方案:同步和异步的方案。

3.2.1、Codis 同步扩缩容方案

  • Redis版本:2.8.21 , 3.2.4 , 3.2.8 , 3.2.11 (均为 Codis 定制版,以下分析基于 3.2.11 )

  • 方案特点:

    • 同步阻塞的扩缩容方案,会影响业务的正常读写请求;
  • Slot/Key映射关系:

    • db->hash_slots 中记录 Slot 和 Keys 的映射关系,字典中 Key/Value 的具体内容为:
      • Key : 实际的 Key 的值;
      • Vlaue : 实际 Key 的 CRC32 的值;
    • db->tagged_keys 中记录带有 Hashtag 的 Key 和其 CRC32 值的映射关系,其中具体的存储格式为:
      • Score : 对应 Key 的 CRC32 值;
      • Obj :对应 Key 的值;
  • 同步迁移命令:

    • slotsmgrtslot $targetIP $targetPort $timeout $slotID :获取特定 Slot 的所有key,依次序列化单个 KV 并通过 slotsrestore 命令同步阻塞发送给目标节点;

    • slotsmgrttagslot $targetIP $targetPort $timeout $slotID

  • 数据结构:

    // redisDb中存储Slot和Keys的映射关系
    typedef struct redisDb {
    ...

    dict *hash_slots[1024]; // 额外的字典记录Slot和Keys的映射关系
    int hash_slots_rehashing; // 标记hash_slots字典是否处于Rehashing状态
    struct zskiplist *tagged_keys; // 记录Key的CRC32值和Key的映射关系

    ...

    } redisDb;
  • 扩/缩容流程(假设从节点 A 迁移数据到节点 N ):

    • 连接源节点(节点 A ),执行特定的数据迁移命令;
    • 源节点(节点 A )依次序列化待迁移的 KV 数据并封装成 slotrestore 命令,同步阻塞的将数据迁移到目标节点(节点 N );
    • 目标节点(节点 N )同步执行完成 slotrestore 命令后,返回执行结果给源节点(节点 A );

Codis同步扩缩容的流程

3.2.2、Codis 异步扩缩容方案

  • Redis版本:3.2.8 , 3.2.11 (均为 Codis 定制版,以下分析基于 3.2.11 )

  • 方案特点:

    • 基于 Epoll 文件事件实现的异步扩缩容方案;
    • Codis 作者曾将该方案提交到 Redis 社区版本中( Pull/3997 ),后来由于方案过于复杂而被废弃;
  • Slot/Key映射关系:

    • db->hash_slots 中记录 Slot 和 Keys 的映射关系,字典中 Key/Value 的具体内容为:
      • Key : 实际的 Key 的值;
      • Vlaue : 实际 Key 的 CRC32 的值;
    • db->tagged_keys 中记录带有 Hashtag 的 Key 和其 CRC32 值的映射关系,其中具体的存储格式为:
      • Score : 对应 Key 的 CRC32 值;
      • Obj :对应 Key 的值;
  • 扩/缩容流程(假设从节点 A 迁移数据到节点 N ):

    • 连接源节点 A ,执行特定的数据迁移命令(其中包含很多自定义的参数,包括命令的最大字节,最大Bluk等);
    • 源节点 A 建立与目标节点 N 的连接,注册文件读写事件,并缓存该连接;
    • 源节点 A 根据迁移命令的参数,从本地 DB 中获取特定的待迁移的 Keys ;
    • 源节点 A 根据 Key 的特征(数据类型以及数据项的数量),按需进行序列化并异步迁移到目标节点 N ;
    • 目标节点 N 处理部分迁移命令之后,回复源节点 A 迁移的执行结果;
    • 源节点 A 在收到执行结果后,又会主动将剩余未迁移的 Keys (包括之前拆分的 Keys )迁移到目标节点 N ,直到单次任务中的 Keys 全部迁移完成;

Codis异步扩缩容的流程

  • 扩/缩容过程中的数据访问:

    • 扩缩容过程中会主动迁移将要访问的 Key ,确保对应 Key 始终都是去目标节点中访问;
  • 单个KV的迁移状态机:

    • STAGE_PREPARE : 迁移前的初始状态;
    • STAGE_PAYLOAD : 可以仅使用一个迁移命令将一个KV迁移到目标节点;
    • STAGE_CHUNKED : 需要使用多个迁移命令将一个KV迁移到目标节点;
    • STAGE_FILLTTL : 当使用多个迁移命令将一个KV迁移到目标节点后,按需发送一个设置过期时间的命令给目标节点;
    • STAGE_DONE :迁移完成的状态;

Codis异步扩缩容方案的迁移状态机

3.3、选择性复制的扩容方案

  • Redis版本:定制版Redis(基于较老的社区版本实现)

  • 方案特点:

    • 基于 Redis 的主从复制实现的数据扩容方案,从库选择性加载部分数据;
    • 只能够实现成倍扩容,无法进行缩容;
    • 需要业务进行切流;
  • Slot/Key映射关系:无需记录映射关系;

  • 扩容流程(假设从分片数 1 扩容到分片数 2 ):

    • 原始的一个分片添加两个从库,进行主从的数据同步;
    • 这两个新添加的从库在加载来自主库的数据时,选择性的过滤加载部分数据,使每个从库中的数据都是主库中数据的一半;
    • 等待数据同步完成之后,将这两个新添加的从库各自提升为主库,使其成为新的两个分片的主库;
    • 业务对业务流量进行切换,使其路由到新的两个分片中,完成扩容;

选择性复制的扩容流程

3.4、Slot扩缩容方案

3.4.1、Slot串行扩缩容方案

  • Redis版本:定制版Redis

  • 方案特点:

    • 每个 Redis 实例内部维护了一个 16384 个 DB 的数据,其中每一个 DB 代表着一个 Slot 的数据集;
    • 扩缩容操作的是整个 DB 的数据集;
    • 使用异步线程的方式,按照 DB(Slot) 依次进行数据的迁移;
    • 迁移期间需要按照 DB(Slot) 进行禁写;
  • Slot/Key映射关系:一个 DB 中的数据集全部属于一个 Slot ;

  • 扩/缩容流程(假设从节点 A 迁移数据到节点 N ):

    • 源节点 A 接收到数据迁移命令;
    • 源节点 A 将对应 DB(Slot) 对应的数据集禁写,并创建异步的迁移任务;
    • 源节点 A 中的异步迁移线程持久化对应 DB(Slot) 的数据集,并将其迁移到目标节点 N ;
    • 源节点 A 中的异步迁移线程等待目标节点 N 的回复消息,然后按需清除本地 DB(Slot)的数据;
    • 源节点 A 中的主线程取消对应 DB(Slot)的禁写,结束迁移;

Slot串行扩缩容流程

3.4.2、Slot并行扩缩容方案

  • Redis版本:定制版Redis

  • 方案特点:

    • 子进程迁移存量数据,父进程迁移增量数据;
    • 父进程会进行阻写,但是阻写窗口比较小;
  • Slot/Key映射关系:无需记录;

  • 扩/缩容流程(假设从节点 A 迁移数据到节点 N ):

    • 源节点 A 接收到数据迁移命令;
    • 源节点 A 创建子进程来迁移存量的数据,同时父进程记录期间待迁移 Slots 的变更操作;
    • 源节点 A 等待子进程迁移完成后,父进程将期间记录的增量变更操作迁移到目标节点 N ;
    • 源节点 A 等待增量迁移的数据处于特定阈值内后,将禁止新的写入;
    • 等待管控节点进行路由拓扑的变更,迁移完成;

Slot并行扩缩容流程

3.5、旁路扩缩容方案

  • Redis版本:定制版Redis(不考虑高级功能的情况下全版本Redis都支持)

  • 方案特点:

    • 旁路组件拉取全部数据并进行解析,过滤和转发来实现扩缩容;
    • 高级功能指的是一些有助于高效迁移的定制版功能;
  • Slot/Key映射关系:无需记录;

  • 扩/缩容流程(假设从节点 A 迁移数据到节点 N ,管控组件为 M ,迁移组件为 X ):

    • 管控组件 M 向旁路迁移组件 X 发起扩缩容任务;
    • 旁路迁移组件 X 伪造自己为 Redis 的一个从库,向源节点 A 发起全量数据同步和增量数据同步;
      • 拉取数据的优化(高级)版本:仅拉取待迁移的特定的 Slots 数据;
    • 旁路迁移组件 X 在本地解析/过滤拉取的全量和增量数据,并将其转发到目标节点 N ;
    • 旁路迁移组件 X 等待增量的迁移命令数处于特定阈值内后,通知管控组件 M 进行拓扑变更;
    • 管控组件 M 拓扑变更完成后,流量路由到目标节点 N ,同时旁路迁移组件会同步完成后续增量的数据;
    • 旁路迁移组件执行数据清理工作,完成扩缩容;

旁路扩缩容流程

四、思考

4.1、RedisCluster 演进方向的思考

RedisCluster 中的扩缩容功能自从 Redis 3.0.0 支持以来,架构上基本上没有特别大的变动,不过社区中谈论的声音却从未停止过,比如早期版本中 Codis 作者提交的关于异步数据迁移方案 Pull/3997 ,最近社区关于 Slot 迁移原子性以及可靠性的 Pull/10517 ,以及社区对于 Redis Cluster v2版本的规划与思考 等。

4.2、Redis扩缩容的可能性

  • 无数据变动的扩缩容方案:
    • 很多 Redis 使用者主要还是用于缓存的业务场景,因此有很多场景下及时数据出现丢失也不会对业务产生多么严重的影响,只不过我们需要控制数据丢失的百分比,因此基于这点其实我们可以实现一种不迁移数据的渐进式扩缩容方案,从而能够快速实现对资源的扩缩容,以满足极端场景下业务扩缩容的需求。

五、参考链接

作者: bugwz
链接: https://bugwz.com/2022/12/13/redis-resharding/
声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 咕咕