Redis主从复制演进史与奇思妙想

Redis 的主从复制模型从 Redis2.8 版本到 Redis7.0 经历了很多大的优化与改造,从最初版本的全量数据同步,到后续的 PSYNC 的增量数据同步,无盘数据传输方案,PSYNC2 的同源数据同步方案,无盘数据加载方案到当前的最新版本中的共享复制缓冲区的方案。同时社区中也诞生了一些奇妙的解决方案,例如基于AOF文件的增量同步等。这篇文章主要借鉴于 Redis 主从复制演进历程与百度实践 ,同时按照自己的理解绘制了一些示意图。

一、简介

目前 Redis 支持两种主从数据同步方式:全量同步和增量同步。

二、Redis主从复制演进史

2.1、SYNC方案

  • 版本范围:1.3.6 ~ 2.6.17 (以下分析基于 2.6.17 版本)

  • 方案特点:

    • 支持全量数据同步;
  • 持久化及传输流程:

    • 调用 fork 生成子进程,并在子进程中将内存中的数据持久化到 rdb 文件中;
    • 获取所有状态为 WAIT_BGSAVE_END 的从库,为其注册发送 rdb 数据的事件;
    • 发送 rdb 数据完成后,将发送堆积的增量数据给从库;
  • 交互流程:

    • 主库:
      • 接收从库的建连请求;
      • 处理从库发送的探测消息,并依次按需给从库返回 pong / ok / ok 消息;
      • 处理从库发送的 sync 命令,使用 fork 的方式持久化 rdb 数据,之后在主线程中注册一个读写事件将其数据发送给从库;
    • 从库:
      • 外部对从库执行 slaveof master_ip master_port 操作,从库主动与主库建立连接;
      • 从库依次按需发送 ping / auth / replconf listening-port $port 消息给主库,并接受主库回复;
      • 从库给主库发送 sync 命令,准备接收主库的 rdb 消息内容,并在接收完成后加载数据;

SYNC方案的交互流程

  • 复制状态机:
    • 主库(slave->replstate):
      • REDIS_REPL_NONE : 创建从库客户端的初始状态;
      • REDIS_REPL_WAIT_BGSAVE_START : 当前存在正在执行 bgsave 的任务,需要等待下一次的 bgsave 的标记状态;
      • REDIS_REPL_WAIT_BGSAVE_END : 对应客户端正在等待 bgsave 完成的标记状态;
      • REDIS_REPL_SEND_BULK : 正在给对应的客户端发送 rdb 数据的状态;
      • REDIS_REPL_ONLINE : 发送完成 rdb 数据后状态;
    • 从库(server.repl_state):
      • REDIS_REPL_NONE : 初始状态;
      • REDIS_REPL_CONNECT : 从库执行 slaveof 之后的状态;
      • REDIS_REPL_CONNECTING : 从库连接主库之后的状态;
      • REDIS_REPL_RECEIVE_PONG : 从库向主库发送 ping 之后等待接收 pong 时的状态;
      • REDIS_REPL_TRANSFER : 从库开始接收 rdb 数据的状态;
      • REDIS_REPL_CONNECTED : 从库接收 rdb 并加载数据完成的状态;

SYNC方案的复制状态机

2.2、PSYNC方案

  • 版本范围:2.8.0 ~ 2.8.17 (以下分析基于 2.8.17 版本)

  • 方案特点:

    • 引入 repl_backlog 的概念,用于在主库上保存一部分写入历史,作为后续从库增量同步的数据源;
    • 引入 psync_runid 和 psync_offset 的概念,用于支持从库发起增量同步,并且用于主库进行增量同步的验证;
  • 持久化及传输流程:

    • 主库调用 fork 生成子进程,并在子进程中将内存中的数据持久化到 rdb 文件中;
    • 主库获取所有状态为 WAIT_BGSAVE_END 的从库,为其注册发送 rdb 数据的事件;
    • 主库发送 rdb 数据完成后,将发送堆积的增量数据给从库;
  • 交互流程:

    • 主库:

      • 接收从库的建连请求;
      • 处理从库发送的探测消息,并依次按需给从库回复消息;
      • 处理从库发送的 psync runid offset 或 sync 命令,校验 runid 和 offset ,之后主库给从库回复标识以及对应数据,其中标识为:
        • 全量同步标识 :+FULLRESYNC runid offset
        • 增量同步标识 : +CONTINUE
    • 从库:

      • 外部对从库执行 slaveof master_ip master_port 操作,从库主动与主库建立连接;

      • 从库向主库发送 ping 命令,并接收回复消息;

      • 从库按需向主库发送 auth 命令,并接收回复消息;

      • 从库向主库发送 replconf listening-port $port 消息,并接收回复消息;

      • 从库按需向主库发送 replconf ip-address $ip 消息,并接收回复消息;

      • 从库向主库发送 replconf capa eof 消息,并接收回复消息;

      • 从库向主库发送 psync runid offset 或者 sync 消息,并接收回复消息,从库之后进入全量或增量数据同步;

PSYNC方案的交互流程

  • 复制状态机:

    • 主库(slave->replstate):

      • REPL_STATE_NONE : 创建从库客户端后的初始状态;
      • SLAVE_STATE_WAIT_BGSAVE_START : 等待开始生成一个 rdb 数据文件;
      • SLAVE_STATE_WAIT_BGSAVE_END : 等待生成一个 rdb 数据文件完成;
      • SLAVE_STATE_SEND_BULK : 正在给对应的客户端发送 rdb 数据的状态;
      • SLAVE_STATE_ONLINE : 发送完成 rdb 数据后状态;
    • 从库(server.repl_state):

      • REPL_STATE_NONE : 初始状态;
      • REPL_STATE_CONNECT : 从库执行 slaveof 之后的状态;
      • REPL_STATE_CONNECTING : 从库连接主库之后的状态;
      • REPL_STATE_RECEIVE_PONG : 从库向主库发送 ping 之后等待接收 pong 时的状态;
      • REPL_STATE_SEND_AUTH : 从库接下来按需向主库发送 auth 消息;
      • REPL_STATE_RECEIVE_AUTH : 从库向主库发送 auth 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_PORT : 从库接下来要向主库发送 replconf listening-port $port 消息;
      • REPL_STATE_RECEIVE_PORT : 从库向主库发送 replconf listening-port $port 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_IP : 从库接下来按需向主库发送 replconf ip-address $ip 消息;
      • REPL_STATE_RECEIVE_IP : 从库向主库发送 replconf ip-address $ip 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_CAPA : 从库接下来要向主库发送 replconf capa eof 消息;
      • REPL_STATE_RECEIVE_CAPA : 从库向主库发送 replconf capa eof 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_PSYNC : 从库接下来要向主库发送 psync runid offset 或者 sync 消息
      • REPL_STATE_RECEIVE_PSYNC : 从库向主库发送 psync / sync 之后等待接收返回消息时的状态;
      • REPL_STATE_TRANSFER : 从库开始等待接收全量(rdb)的数据;
      • REPL_STATE_CONNECTED : 从库开始等待接收增量的数据;

PSYNC方案的复制状态机

2.3、无盘传输方案

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

  • 方案特点:

    • 主库无需将 rdb 数据持久化就可以将数据传输给从库(引入 repl-diskless-sync 开关控制);
    • 支持同时给多个从库传输 rdb 数据;
  • 持久化及传输流程(仅介绍无盘传输):

    • 主库获取所有状态为 WAIT_BGSAVE_START 的从库列表,记录对应的 fd 信息;

    • 主库调用 fork 生成子进程,并在子进程中将持久化的数据写给对应的 fds ,传输 rdb 前发送标记信息为 "$EOF: $eofmask ,传输 rdb 后发送标记信息为 $eofmark (其中 $eofmask 为 40 位的随机数);

    • 主库的子进程传输数据完成后,通过管道的方式告知父进程相关从库的数据同步状态;

    • 主库的父进程后续将发送堆积的增量数据给从库;

  • 交互流程:与 2.2 PSYNC 方案完全一致;

  • 复制状态机:与 2.2 PSYNC 方案完全一致;

2.4、PSYNC2方案

  • 版本范围:4.0 ~ 5.0.14(以下分析基于 5.0.14 版本)

  • 方案特点:

    • 支持同源增量数据同步,解决了切主之后,从库与新主库之间需要进行全量同步的问题;
  • 持久化及传输流程(仅考虑有盘传输):

    • 主库调用 fork 生成子进程,并在子进程中将内存中的数据持久化到 rdb 文件中;
    • 主库获取所有状态为 WAIT_BGSAVE_END 的从库,为其注册发送 rdb 数据的事件;
    • 主库发送 rdb 数据完成后,将发送堆积的增量数据给从库;
  • 交互流程:

    • 主库:

      • 接收从库的建连请求;
      • 处理从库发送的探测消息,并依次按需给从库回复消息;
      • 处理从库发送的 psync replid offset 或 sync 命令,校验 replid 和 offset ,之后主库给从库回复标识以及对应数据,其中标识为:
        • 全量同步标识 :+FULLRESYNC replid offset
        • 增量同步标识 : +CONTINUE 或者 +CONTINUE replid
    • 从库:

      • 外部对从库执行 slaveof master_ip master_port 操作,从库主动与主库建立连接;

      • 从库向主库发送 ping 命令,并接收回复消息;

      • 从库按需向主库发送 auth 命令,并接收回复消息;

      • 从库向主库发送 replconf listening-port $port 消息,并接收回复消息;

      • 从库按需向主库发送 replconf ip-address $ip 消息,并接收回复消息;

      • 从库向主库发送 replconf capa eof capa psync2 消息,并接收回复消息;

      • 从库向主库发送 psync replid offset 或者 sync 消息,并接收回复消息,从库之后进入全量或增量数据同步;

PSYNC2方案的交互流程

  • 复制状态机:

    • 主库(slave->replstate):

      • REPL_STATE_NONE : 创建从库客户端后的初始状态;
      • SLAVE_STATE_WAIT_BGSAVE_START : 等待开始生成一个 rdb 数据文件;
      • SLAVE_STATE_WAIT_BGSAVE_END : 等待生成一个 rdb 数据文件完成;
      • SLAVE_STATE_SEND_BULK : 正在给对应的客户端发送 rdb 数据的状态;
      • SLAVE_STATE_ONLINE : 发送完成 rdb 数据后状态;
    • 从库(server.repl_state):

      • REPL_STATE_NONE : 初始状态;
      • REPL_STATE_CONNECT : 从库执行 slaveof 之后的状态;
      • REPL_STATE_CONNECTING : 从库连接主库之后的状态;
      • REPL_STATE_RECEIVE_PONG : 从库向主库发送 ping 之后等待接收 pong 时的状态;
      • REPL_STATE_SEND_AUTH : 从库接下来按需向主库发送 auth 消息;
      • REPL_STATE_RECEIVE_AUTH : 从库向主库发送 auth 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_PORT : 从库接下来要向主库发送 replconf listening-port $port 消息;
      • REPL_STATE_RECEIVE_PORT : 从库向主库发送 replconf listening-port $port 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_IP : 从库接下来按需向主库发送 replconf ip-address $ip 消息;
      • REPL_STATE_RECEIVE_IP : 从库向主库发送 replconf ip-address $ip 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_CAPA : 从库接下来要向主库发送 replconf capa eof capa psync2 消息;
      • REPL_STATE_RECEIVE_CAPA : 从库向主库发送 replconf capa eof capa psync2 之后等待接收返回消息时的状态;
      • REPL_STATE_SEND_PSYNC : 从库接下来要向主库发送 psync replid offset 或者 sync 消息;
      • REPL_STATE_RECEIVE_PSYNC : 从库向主库发送 psync / sync 之后等待接收返回消息时的状态;
      • REPL_STATE_TRANSFER : 从库开始等待接收全量(rdb)的数据;
      • REPL_STATE_CONNECTED : 从库开始等待接收增量的数据;

PSYNC2方案的复制状态机

2.4.1、同源增量同步详解

  • 关键变量:
    • server.replid : 当前实例对应主库的 replid ,如果当前实例为主库则为其自身的 replid ,该信息会在主从同步交互的流程中同步给从库,该信息会被持久化到 rdb 文件中;
    • server.replid2 : 当前实例记录的前一个主库的 replid ;
    • server.second_replid_offset : 与 server.replid2 对应,记录的是前一个主库对应的复制 offset 值,用于主库校验从库发起的增量同步请求是否合法;
    • server.cached_master :用于记录当前连接的主库信息,用于记录下一次发起增量同步时所需要的信息;
  • 主从复制 ID 变更流程:
    • 从库 => 主库 : replid 为自己生成新的,replid2 为老主库的 replid ;
    • 主库 => 从库 : replid 为新主库的 replid ,replid2 清空;
    • 从库 => 从库(变更主库) : replid 为新主库的 replid ,replid2 清空;

PSYNC2同源增量同步中复制 ID 变更图解

主从复制偏移校验流程

2.5、无盘加载方案

  • 版本范围:6.0.0 ~ 6.2.6(以下分析基于 6.2.6 版本)

  • 方案特点:

    • 从库支持了无盘加载 rdb 数据,即无需将 rdb 存储到本地后就可以将其数据加载到内存中;
    • 从库支持在加载 rdb 数据时使用临时 db 备份之前内存的数据,避免加载的 rdb 数据异常;
  • 无盘加载启用条件(满足其一即可):

    • 加载数据前要求备份原始数据(REPL_DISKLESS_LOAD_SWAPDB);
    • 本地无任何数据的情况(REPL_DISKLESS_LOAD_WHEN_DB_EMPTY);
  • 数据加载流程(仅考虑无盘加载):

    • 从库注册一个读事件 readSyncBulkPayload ,用于从主库接收 rdb 数据;
    • 从库根据设定的加载的条件,按需备份本地的 DB 数据;
    • 从库不断的从与主库的连接 socket 中读取传输的 rdb 数据,并解析后加载到本地 DB 中;
    • 从库根据配置的清理 DB 的策略,异步或同步的清空备份的 DB 数据,完成数据加载;
  • 交互流程:与 PSYNC2 方案的交互流程完全一致;

  • 复制状态机:与 PSYNC2 方案的复制状态机完全一致;

2.6、共享复制缓冲区

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

  • 方案特点:

    • 创造性的将 Backlog 和从库连接的 OutputBuffer 合二为一,节省了多从库场景下的重复内存占用问题;
  • 数据结构设计:

    • 默认情况下每一个缓存区块(replBufBlock 节点)的最小 buffer 大小为 16K (PROTO_REPLY_CHUNK_BYTES);
    • 默认情况下每添加 64(REPL_BACKLOG_INDEX_PER_BLOCKS) 个缓存区块,就会记录一些快查索引节点;
    // server.repl_backlog 的类型变成了 replBacklog* 类型
    typedef struct replBacklog {
    listNode *ref_repl_buf_node; // 复制缓冲区块的引用节点
    size_t unindexed_count; // 从上一次向 blocks_index 添加索引后增加的区块数量
    rax *blocks_index; // 用于快速查询的复制缓冲区块的索引集
    long long histlen; // 积压缓冲区的实际大小
    long long offset; // 复制积压缓冲区中记录的第一个有效字节的偏移值
    } replBacklog;

    // ref_repl_buf_node 中的每一个节点的数据结构
    typedef struct replBufBlock {
    int refcount; // 使用该节点的引用计数
    long long id; // 复制缓冲区块的唯一编号,递增
    long long repl_offset; // 该区块的第一有效字节数据对应的复制偏移值
    size_t size, used; // 记录柔性数组对应内存块大小和使用的大小
    char buf[]; // 柔性数组存储复制堆积数据
    } replBufBlock;

    // 客户端连接的数据结构
    typedef struct client {
    ...
    listNode *ref_repl_buf_node; // 复制缓冲区块的引用节点
    size_t ref_block_pos; // 下一个要发送的偏移量
    ...
    } client;

    共享复制缓冲区数据结构图

  • 复制状态机:

    • 主库(slave->replstate):

      • REPL_STATE_NONE : 创建从库客户端后的初始状态;
      • SLAVE_STATE_WAIT_BGSAVE_START : 等待开始生成一个 rdb 数据文件;
      • SLAVE_STATE_WAIT_BGSAVE_END : 等待生成一个 rdb 数据文件完成;
      • SLAVE_STATE_SEND_BULK : 正在给对应的客户端发送 rdb 数据的状态;
      • SLAVE_STATE_ONLINE : 发送完成 rdb 数据后状态;
    • 从库(server.repl_state):

      • REPL_STATE_NONE : 初始状态;
      • REPL_STATE_CONNECT : 从库执行 slaveof 之后的状态;
      • REPL_STATE_CONNECTING : 从库连接主库之后的状态;
      • REPL_STATE_RECEIVE_PING_REPLY : 从库向主库发送 ping 之后等待接收 pong 时的状态;
      • REPL_STATE_SEND_HANDSHAKE : 从库处于此状态时会依次向主库发送auth(按需), replconf listening-port $port , replconf ip-address $ip (按需), replconf capa eof capa psync2 消息;
      • REPL_STATE_RECEIVE_AUTH_REPLY : 从库按需从主库处接收 auth 消息的回复;;
      • REPL_STATE_RECEIVE_PORT_REPLY : 从库从主库处接收 replconf listening-port $port 消息的回复;
      • REPL_STATE_RECEIVE_IP_REPLY : 从库按需从主库处接收 replconf ip-address $ip 消息的回复;
      • REPL_STATE_RECEIVE_CAPA_REPLY : 从库从主库处接收 replconf capa eof capa psync2 消息的回复;;
      • REPL_STATE_SEND_PSYNC : 从库接下来要向主库发送 psync replid offset 或者 sync 消息;
      • REPL_STATE_RECEIVE_PSYNC_REPLY : 从库向主库发送 psync / sync 之后等待接收返回消息时的状态;
      • REPL_STATE_TRANSFER : 从库开始等待接收全量(rdb)的数据;
      • REPL_STATE_CONNECTED : 从库开始等待接收增量的数据;

Redis7的复制状态机

三、奇思妙想

3.1、AOF增量同步方案

我们知道 Redis 实现了基于 Backlog 的增量复制方案,但是考虑到线上实际的资源占用,Backlog 的内存大小通常不会设置的太大。如果 Redis 在写入量很大的情况下出现网络异常导致主从同步中断,从库重连时大概率会由于主库的 Backlog 被冲掉而导致无法进行增量同步的情况。在这种情况下,业界就出现了一些使用 AOF 来扩展 Backlog 数据范围的方案,从而形成了比较典型的基于 AOF 的增量同步方案。

  • 方案特点:
    • 基于 AOF 文件实现增量的数据同步,支持同步完成 AOF 文件后选择是否切换到 Backlog 的数据同步;
  • 持久化流程(AOF 数据持久化):
    • 主库关闭重写 AOF 文件,限制单个 AOF 文件大小,允许 AOF 文件按照文件大小进行滚动拆分;
    • 主库将与 Backlog 中完全一致的写操作以同步或者异步的方式持久化到 AOF 文件中;
    • 主库保证 Backlog 中数据始终可以与最新 AOF 中的一段数据完全对应;

AOF磁盘数据与内存数据的映射关系

  • 增量同步流程:
    • 主库处理从库发起的 psync replid offset 增量同步请求,尝试寻找 offset 对应的数据所在的位置;
      • 如果 offset 可以在 Backlog 中找到,则可以直接从 Backlog 中进行增量数据同步【主线程直接发送数据】;
      • 如果 offset 可以在 AOF 中找到,则可以直接从 AOF 中进行增量数据同步(发送数据文件)【单独线程发送数据】;
    • 增量数据同步延迟较小后,后续可以执行两种不同的策略:
      • 继续使用独立的线程不断的发送 AOF 中的数据;
      • 切换到使用 Backlog 的方式发送后续的增量数据;

AOF增量同步流程

3.2、社区的其他讨论

四、参考链接

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