Codis 是一个基于 Proxy 的 Redis 集群化方案,其具体介绍与使用可参考 Codis 使用文档。本文首先从运维角度出发介绍 Codis,便于了解每个操作背后的具体内容,随后简要介绍了 Codis-Proxy 基本的工作原理,最后简单介绍了扩缩容相关操作。


1. 集群管理

Codis-Dashboard 主要负责集群元数据的管理,在下面的讨论中,我们假设集群的元数据存储在 zk 中。Codis 集群的元数据大致有以下几种:

  • Group: 记录该 Group 下 Servers 地址及状态 (是否有 Promoting,是否 out-of-sync, 是否可读等)
  • SlotMapping: 包含 Slot-Group 映射关系,以及该 Slot 的状态 (用于数据迁移)
  • Proxy: 记录 Proxy 的基本信息
  • Sentinel: 记录 Sentinel 的地址及状态 (是否 out-of-sync)

Codis-Dashboard 在本地保存了一份集群元数据的缓存 Topom.cache:(Cache Aside Pattern)

  • 写操作:修改集群元数据时,会直接写 zk,不论写 zk 成功与否此时都会将缓存 Topom.cache 中对应的内容标记为失效 (nil)
  • 读操作:读取元数据时,遍历 Topom.cache,以 zk 中的数据填充失效的缓存部分,随后返回 Topom.cache

下文操作均会先读缓存,再更新 zk,并将相应的缓存信息标记为失效。值得注意的是,更新 zk 操作不一定会成功执行,但是将缓存信息标记为失效一定会执行。

1.1. Group 相关操作

  • 添加 Group

    1
    
    codis-admin [-v] --dashboard=ADDR  --create-group  --gid=ID
    

    含义:添加指定 ID 的 Group。

    在添加 Group 时,只需注意 Group ID 范围在 $[0, 9999]$ 内,且该 Group ID 目前不在集群内即可。

  • Group 中添加 Server

    1
    
    codis-admin [-v] --dashboard=ADDR  --group-add  --gid=ID --addr=ADDR [--datacenter=DATACENTER]
    

    含义:向 gid = ID 的 Group 中添加地址为 ADDR 的 Server。

    group_add_server

  • Group 中删除 Server

    1
    
    codis-admin [-v] --dashboard=ADDR  --group-del  --gid=ID --addr=ADDR
    

    含义:从 gid = ID 的 Group 删除地址为 ADDR 的 Server。

    group_del_server

    可以发现,可以直接移除可读从库,因此在移除从库时,建议先取消可读并点击 SYNC 后再移除该从库。

  • 删除 Group

    1
    
    codis-admin [-v] --dashboard=ADDR  --remove-group  --gid=ID
    

    含义:删除 gid = ID 的 Group.

    在删除 Group 之前,需先清空 Group 中的 Server。

  • 建立/取消主从同步

    1
    
    codis-admin [-v] --dashboard=ADDR  --sync-action  --create --addr=ADDR
    

    含义:向地址为 ADDR 发送 SLAVEOF 命令,在 Group 内依据 Server 的位置建立主从关系。

    • index = 0: SLAVEOF NO ONE
    • index > 0: 成为 index = 0 的 Server 的从库

    group_add_sync

    1
    
    codis-admin [-v] --dashboard=ADDR  --sync-action  --remove --addr=ADDR
    

    含义:删除地址为 ADDR 的实例上 Pending 的主从同步操作。

    group_del_sync

    • 该操作极少使用,只有在原主库所在机器宕机后,不想机器上的实例在机器启动时收到 SLAVEOF 操作时会使用。
    • 另外,在人工介入集群的故障恢复过程中时,最好直接停止集群中的 Sentinel 实例进程,以免受到 Sentinel 的干扰
  • 开启/关闭从库可读

    1
    
    codis-admin [-v] --dashboard=ADDR --replica-groups --gid=ID --addr=ADDR (--enable|--disable)
    

    含义:依据 enable/disable 开启/关闭特定实例的可读状态。

    group_replica_read

  • 响应 Sentinel 切主

    Codis-Dashboard 通过订阅 Sentinel 的 +switch-master 消息响应 Sentinel 执行故障恢复时的切主行为,当监听到切主时:

    group_switch_master

  • Group SYNC

    1
    
    codis-admin [-v] --dashboard=ADDR  --resync-group  [--gid=ID | --all]
    

    含义:将该 Group 相关的 Server 地址,是否从库可读和 Slot-Group 映射关系同步至 Codis-Proxy。

    由上述可知,Group 会在以下情况下标记为 out-of-sync:

    • 从 Group 中去除一个可读从库
    • 开启/关闭可读
    • Sentinel 自动切主后
  • Promote

    1
    
    codis-admin [-v] --dashboard=ADDR  --promote-server --gid=ID --addr=ADDR
    

    含义:将 gid = ID 的 Group 中地址为 ADDR 的实例提升为主库。

    group_promote_server

1.2. Slot 相关操作

  • 分配 Slot

    1
    
    codis-admin [-v] --dashboard=ADDR --slot-action --create --sid=ID --gid=ID
    

    含义:将 sid = ID 的 slot 分配至 gid = ID 的 Group

    slot_2_group

    1
    
    codis-admin [-v] --dashboard=ADDR --slot-action --create-some --gid-from=ID --gid-to=ID --num-slots=N
    

    含义:从 gid-from = ID 的 Group 中迁移不大于 num-slots = N 的 slots 至 gid-to = ID 的 Group

    具体操作流程同上。

    1
    
    codis-admin [-v] --dashboard=ADDR --slot-action --create-range --beg=ID --end=ID --gid=ID
    

    含义:将 $[beg, end]$ 之间的 slots 分配至 gid = ID 的 Group

    具体操作流程同上。

    1
    
    codis-admin [-v] --dashboard=ADDR --rebalance [--confirm]
    

    含义:重分配 Slots,使得各个 Group 所分配到的 Slots 数量大致相等

  • 移除 Pending 状态的操作

    1
    
    codis-admin [-v] --dashboard=ADDR --slot-action --remove --sid=ID
    

    含义:移除 sid = ID 的 Slot 上 ActionPending 状态的分配操作

  • 参数设置

    1
    
    codis-admin [-v] --dashboard=ADDR --slot-action --interval=VALUE
    

    含义:设置 Codis-Dashboard 后台执行 slot 相关操作的时间间隔,有效范围为 $[0, 1]$ (秒)

    1
    
    codis-admin [-v] --dashboard=ADDR --slot-action --disabled=VALUE
    

    含义:暂停/开始 slot 相关操作。

1.3. Codis-Proxy 相关操作

  • 添加 Codis-Proxy

    1
    
    codis-admin [-v] --dashboard=ADDR --create-proxy --addr=ADDR
    

    add_codis_proxy

  • Online Codis-Proxy

    1
    
    codis-admin [-v] --dashboard=ADDR --online-proxy --addr=ADDR
    
    • 若 Codis-Proxy 此前未在集群中,则与添加时的流程完全一致
    • 若 Codis-Proxy 此前已在集群中,则会 Online 现有的 Codis-Proxy
  • 删除 Codis-Proxy

    1
    
    codis-admin [-v] --dashboard=ADDR --remove-proxy (--addr=ADDR|--token=TOKEN|--pid=ID)       [--force]
    

    del_codis_proxy

  • Codis-Proxy SYNC

    1
    
    codis-admin [-v] --dashboard=ADDR --reinit-proxy (--addr=ADDR|--token=TOKEN|--pid=ID|--all) [--force]
    

    初始化主要包括以下三部分

    • 将集群 Slots 信息同步至 Codis Proxy
    • Start Codis Proxy: Codis Proxy & Router Online 状态设置为 true
    • 令 Codis Proxy 订阅 Sentinel 主从切换信息

1.4. Sentinel 相关操作

  • 添加 Sentinel

    1
    
    codis-admin [-v] --dashboard=ADDR --sentinel-add --addr=ADDR      
    

    add_sentinel

    可以发现在添加 Sentinel 时,只是将 Sentinels 的状态标记为 out-of-sync。这是因为此时并未让 Sentinel 监听集群中的 Server 实例,也未令 Codis-Dashboard 和 Codis-Proxy 订阅 Sentinel 的 +switch-master 消息。

  • 删除 Sentinel

    1
    
    codis-admin [-v] --dashboard=ADDR --sentinel-del --addr=ADDR [--force]
    

    del_sentinel

  • Sentinel SYNC

    1
    
    codis-admin [-v] --dashboard=ADDR --sentinel-resync
    

    sentinel_sync

2. 请求处理

codis_proxy

Codis-Proxy 处理请求的过程大致如上图所示:

  • SessionBackendConn 之间的通信

    • 每个客户端连接对应一个 Session
    • 每个后端 server 对应的 BackendConn 数由配置项 backend_primary_parallelbackend_replica_parallel 控制
    • SessionBackendConn 之间通过管道 bc.input 通信,每个 BackendConn 负责处理消费一个管道,其容量为固定大小 $1024$,因此当 Session 数量远大于 BackendConn 数量时,在 bc.input 中排队阻塞的概率较大
  • 按序回复

    • Session 中的 tasks 管道保证请求按序回复
    • BackendConn 中的 tasks 管道保证转发至 Servers 的请求,会按序收包
  • 每个请求会有两个 WaitGroup

    • r.Batch: Session 依此等待请求处理完成
    • r.Group: 其实际为 Slot.refs (请求哈希至该 Slot, 便会令 r.Group = Slot.refs)。Slot 依此会在每次更新路由表 (Slot-Group 映射) 前等待当前 Slot 上的请求处理完毕
  • 批量操作命令

    • 只要批量操作多个 key 的请求可以拆成多个单次操作并行执行,则 Codis 支持该命令
    • 因为 Codis 是将批量操作拆分成多个请求转发至 Redis,因此批量操作的耗时大致上取决于最慢的那个子请求的耗时
    • 因为批量操作的存在,Codis-Proxy 的扇出一般大于 $1$,因此当 Codis-Proxy 的 QPS 并无大幅上涨时,底层 Servers 的请求也可能上涨 (每个批量操作操作的 key 数量增长所致)

3. 扩缩容

在向 Group 中添加 Server 时,我们可以看到只有支持 SlotsInfo 命令的 Server 方可加入集群。事实上 Codis 为了能平滑扩缩容,修改了 Redis 源码以记录当前 Server 所负责的 Slot 以及每个 Slot 中的 key 的信息,这些信息记录在 redisDb->hash_slots 中。

当然,没有扩缩容需求的话,可以实现一个空的 SlotsInfo 请求处理函数便可将 Server 添加至 Codis 集群提供服务。

3.1. 相关命令

修改后的 Redis 也实现了一系列 Slot 相关的命令以支持扩缩容,参考redis 修改部分(增加若干指令)slots.cslots_async.c,介绍如下:

3.1.1. 调试相关

  • slotshashkey key1 [key2 …]

    • 命令说明:计算并返回给定 key 的 slot 序号

    • 命令参数:输入为 1 个或多个 key

    • 返回结果: 操作返回 array,其中 slot 表示对应 key 的 slot 序号,即 $hash32(key) \ \% \ \mathrm{NUM\_OF\_SLOTS}$

      1
      
      response := []int{slot1, slot2...}
      
  • slotsinfo [start] [count]

    • 命令说明:获取 redis 中 slot 的个数以及每个 slot 的大小

    • 命令参数:缺省查询 $[0, \mathrm{MAX\_SLOT\_NUM})$

      • start - 起始的 slot 序号

        缺省 = $0$

      • count - 查询的区间的大小,即查询范围为 $[start, start + count)$

        缺省 = $\mathrm{MAX\_SLOT\_NUM}$

    • 返回结果:返回结果是 slotinfo 的 array;slotinfo 本身也是一个 array。

      1
      2
      
      response := []slotinfo{slot1, slot2, slot3, ...}
      slotinfo := []int{slotnum, slotsize}
      

      其中:

      • slotnum: slot 序号
      • slotsize: slot 内数据个数
  • slotsscan slotnum cursor [COUNT count]

    • 命令说明:获取指定 slotnum 下的 key 列表

    • 命令参数:参数说明类似 SCAN 命令

      • slotnum - 查询的 slot 序号,$[0, \mathrm{MAX\_SLOT\_NUM})$
      • cursor - 说明参考 SCAN 命令
      • [COUNT count) - 说明参考 SCAN 命令
      • 暂不支持 MATCH 查询
    • 返回结果:参考 SCAN 命令

      • 返回更新后的 cursor 以及一组 key 列表
  • slotsdel slot1 [slot2 …]

    • 命令说明:删除 redis 中若干 slot 下的全部 key-value
    • 命令参数:接受至少 1 个 slotnum 作为参数
    • 返回结果:格式参见 slotsinfo,不同的是:slotsize 表示删除后剩余大小,通常为 0
  • slotscheck

    • 命令说明:对 redis 内的 slots 进行一致性检查,即满足如下两条

      • 每个 slot 中保存的 key 都能在 db 中找到对应的 val
      • 每个 db 中的 key 都能在对应的 slot 中查找到

      该操作比较慢,仅仅作为 redis 开发的调试工具使用,不能在线上使用

    • 命令参数:0 参数

    • 返回结果:

      • 成功返回字符串 OK
      • 如果 check 失败,会返回 ERR 并包含对应出错的 key

3.1.2. 同步迁移

通常客户端请求

  • slotsmgrtslot host port timeout slot

    • 命令说明:随机选择 slot 下的 1 个 key-value 到迁移到目标机(同步 IO 操作)

      • 如果当前 slot 已经空了或者选择的 key 刚好过期,返回 0
      • 如果当前 slot 下面还有 key 则选择一个进行迁移
      • 同时返回当前 slot 剩余 key 的个数
      • 迁移过程在目标机器调用 slotsrestore 命令,迁移会 覆盖旧值
    • 命令参数:

      • host:port - 目标机地址
        • redis 内部缓存到 host:port 的连接 $30s$,超时或错误则关闭
      • timeout - 操作超时,单位 $ms$
        • 过程需要 3 个同步操作:
          1. 建立连接(可被缓存优化)
          2. 发送 key-value 数据
          3. 接受目标机返回
        • 指令保证每个操作不超过 timeout
      • slot - 指定迁移的 slot 序号
    • 返回结果: 操作返回 int

      1
      
      response := []int{succ,size}
      

      其中:

      • succ: 表示迁移是否成功。
        • 0 表示当前 slot 已经空了(迁移成功个数 = 0)
        • 1 表示迁移一个 key 成功,并从本地删除(迁移成功个数 = 1)
      • size: 表示 slot 下剩余 key 的个数
  • slotsmgrtone host port timeout key

    • 命令说明:迁移 key 到目标机,与 slotsmgrtslot 相同

    • 命令参数:参见 slotsmgrtslot

    • 返回结果:操作返回整数,其中 succ 的含义与 slotsmgrtslot 相似

      1
      
      response := int(succ)
      
  • slotsmgrttagone host port timeout key

    • 命令说明:迁移与 key 有相同的 tag 的所有 key 到目标机

      • 当 key 中不包含合法 tag 时,命令退化为 slotsmgrtone,时间复杂度为 $O(1)$
      • 当 key 中包含合法 tag 时,命令会计算 tag 的 hash 值,并在 skiplist 中找到所有具有相同 hash 值的 key-value 对,原子地迁移到目标机,时间复杂度为 $O(log(n))$

      修改的 redis 中,会将所有含有 tag 的 key,组织在 skiplist 中,并按照 tag 的 hash 值进行排序。当对按照某一 tag 进行迁移数据时,实际操作会将所有具有相同 hash 值的 tag 所涉及到的所有 key 一起迁移。也就是说,真正迁移的数据可能包含更多的 key,但是这么设计会减少 tag 迁移过程对字符串的比较次数,显著提升性能。

    • 命令参数:参见 slotsmgrtone

    • 返回结果:操作返回整数,其中 succ 表示成功迁移的 key 的个数。

      1
      
      response := int(succ)
      
  • slotsmgrttagslot host port timeout slot

    • 命令说明:与 slotsmgrtslot 对应的迁移指令
    • 其他说明参考 slotsmgrtslot 以及 slotsmgrttagone 的解释即可

衍生命令

  • slotsrestore key1 ttl1 val1 [key2 ttl2 val2 …]
    • 命令说明:该命令是对 redis-2.8 的 restore 命令的扩展
      • 可以对 restore 多个 key-value
      • 过程是原子的

      restore 不同的是,slotsrestore 只支持 replace,即一定覆盖旧值。如果旧值已经存在,那么只可能是 redis-slots 或者 proxy 的实现 bug,程序会通过 redisLog 打印一条冲突记录。

3.1.3. 异步迁移

通常客户端请求

  • slotsmgrtone-async host port timeout maxbulks maxbytes key1 [key2 ...]

    • 命令说明:迁移 key1, key2 … 到目标机

    • 命令参数:

      • host:port: 目标机地址
      • timeout: 操作超时
        • 默认值 $30s$
        • timeout 时间内未发送 slotsrestore-async 迁移命令时,同步客户端会被服务器关闭
        • 也是所同步 key 在同步途中设置临时过期时间,用于错误处理,保证单个数据的完整性
        • 可在 Codis-Dashboard 的配置文件中通过配置项 migration_timeout 设置
      • maxbulks:大对象进行指令拆分时,单个指令可包含的数据分片个数
        • 默认值 $200$,最大值 $512 \times 1024$
        • 可在 Codis-Dashboard 的配置文件中通过配置项 migration_async_maxbulks 设置
      • maxbytes:同步客户端输出缓冲区大小,缓冲区满时不产生新的迁移指令
        • 默认值 512 KB,最大值 INT_MAX / 2 Byte
        • 同时需满足 Redis 对 Normal Client 的缓冲区大小限制
        • 可在 Codis-Dashboard 的配置文件中通过配置项 migration_async_maxbytes 设置
    • 返回结果:操作返回整数,表示已迁移 key 的数量

  • slotsmgrttagone-async host port timeout maxbulks maxbytes key1 [key2 ...]

    • 命令说明:迁移与 key1, key2, … 有相同的 tag 的所有 key 到目标机
    • 其他说明参考 slotsmgrttagone 以及 slotsmgrtone-async 的解释即可
  • slotsmgrtslot-async host port timeout maxbulks maxbytes slot numkeys

    • 命令说明:从指定 slot 中迁移至多 numkeys 个 key 至目标机

    • 命令参数:

      • slot:slot number
      • numkeys:本次迁移指令的数量上限
        • 默认值 $100$
        • 大对象在拆分时,会被当成多个 key 被计算,所以实际迁移的 key 的数量一般小于 numkeys
        • 可在 Codis-Dashboard 的配置文件中通过配置项 migration_async_numkeys 设置
      • 其他命令参数说明同 slotsmgrtone-async
    • 返回结果:操作返回两个整数

      • 已迁移 key 的数量
      • 该 slot 中剩余 key 的数量
  • slotsmgrttagslot-async host port timeout maxbulks maxbytes slot numkeys

    • 命令说明:与 slotsmgrtslot-async 对应的迁移指令,会在迁移 key 时同时迁移具有相同 tag 的 key
    • 其他说明参考 slotsmgrttagone 以及 slotsmgrtslot-async 的解释即可
  • slotsmgrt-exec-wrapper key command [arg1 ...]

    • 命令说明:依赖 key 的状态决定如何执行包装的 command

    • 返回结果:操作返回 Arrays

      • Arrays[0] 为整数标识
        • -1 表示所包装的请求格式错误
        • 0 表示所操作的 key 不存在
        • 1 表示包装的请求为写操作,且该 key 正在迁移中
        • 2 表示所包装的请求可被执行
      • Arrays[1] 返回值类型取决于 Arrays[0]
        • Arrays[0] == 2 时,Arrays[1] 表示所包装的 command 执行的结果
        • Arrays[0] != 2 时,Arrays[1] 为一条错误信息
    • 附加说明:

      • 在所包装的请求为读操作时,可直接执行
      • 在所包装的请求为写操作时
        • 如果 key 未处于迁移状态,可执行

          需注意的是,此处写操作并不会转发至 AOF 和从库

        • 如果 key 处于迁移状态,则不会执行写操作,直接返回 error

衍生命令

  • slotsrestore-async-auth passwd

    • 命令说明:同步客户端认证

      需注意的是会使用本机的 requirepass 向目标机进行认证,因此我们需保证同一集群内的所有 Codis-Server 密码一致

  • slotsrestore-async-select db

    • 命令说明:同 SELECT db,保证同一 slot 中的 key 即使处于不同 Codis-Server 中,但是 db ID 会是相同的
  • slotsrestore-async 类命令

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    /* *
     * SLOTSRESTORE-ASYNC delete $key
     *                    expire $key $ttl
     *                    object $key $ttl $payload
     *                    string $key $ttl $payload
     *                    list   $key $ttl $hint [$elem1 ...]
     *                    hash   $key $ttl $hint [$hkey1 $hval1 ...]
     *                    dict   $key $ttl $hint [$elem1 ...]
     *                    zset   $key $ttl $hint [$elem1 $score1 ...]
     * */
    
    • 命令说明:按照所包装命令对 key 执行删除、设置过期时间或 restore 操作

      • delete: 删除该 key
      • expire: 给该 key 设置过期时间
      • object: 表示该 key 的 val (payload) 为 RDB 格式
        • 小对象使用 RDB
      • string: 表示该 key 为字符串类型,且其 val 未压缩
        • 字符串类型不压缩
      • list/hash/dict/zset:表示该 key 为列表/哈希表/集合/有序集合类型
        • 大对象进行指令拆分,单条指令可包含多个数据分片,由参数 maxbulks 控制
        • hint: 该 key 总的元素个数
    • 于何处衍生该命令?

      • 在客户端发送 slotsmgrtone-asyncslotsmgrttagone-asyncslotsmgrtslot-asyncslotsmgrttagslot-async 时会尝试在 $500\mu s$ 内发送不少于 $3$ 条 slotsrestore-async 命令至目标机
      • 在源端收到目标机返回的 slotsrestore-async-ack 时,会尝试在 $10\mu s$ 内发送不少于 $2$ 条 slotsrestore-async 命令至目标机
      • 在目标机调用 slotsmgrtone-async-dumpslotsmgrttagone-async-dump
    • 返回结果:在目标机收到并处理 slotsrestore-async 命令后,会返回 slotsrestore-async-ack errno message 请求至源端

      • errno = -1: 有错误发生,message 会显示具体的错误信息
      • errno = 0 : 成功执行相应操作,message 为一个整型字符串,其含义依据所包装的请求而定
        • delete: 0 or 1 表示成功删除的 key 个数
        • expire/object/string: 1
        • list/hash/dict/zset: 执行 restore 操作后目标机该 key 中所含的元素个数
  • slotsrestore-async-ack errno message

    • 命令说明:响应来自目标机的 ack 回复,尝试继续发送 slotsrestore-async 命令以迁移数据,当数据迁移完毕时,通知客户端 (发送数据迁移请求的客户端),在本地删除已迁移 key

调试命令

  • slotsmgrtone-async-dump timeout maxbulks key1 [key2 ...]

    • 命令说明:要求服务器迁移 key1, key2, … 至本客户端
    • 命令参数的说明同 slotsmgrtone-async,唯一不同的是此处 maxbulks 的默认值为 $1000$,最大值为 INT_MAX
  • slotsmgrttagone-async-dump timeout maxbulks key1 [key2 ...]

    • 命令说明:要求服务器迁移 key1, key2, … 及其相同 tag 的 key 至本客户端
    • 命令参数的说明同上
  • slotsmgrt-async-fence

    • 命令说明:判断或等待数据迁移完毕
    • 返回结果
      • OK: 当前该客户端所在 db 无数据迁移任务
      • error: 表示重复调用 slotsmgrt-async-fence 命令
      • 已迁移完毕的返回值,取决于触发当前迁移任务的请求类型
  • slotsmgrt-async-cancel

    • 命令说明:终止数据迁移
    • 返回结果:整型
      • 0: 表示当前该客户端所在 db 无数据迁移任务
      • 1: 表示当前该客户端所在 db 数据迁移任务已终止
  • slotsmgrt-async-status

    • 命令说明:返回当前该客户端所在 db 数据迁移任务的运行状态
    • 返回结果:bulk strings
      • host: 目标机 IP
      • port: 目标机端口号
      • used: 同步客户端是否已通过认证以及选择相应的 db
      • timeout: 客户端操作超时时间
      • lastuse: 表示上一次使用同步客户端发送迁移命令的时间
      • since_lastuse: 当前时间 $-$ lastuse
      • sending_msgs: 已发送 slotsrestore-async 类迁移命令但还未收到 slotsrestore-async-ack 的请求数
      • blocked_clients: 阻塞等待当前迁移任务完成的客户端数量
      • batched_iterator: 迁移迭代器状态
        • keys: 将要迁移的 key
        • timeout: 临时过期时间 (其值与客户端操作超时时间相同)
        • maxbulks: 大对象进行指令拆分时,单个指令可包含的数据分片个数
        • maxbytes: 同步客户端输出缓冲区大小,缓冲区满时不产生新的迁移指令
        • estimate_msgs: 本次迁移任务的 slotsrestore-async 指令数量
        • removed_keys: 已迁移完成的 key 列表的长度,会随着迁移任务的进行增长,在迁移任务结束时会依据列表 it->removed_keys 删除本地的 key
        • chunked_vals: 迁移过程中产生的中间值列表的长度,在迁移任务结束时会依据列表 it->chunked_vals 释放中间值
        • iterators: 当前尚未迁移的 key 的列表

3.2. Codis-Dashboard 扩缩容流程

slot-相关操作 中我们发现 Codis-Dashboard 只是会把相关 slot 标记为 ActionPending 状态,事后具体的 slot 分配以及数据迁移操作会有 Goroutine ProcessSlotAction 执行:

  • 并行操作的 slot 数量由配置项 migration_parallel_slots 控制
  • 数据迁移时使用的迁移方式由配置项 migration_method 控制

其执行流程如下图所示:

process_slot_action

可以发现其中有多次将 Slot 相关状态信息同步至 Codis-Proxy 的操作,此时 Codis-Proxy 获取的 Slot-Group 映射关系与 Slot 当前 Action 的状态有关:

Slot.Action.State Slot-Group Map
ActionNothing, ActionPending backend = GroupId, Replicas = GroupId.replicas
ActionPreparing backend = GroupId
ActionPrepared \
ActionMigrating backend = Action.TargetId, migrateFrom = GroupId
ActionFinished backend = Action.TargetId

在迁移 Slot 中的数据时,根据 migration_method 配置选择不同的迁移方案:

3.2.1. 同步迁移 — sync

通过循环调用 slotsmgrttagslot host port timeout slot 逐步迁移该 slot 中的 key 至目标机:

codis_dashboard_sync_slot

同步迁移存在以下问题:

  • 迁移速度慢
    • 序列化 & 反序列化时间开销大
    • 单个数据迁移以 $ms$ 为单位
    • 迁移单位过小:单次操作 $1$ 个或几个数据
    • 受网络 RTT 以及迁移指令间隔 (Codis 默认 $1ms$) 影响
  • 出错无法修复
    • 失败的迁移会永远失败
    • 失败的迁移会阻塞服务
  • 迁移过程阻塞 Redis
    • 单线程阻塞,无法提供服务
      • 访问无关数据以及被迁移数据都是不支持的
    • 有误触发 HA 的风险:主从切换、丢失数据
  • 低版本 Redis 数据大小会受到协议限制

3.2.2. 异步迁移 — semi-async

通过循环调用 slotsmgrttagslot-async host port timeout maxbulks maxbytes slot numkeys 逐步迁移该 slot 中的 key 至目标机:

codis_dashboard_semi_sync_slot

针对同步迁移的缺陷,异步迁移的实现进行了一系列的优化:

  • 降低序列化 & 反序列化开销
    • string 类型不压缩
    • 小对象使用 RDB
    • 大对象使用指令进行拆分
      • 单个指令可包含多个数据分片 (通过参数 maxbulks 进行配置)
  • 批量迁移
    • 单次请求可以迁移多个 key (通过参数 numkeys 进行配置)
    • 迁移开销预估,避免大对象与其他对象在同一 batch 内迁移 (除非具有相同 tag)
  • 流量优化
    • 流量加速
      • 充分利用 RESP 的 Pipeline
      • 每条 slotsrestore-async-ack 会尝试产生不少于 $2$ 条新的 slotsrestore-async 迁移指令 (指数级增长)
    • 流量限速
      • 每条 slotsrestore-async-ack 用于产生迁移指令的时间限制为 $10 \mu s$
      • 使用参数 maxbytes 控制同步客户端输出缓冲区的大小,当缓冲区满时不产生新的迁移指令
  • 错误处理
    • 在每条迁移指令中,为 key 加上临时过期时间 (通过参数 timeout 进行配置),用于错误处理,保证单个数据的完整性
  • 读写冲突处理
    • 访问无关数据,允许
    • 访问被迁移数据,只读

3.3. Codis-Proxy 在扩缩容期间请求的处理

Codis-Proxy 在某个 Slot 处于迁移状态时,处理读写请求的逻辑与 Codis-Dashboard 所配置(migration_method)的同步方式有关。

同步迁移

  1. 向源端发送 SLOTSMGRTTAGONE host port 3000 key 命令,将本次客户端请求涉及到的 key 迁移至目标机
  2. 将客户端请求路由至目标机

异步迁移

向源端发送 SLOTSMGRT-EXEC-WRAPPER key command [arg1 ...] 命令

  • 如果该 key 已迁移,则将客户端请求路由至目标机
  • 如果该 key 尚未迁移:
    • 如请求为读请求,则可直接获得结果
    • 如请求为写请求,则不断重试发送 SLOTSMGRT-EXEC-WRAPPER 命令判断 key 是否已迁移,直至 key 已迁移

4. 参考资料