TOC
本文主要梳理 Redis 主从同步的流程与细节;
Redis 主从同步
概要
Redis 提供了主从模式支撑 Redis 服务的可靠性,同时主从模式采用的是读写分离的机制,主实例允许读写操作,从实例只允许读操作,从实例通过主从同步更新数据;我发现很多主从模式方案都会使用读写分离机制,两者为什么会绑定在一起呢?问题答案关键就是不读写分离,主从同步的代价很高;
假如,主从实例都允许修改,修改请求发送到不同的实例上,为了保持实例间的数据一致性,就涉及到加锁,实例间协商是否同意修改的一系列操作,这样主从模式同步的代价会很高;采用读写分离的方式,我们大多数请求只要保证最终一致性即可,写操作仅在主实例执行,从实例通过主从同步更新数据,这是代价最低的方式;当然如果业务必须拿到最新的数据,可以通过指定查询主实例;
全量同步步骤
Redis 主从同步交互步骤大致如下:
- 从实例通过命令 replicaof(Redis >= 5.0) 或 slaveof(Redis < 5.0) 与主实例建立主从关系,并开始同步数据;
- 主实例接收到从实例的 replicaof 命令,建立一个输出缓冲区「client-output-buffer」;
- 主实例执行 bgsave 命令 fork 子进程生成 RDB 文件,发送给从实例,生成 RDB 文件不会阻塞主进程,但 fork 子进程会;
- 从实例接受到 RDB 文件后,清理当前数据库数据,再加载 RDB 文件初始化数据;
- 主实例用输出缓冲区「client-output-buffer」记录 RDB 文件生成后收到的所有写操作,并发送给从实例执行;
- 从实例不断执行主实例发送的指令,完成主从同步;
下面详细介绍一下 replicaof, slaveof 命令发起后具体的处理逻辑;
首先,当在从实例执行 replicaof 命令,从实例会发送 psync ? -1 的命令到主实例上;然后主实例会发送 FULLRESYNC {runId} {offset} 命令给从实例,从实例保存主实例的 runId 和 offset 数据,等待主实例发送 RDB 文件;
psync {runID} {offset} 与 FULLRESYNC {runId} {offset}
- runId: 每个 Redis 实例都会分配一个 Id, 这个 runId 为主实例的 Id; 第一次同步,从实例不知道主实例 Id, 所以为 ?;
- offset: 表示复制进度,-1 表示第一次复制;
增量同步步骤
Redis版本 >= 2.8 后,开始支持增量同步的方式,如果主从实例连接断开,重连后会走 Redis 增量同步逻辑;实现增量同步的关键是 Redis 主实例维护了一个环形缓冲区 repl_backlog_buffer;主实例会记录自己写到的位置(master_repl_offset),从实例会记录自己读到的位置(slave_repl_offset);具体步骤如下:
- 从实例发送 psyc {runId} slave_repl_offset;
- 主实例根据 master_repl_offset 和 slave_repl_offset 执行对应同步逻辑
- 如果 slave_repl_offset 还在环形缓冲区,将 slave_repl_offset 到 master_repl_offset 的命令通过输出缓冲区「client-output-buffer」发送给从实例;
- 如果 slave_repl_offset 不在环形缓冲区,则认为从实例断开时间过长,走全量同步逻辑;
长连接复制
长连接复制是主从库正常运行后的常规同步阶段。在这个阶段中,主从库之间通过命令传播实现同步。
问题避免
避免主实例挂载过多的从实例;
在全量同步逻辑中,主实例需要 fork 子进程生成 RDB 并传输 RDB 到从实例,fork 操作会阻塞主线程处理正常请求,传输 RDB 文件需要占用主库网络带宽。可以使用「主-从-从」模式,将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上;
合理设置环形缓冲区 repl_backlog_buffer 大小;
repl_backlog_buffer 大小可通过参数 「repl_backlog_size」 设置;如果环形缓存区过小可能会导致以下问题:
- 长连接复制阶段,如果从实例读取命令过慢,主实例覆盖了从实例未读取的命令,则会造成主从不一致的风险;
- 主从实例断连后,发现 slave_repl_offset 不在环形缓冲区则进行全量同步,设置过小会导致一断连就全量同步;
- 主实例生成的 RDB 文件过大,从实例同步期间,积累的写命令写满了环形缓冲区;
RDB 如何传输到从实例
有两种方式,一种先生成 RDB 文件,再将文件发送到从实例上;另一种直接通过 socket 连接直接发送 RDB 文件,不生成文件在主实例中;可以通过参数 「repl-diskless-sync」 设置
合理设置输出缓冲区「client-output-buffer」避免复制风暴问题
主从之间的输出缓冲区可通过参数 client-output-buffer-limit slave xxxmb xxxmb xx 设置;
如果从实例读取命令过慢,会导致输出缓冲区数据堆积,当超过 client-output-buffer-limit 后,主实例会断开与从实例的连接,中断后,从实例再次发起复制请求,此时可能导致恶性循环,引发复制风暴;
client-output-buffer-limit 配置参数介绍
client-output-buffer-limit 用于配置 Redis 服务端的输出缓冲区大小;Redis 将客户端分为三类:normal(普通客户端),pubsub(订阅客户端), slave(从实例客户端),配置示例如下:
# 表示对普通客户端缓冲区大小和持续写入量不做限制
client-output-buffer-limit normal 0 0 0
# 表示对订阅客户端缓冲区超过 8MB,则断开客户端连接;如果 60 秒内写入量超过 2MB,则断开客户端连接
client-output-buffer-limit pubsub 8mb 2mb 60
# 表示对从实例客户端缓冲区超过 521MB,则断开客户端连接;如果 60 秒内写入量超过 128MB,则断开客户端连接
client-output-buffer-limit slave 512mb 128mb 60
主从模式如果主实例挂了会如何?
主实例如果挂了,则 Redis 将不再支持写操作,仍可以支持读操作;选主问题可通过 sentinel 模式解决;