读完这篇文章,就基本搞定了 Redis 主从复制

什么是主从复制?

两台主机或者说两个节点以上,通过将其中一个节点的数据复制一份到另外节点上,前者叫主节点,后者叫从节点。

它们之间的区别在于:

  • 主节点能读能写

  • 从节点只能读

除了每次建立主从关系,从节点主动向主节点请求复制信号外,其余时候都是被动的接收主节点的数据指令。基于这句话,可以得出一个小结:

  • 主从复制是单向的,由主节点发送给从节点。

默认情况下,每个 Redis 节点都是主节点,每个主节点可以拥有多个从节点,或者没有。每个从节点只能拥有一个主节点。

主从复制有什么作用?

主从复制的作用大概有以下几点:

  • 数据冗余
    • 通俗点就是备份,主从复制时热备份,是持久化之外的数据冗余另一种方案。
  • 故障恢复
    • 主机若是发生故障,随时进行切换。由从节点提供服务,从而实现快速故障恢复,实际上这是一种服务冗余。
  • 负载均衡
    • 主从复制实现了读写分离,主节点提供写服务,从节点提供读服务。
    • 应用需要写服务,就连接 Redis 主节点,而需要读服务,就连接 Redis 从节点。
    • 在读多少写的场景下,通过这种方式,分担读写负载,可以有效提高 Redsi 的并发量。
  • 高可用基础
    • 主从复制是 Redis 实现哨兵和集群的基础。

如何使用主从复制?

这种关系只需要在从节点设置即可,主节点不需要配置,一句话:配从不配主。

构成关系

建立主从关系有三种方式:

  • 配置文件
    • slaveof <master-host> <master-port>
  • 服务启动命令参数
    • redis-server redis.conf slaveof <master-host> <master-port>·
  • Redis 客户端
    • slaveof <master-host> <master-port>

三种方式等效。

建立主从复制

可以准备两个节点,为方便测试,可以使用同一台主机,运行 Redis 的时候,使用不同配置文件即可。

测试数据

  • 192.168.188.150:6389 --- 主节点
  • 192.168.188.150:6390 --- 从节点
192.168.188.150:6389> slaveof 192.168.188.150 6390

主节点写数据,会将命令发送给从节点。

断开主从复制

192.168.188.150:6389> slaveof no one

从节点断开复制后,不会删除已有的数据。

主从复制是原理是什么?

大体分为三个过程:

  • 连接建立阶段
  • 数据同步阶段
  • 命令传播阶段

连接建立阶段

属于数据同步的准备阶段。

步骤1:保存主节点信息

从服务器维护两个字段,存储主节点的 ip 和 port 信息。

  • master-host
  • master-port

slaveof 是异步命令,从节点完成主节点 ip 和 port 的保存后,向发送 slaveof 命令的客户端直接返回 OK,实际建立成功是在其返回结果之后。

image-20200327103052896

注意一点:slaveof 命令没有能力发现要建立的主节点是否故障,处理宕机状态。

步骤2:建立 socket 连接

从上一步骤开始,每次从机启动都会建立 socket 连接,并且从节点每秒1次调用复制定时函数 replicationCron(),将上一步保存的主节点信息,用于发现主机是否上线,若是检测到上线,那么将会进行如下动作:

  • 从节点为该 socket 建立处理复制工作的 fork 子进程,负责后续的复制工作,如接收 RDB 文件、接收命令传播等。

  • 主节点接收到从节点的 socket 连接后(即 accept 之后),为该 socket 创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。

7144:S 27 Mar 2020 18:28:29.794 * MASTER <-> REPLICA sync started
7144:S 27 Mar 2020 18:28:29.794 * Non blocking connect for SYNC fired the event.

除了成功,从主节点会不间断的调用 replicationCron() 函数,直到建立连接为止;

image-20200327103052896
步骤3:发送ping命令

首次建立 socket 连接,从节点会发送 ping 命令,确定 socket 连接是否可用以及主节点是否能处理请求。

发送 ping 命令,可能出现三种情况:

  • PONG:成功。
  • 超时:断开 socket 连接,重新连接。
  • pong 之外的情况:从节点断开socket连接,并重连。

步骤4:身份验证

主节点设置了 requirepass 这个配置,那么从节点连接的时候,需要在配置文件中定义 masterauth 命名,指明连接主节点的密码。

需要注意一点:只有主节点的设置密码的状态,和从节点的 masterauth 一致(要么都有设置,要么都没有设置)才能成功,否则就是:"NOAUTH Authentication required"。

如果不一致,则从节点断开 socket 连接,并重连。

步骤5:发送从节点端口信息

身份验证后,从节点会向主节点发送从节点监听的端口号,主节点将该信息保存到该从节点对应的客户端的 slave_listening_port 字段中。

该端口信息除了在主节点中执行 info Replication 时显示以外,没有其他作用。

数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。

具体执行的方式是:

  • 从节点向主节点发送 psync 命令(Redis2.8 以前是 sync 命令),开始同步。

数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为:全量复制、部分复制。

需要注意的是,在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。

原因在于:

  • 在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求
  • 而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。

命令传播阶段

数据同步阶段完成后,主从节点进入命令传播阶段。

在这个阶段主节点将执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING 和 REPLCONF ACK。

延迟与不一致

需要注意的是,命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复。因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。

数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的 repl-disable-tcp-nodelay 配置等有关。

有关配置:

  • **repl-disable-tcp-nodelay no:**该配置作用于命令传播阶段,控制主节点是否禁止与从节点的 TCP_NODELAY(tcp 无延迟)。
    • 默认 no,即不禁止 TCP_NODELAY。
    • 当设置为 yes 时,TCP 会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差
    • 具体发送频率与 Linux 内核的配置有关,默认配置为 40ms。当设置为 no 时,TCP 会立马将主节点的数据发送给从节点,带宽增加但延迟变小。

一般来说,只有当应用对 Redis 数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为 yes;多数情况使用默认值 no。

数据是每次复制全部,还是更改的一部分?

在 Redis2.8 以前,从节点向主节点发送 sync 命令请求同步数据,此时的同步方式是全量复制。

在 Redis2.8 及以后,从节点可以发送 psync 命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。

  • **全量复制:**用于初次复制或其它无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
  • **部分复制:**用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,造成主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

全量复制

Redis 通过 psync 命令进行全量复制的过程如下:

  1. 两种情况需要全量复制
    • 从节点判断无法进行部分复制,向主节点发送全量复制的请求
    • 从节点发送部分复制的请求,但主节点判断无法进行部分复制。
  2. slave 向 master 传递命令 psync? -1 (因为第一次通信不知道master的runid和偏移量,所以传-1)
  3. 主节点收到全量复制的命令后,执行 bgsave,在后台生成 RDB 文件,并使用一个缓冲区(称为复制缓挤压冲区)记录从现在开始执行的所有写命令。
    • 这个过程会耗费主服务器大量的CPU、内存和磁盘I/O资源
  4. 主节点的 bgsave 执行完成后,将 RDB 文件发送给从节点;
    • 耗费主服务器大量的网络资源包括带宽和流量,并对主服务器响应命令请求的时间产生影响
  5. 从节点接收完整的数据之后,清除自己的旧数据,然后载入接收的 RDB 文件,将数据库状态更新至主节点执行 bgsave 时的数据库状态。
    • 加载的过程中会因为阻塞而没办法处理命令请求
  6. 主节点将前述复制积压缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态。
  7. 如果从节点开启了 AOF,则会触发 bgrewriteaof 的执行,从而保证 AOF 文件更新至主节点的最新状态。

image-20200327103052896

标注的流程:

  1. 同步启动
  2. 非阻塞连接同步触发事件
  3. master 发送 PING,可以复制。
  4. 部分数据同步
  5. 请求同步的 192.168.188.150:6379 副本
  6. 副本 192.168.188.150:6379 请求完全全量复制
  7. 启动 BGSAVE 以便与目标磁盘同步
  8. 从 master 完全同步:15644e0ce565e028c59e14c1271dae1ae1764aab:0
  9. DB 保存在磁盘上
  10. RDB:copy-on-write 使用的 4mb 内存
  11. 与副本 192.168.188.150:6379 的同步成功
    • 主 <-> 复制同步:receiving 187 bytes from master,从 master 接受到 187 bytes 的数据
    • 主 <-> 复制同步:Flushing old data,清除旧数据
    • 主 <-> 复制同步:Loading DB in memory,将 db 载入到内存中
    • 主 <-> 复制同步:Finished with success,完成同步

以上就是数据同步过程,不同情况有细为差别,其中需要注意的地方:

  • 第 4 步,master 节点可以接收部分复制,但是从节点判断无法部分复制,所以再次请求 master 节点,要求全量复制。
  • 7 - 9 这几步已经将数据从 master 节点的数据同步过来了,并且写入到磁盘中。
  • 10 步报道上面数据同步是用了多少内存
  • 11 步将数据载入到内存中。

通过全量复制的过程可以看出,全量复制是非常重型的操作:

  • 主节点通过 bgsave 命令 fork 子进程进行 RDB 持久化,该过程是非常消耗 CPU、内存(页表复制)、硬盘 IO的。
  • 主节点通过网络将 RDB 文件发送给从节点,对主从节点的带宽都会带来很大消耗。
  • 从节点清空老数据、载入新 RDB 文件的过程是阻塞的,无法响应客户端的命令。如果从节点执行bgrewriteaof,也会带来额外的消耗。

部分复制

由于全量复制在主节点数据量较大时效率太低,因此 Redis2.8 开始提供部分复制,用于处理网络中断时的数据同步。

复制偏移量

  • 主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;
  • 主节点每次向从节点传播 N 个字节数据时,主节点的 offset 增加 N;
  • 从节点每次收到主节点传来的 N 个字节数据时,从节点的 offset 增加N。

offset 用于判断主从节点的数据库状态是否一致:

  • 如果二者 offset 相同,则一致;
  • 如果 offset 不同,则不一致,此时可以根据两个 offset 找出从节点缺少的那部分数据。
  • 假设主节点的 offset 是 1000,从节点的 offset 是 500,那么部分复制只需要将 offset 为 501-1000 的数据传递给从节点,而 offset 为 501-1000 的数据存储的位置。

复制积压缓冲区

复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;

复制积压缓冲区在当主节点存在从节点时创建,其作用是:

  • 备份主节点最近发送给从节点的数据。
  • 注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。

在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;

除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset) 。

由于复制积压缓冲区定长且先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。

由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点 offset 的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。

合适的复制积压缓冲区,可以提高网络中断时部分复制执行的概率:

  • 可以根据需要增大复制积压缓冲区的大小
  • 通过配置 repl-backlog-size 来设置。
  • 假设网络中断的平均时间是 60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为 100KB,则复制积压缓冲区的平均需求为 6MB,保险起见,可以设置为 12MB,来保证绝大多数断线情况都可以使用部分复制。

从节点将 offset 发送给主节点后,主节点根据 offset 和缓冲区大小决定能否执行部分复制:

  • 如果 offset 偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
  • 如果 offset 偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。

服务器运行ID(runid)

每个 Redis 节点(无论主从),在启动时都会自动生成一个随机 ID (每次启动都不一样),由 40 个随机的十六进制字符组成;runid 用来唯一识别一个 Redis 节点。通过 info server 命令,可以查看节点的 runid:

主从节点初次复制时,主节点将自己的 runid 发送给从节点,从节点将这个 runid 保存起来;当断线重连时,从节点会将这个 runid 发送给主节点;主节点根据 runid 判断能否进行部分复制:

  • 如果从节点保存的 runid 与主节点现在的 runid 相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制
    • 判断偏移量 是否还在积压队列中累积的命令范围内
  • 如果从节点保存的 runid 与主节点现在的 runid 不同,说明从节点在断线前同步的 Redis 节点的状态,并不是当前的主节点的状态,只能进行全量复制。
    • 判断 runid 和 复制 master_replid

psync 命令执行

psync命令执行过程中,主从节点是如何确定使用全量复制还是部分复制的。

解析:

  • 根据从节点当前状态,判断是否执行 psync 命令
    • 如果从节点之前未执行过 slaveof 或最近执行了 slaveof no one,则从节点发送命令为 psync ? -1,向主节点请求全量复制;
    • 如果从节点之前执行了 slaveof,则发送命令为 psync ,其中 runid 为上次复制的主节点的 runid,offset 为上次复制截止时从节点保存的复制偏移量。
  • 主节点接收到 psync 命令,根据当前节点状态,判断决定全量还是部分。
    • 如果主节点版本低于 Redis2.8,则返回 -ERR 回复,此时从节点重新发送 sync 命令执行全量复制;
    • 如果主节点版本是 2.8及后续版本、runid 与从节点发送的 runid 相同,且从节点发送的 offset 之后的数据在复制积压缓冲区中都存在,则回复 +CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
    • 如果主节点版本是 2.8及后续版本,但 runid 与从节点发送的 runid 不同,或从节点发送的 offset 之后的数据已不在复制积压缓冲区中(在队列中被挤出了),则回复 +FULLRESYNC ,表示要进行全量复制。其中 runid 表示主节点当前的 runid,offset 表示主节点当前的 offset,从节点保存这两个值,以备使用。

命令传播阶段及心跳机制

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING 和 REPLCONF ACK。心跳机制对于主从复制的超时判断、数据安全等有作用。

主 → 从:PING

每隔指定的时间,主节点会向从节点发送 PING 命令,这个 PING 命令的作用,主要是为了让从节点进行超时判断。PING发送的频率由 repl-ping-slave-period 参数控制,单位是秒,默认值是10s。

关于该 PING 命令究竟是由主节点发给从节点,还是相反,有一些争议。因为在 Redis 的官方文档中,对该参数的注释说明是从节点向主节点发送 PING 命令,如下图所示:

image-20200327103052896

但是根据该参数的名称(含有ping-slave)及代码实现,我认为该PING命令是主节点发给从节点的。相关代码如下:

listIter li;
listNode *ln;
robj *ping_argv[1]

if (replication_cron_loops % server.repl_ping_slave_period)== 0){			 
    ping_argv[0] = createStringObject("PING", 4)
    replicationFeedSlaves(server.slaves, server.slaveseldb, ping_argv[0]);
    decrRefCount(ping_argv[0]);

从 → 主:REPLCONF ACK

在命令传播阶段,从节点会向主节点发送 REPLCONF ACK 命令,频率是每秒 1 次;

命令格式为:REPLCONF ACK ,其中 offset 指从节点保存的复制偏移量。

REPLCONF ACK 命令的作用包括:

  • 实时监测主从节点网络状态
  • 检测命令丢失
  • 辅助保证从节点的数量和延迟

实时监测主从节点网络状态

该命令会被主节点用于复制超时的判断。此外,在主节点中使用 info Replication,可以看到其从节点的状态中的lag 值,代表的是主节点上次收到该 REPLCONF ACK 命令的时间间隔,在正常情况下,该值应该是0或1。

image-20200327103052896

检测命令丢失

从节点发送了自身的 offset,主节点会进行 offset 对比,如果从节点数据缺失(如网络丢包),主节点会推送缺失的数据(这里也会利用复制积压缓冲区)。

注意,offset 和复制积压缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;

区别在于:

  • 前者是在断线重连后进行的,
  • 后者是在主从节点没有断线的情况下进行的。

辅助保证从节点的数量和延迟

Redis 主节点中使用 min-slaves-to-write 和 min-slaves-max-lag 参数,来保证主节点在不安全的情况下不会执行写命令。

所谓不安全,是指从节点数量太少,或延迟过高。

例如 min-slaves-to-write 和 min-slaves-max-lag 分别是 3 和 10,含义是如果从节点数量小于 3 个,或所有从节点的延迟值都大于 10s,则主节点拒绝执行写命令。

而这里从节点延迟值的获取,就是通过主节点接收到 REPLCONF ACK 命令的时间来判断的,即前面所说的 info Replication 中的 lag 值。

应用问题

读写分离及其中的问题

在主从复制基础上实现的读写分离,可以实现 Redis 的读负载均衡:

  • 由主节点提供写服务,由一个或多个从节点提供读服务
    • 多个从节点既可以提高数据冗余程度,也可以最大化读负载能力
  • 在读负载较大的应用场景下,可以大大提高 Redis 服务器的并发量。

下面介绍在使用Redis读写分离时,需要注意的问题:

延迟与不一致问题

前面已经讲到,由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度较低,可能的优化措施包括:

  • 优化主从节点之间的网络环境(如在同机房部署);
  • 监控主从节点延迟(通过 offset )判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;
  • 使用集群同时扩展写负载和读负载等。

在命令传播阶段以外的其它情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。

从节点的 slave-serve-stale-data 参数便与此有关,它控制这种情况下从节点的表现:

  • 如果为 yes(默认值),则从节点仍能够响应客户端的命令
  • 如果为 no,则从节点只能响应 info、slaveof 等少数命令。
  • 该参数的设置与应用对数据一致性的要求有关
  • 如果对数据一致性要求很高,则应设置为 no。

数据过期问题

在单机版 Redis 中,存在两种删除策略:

  • 惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
  • 定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。

在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过 Redis 从节点读取数据时,很容易读取到已经过期的数据。

Redis3.2 中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端。将Redis 升级到 3.2 可以解决数据过期问题。

故障切换问题

在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的 Redis 节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写 Redis 数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。

小结

在使用读写分离之前,可以考虑其他方法增加 Redis 的读负载能力:

  • 如尽量优化主节点(减少慢查询、减少持久化等其他情况带来的阻塞等)提高负载能力
  • 使用 Redis 集群同时提高读负载能力和写负载能力等。

如果使用读写分离,可以使用哨兵,使主从节点的故障切换尽可能自动化,并减少对应用程序的侵入。

复制超时问题

主从节点复制超时是导致复制中断的最重要的原因之一。

超时判断意义

在复制连接建立过程中及之后,主从节点都有机制判断连接是否超时,其意义在于:

  • 如果主节点判断连接超时,其会释放相应从节点的连接,从而释放各种资源,否则无效的从节点仍会占用主节点的各种资源(输出缓冲区、带宽、连接等);
    • 释放资源
  • 此外连接超时的判断可以让主节点更准确的知道当前有效从节点的个数,有助于保证数据安全(配合前面讲到的 min-slaves-to-write 等参数)。
    • 获知从节点数量
  • 如果从节点判断连接超时,则可以及时重新建立连接,避免与主节点数据长期的不一致。
    • 避免长时间数据不一致

判断机制

主从复制超时判断的核心,在于 repl-timeout 参数,该参数规定了超时时间的阈值(默认60s),对于主节点和从节点同时有效。

主从节点触发超时的条件分别如下:

  • 主节点:每秒 1 次调用复制定时函数 replicationCron(),在其中判断当前时间距离上次收到各个从节点REPLCONF ACK 的时间,是否超过了 repl-timeout 值,如果超过了则释放相应从节点的连接。

  • 从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:

    • 如果当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过 repl-timeout,则释放与主节点的连接;
    • 如果当前处于数据同步阶段,且收到主节点的 RDB 文件的时间超时,则停止数据同步,释放连接;
    • 如果当前处于命令传播阶段,且距离上次收到主节点的 PING 命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。

需要注意的坑  

下面介绍与复制阶段连接超时有关的一些实际问题:

数据同步阶段:

  • 在主从节点进行全量复制 bgsave 时,主节点需要首先 fork 子进程将当前数据保存到 RDB 文件中,然后再将 RDB 文件通过网络传输到从节点。
  • 如果 RDB 文件过大,主节点在 fork 子进程 + 保存 RDB 文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时。此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连……这是个悲伤的循环。为了避免这种情况的发生。
    • 建议:Redis 单机数据量不要过大,另一方面就是适当增大 repl-timeout 值,具体的大小可以根据 bgsave 耗时来调整

命令传播阶段:

  • 如前所述,在该阶段主节点会向从节点发送 PING 命令,频率由 repl-ping-slave-period 控制;该参数应明显小于 repl-timeout 值(后者至少是前者的几倍)。
  • 否则,如果两个参数相等或接近,网络抖动导致个别PING 命令丢失,此时恰巧主节点也没有向从节点发送数据,那从节点很容易判断超时。

慢查询导致的阻塞:

  • 如果主节点或从节点执行了一些慢查询(如 keys * 或者对大数据的 hgetall 等),导致服务器阻塞;
  • 阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。

复制中断问题

主从节点超时是复制中断的原因之一,除此之外,还有其它情况可能导致复制中断,其中最主要的是复制缓冲区溢出问题。

复制缓冲区溢出

前面曾提到过,在全量复制阶段,主节点会将执行的写命令放到复制缓冲区中,该缓冲区存放的数据包括了以下几个时间段内主节点执行的写命令:

  • bgsave 生成RDB文件
  • RDB 文件由主节点发往从节点
  • 从节点清空老数据并载入RDB文件中的数据。

当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;这种情况可能引起全量复制 → 复制缓冲区溢出导致连接中断 → 重连 → 全量复制 → 复制缓冲区溢出导致连接中断……的循环。

复制缓冲区的大小由 client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds} 配置,默认值为 client-output-buffer-limit slave 256MB 64MB 60,其含义是:如果 buffer 大于 256MB,或者连续 60s 大于 64MB,则主节点会断开与该从节点的连接。该参数是可以通过 config set 命令动态配置的(即不重启 Redis 也可以生效)。

当复制缓冲区溢出时,主节点打印日志如下所示:

image.png

需要注意的是,复制缓冲区是客户端输出缓冲区的一种,主节点会为每一个从节点分别分配复制缓冲区;

而复制积压缓冲区则是一个主节点只有一个,无论它有多少个从节点。

各场景下复制的选择及优化技巧

在介绍了 Redis 复制的种种细节之后,在下面常见的场景中,何时使用部分复制,以及需要注意哪些问题。

第一次建立复制

此时全量复制不可避免,但仍有几点需要注意:

  • 如果主节点的数据量较大,应该尽量避开流量的高峰期,避免造成阻塞;
  • 如果有多个从节点需要建立对主节点的复制,可以考虑将几个从节点错开,避免主节点带宽占用过大。
  • 此外,如果从节点过多,也可以调整主从复制的拓扑结构,由一主多从结构变为树状结构(中间的节点既是其主节点的从节点,也是其从节点的主节点);但使用树状结构应该谨慎——主节点的直接从节点减少,降低了主节点的负担,但是多层从节点的延迟增大,数据一致性变差,且结构复杂,维护相当困难

主节点重启

主节点重启可以分为两种情况来讨论,一种是故障导致宕机,另一种则是有计划的重启。

主节点宕机

主节点宕机重启后,Runid 会发生变化,因此不能进行部分复制,只能全量复制。实际上在主节点宕机的情况下,应进行故障转移处理,将其中的一个从节点升级为主节点,其它从节点从新的主节点进行复制;且故障转移应尽量的自动化(哨兵模式)。

安全重启:debug reload

在一些场景下,可能希望对主节点进行重启,例如主节点内存碎片率过高,或者希望调整一些只能在启动时调整的参数。如果使用普通的手段重启主节点,会使得 runid 发生变化,可能导致不必要的全量复制。

为了解决这个问题,Redis提供了debug reload的重启方式:重启后,主节点的 runid 和 offset 都不受影响,避免了全量复制。

如下图所示,debug reload重启后 runid 和 offset 都未受影响:

image.png

但 debug reload 是一柄双刃剑:它会清空当前内存中的数据,重新从 RDB 文件中加载,这个过程会导致主节点的阻塞,因此也需要谨慎。

从节点重启

从节点宕机重启后,其保存的主节点的 runid 会丢失,因此即使再次执行 slaveof,也无法进行部分复制。

网络中断

如果主从节点之间出现网络问题,造成短时间内网络中断,可以分为多种情况讨论:

  • 网络问题时间极为短暂,只造成了短暂的丢包,主从节点都没有判定超时(未触发repl-timeout)。此时只需要通过 REPLCONF ACK 来补充丢失的数据即可。
  • 网络问题时间很长,主从节点判断超时(触发了repl-timeout),且丢失的数据过多,超过了复制积压缓冲区所能存储的范围。此时主从节点无法进行部分复制,只能进行全量复制。
    • 为了尽可能避免这种情况发生,应该根据实际情况适当调整复制积压缓冲区的大小;此外及时发现并修复网络中断,也可以减少全量复制。
  • 介于前述两种情况之间,主从节点判断超时,且丢失的数据仍然都在复制积压缓冲区中。此时主从节点可以进行部分复制。

总结

全量复制及部分复制

选择全量复制,还是部分复制,由双方一致决定:

  • 部分复制的条件
    • redis 2.8 及后续版本
    • 从节点发送 runid 与主节点的 runid 一致
    • 主从节点 offet 之间的差距的数据在复制积压缓冲区
      • 因为在命令传播阶段,主节点除了将写命令发送给从节点外,还会发送一份到复制积压缓冲区,一旦这个过程还没有来得及传播给从节点,而从节点宕机,那么这份数据会保留在缓冲区。
    • redis 4.0 及后续本
      • 节点执行 shutdown、save 命令会将 master_replid 和 master_repl_offset 保存到 RDB 文件中
      • redis 加载 RDB 文件,会专门处理文件中辅助字段(AUX fields)信息,把其中 repl_id 和 repl_offset加载到实例中,分别赋给 master_replid 和 master_repl_offset 两个变量值
        • 若是开启了 AOF,Redis 优先加载 AOF 文件,但是由于aof文件中没有复制信息,所以导致重启后从实例依旧使用全量复制
      • 从节点发送给主节点 master_replid 和 master_repl_offset+1,从节点同时满足以下两条件
        • 从节点上报 master_replid 串,与主节点的 master_replid1 或 master_replid2 有一个相等,用于判断主从未发生改变
        • 从节点上报的 master_repl_offset+1 字节,还存在于主节点的复制积压缓冲区中,用于判断从库丢失部分是否在复制缓冲区中
  • 全量复制的条件
    • 从节点首次同步主节点数据
    • 主从节点重启
      • 主节点 runid 会变
      • 从节点 runid 会丢失
    • redis 版本 < 4.0,pync 有缺点
    • 部分复制失败,主从失联超过 repl-backlog-ttl(默认60分钟),导致复制积压缓冲区数据被清空

相关配置

配置描述
slaveof 该注释默认注释掉,即Redis服务器默认都是主节点。
Redis 启动时,作用是建立复制关系,开启了该配置的 Redis 服务器在启动后成为从节点。
repl-timeout默认值:60。
复制超时,必须确保此值大于为 repl-ping-replica-period 指定的值,否则每次主副本和副本之间的通信量较低时都会检测到超时
repl-diskless-sync默认:no。
控制主节点是否使用 diskless 复制(无盘复制)。
所谓diskless复制,是指在全量复制时,主节点不再先把数据写入RDB文件,而是直接写入slave的socket中,整个过程中不涉及硬盘;
diskless复制在磁盘IO很慢而网速很快时更有优势。需要注意的是,截至 Redis3.0,diskless复制处于实验阶段,默认是关闭的。
repl-diskless-sync-delay单位是秒,默认5s。
该配置作用于全量复制阶段,当主节点使用 diskless 复制时,该配置决定主节点向从节点发送之前停顿的时间,
只有当 diskless 复制打开时有效,之所以设置停顿时间,是基于以下两个考虑:
a.向slave的socket的传输一旦开始,新连接的slave只能等待当前数据传输结束,才能开始新的数据传输;
b.多个从节点有较大的概率在短时间内建立主从复制。
client-output-buffer-limit slave默认值:256MB 64MB 60。
与全量复制阶段主节点的缓冲区大小有关,见前面的介绍。
repl-disable-tcp-nodelay默认值:no。
与命令传播阶段的延迟有关
masterauth 默认没有设置,与连接建立阶段的身份验证有关
repl-ping-slave-period默认值:10s。
与命令传播阶段主从节点的超时判断有关
repl-backlog-size默认值:1MB。
复制积压缓冲区的大小
repl-backlog-ttl默认:3600 秒。
当主节点没有从节点时,复制积压缓冲区保留的时间,这样当断开的从节点重新连进来时,可以进行部分复制。如果设置为 0,则永远不会释放复制积压缓冲区。
repl-backlog-size复制积压缓冲区队列大小,默认:1 MB
min-slaves-to-write默认值:3。
规定了主节点的最小从节点数目
min-slaves-max-lag默认值:10。
规定了主节点的对应的最大延迟
slave-serve-stale-data默认值:yes。
与从节点数据陈旧时是否响应客户端命令有关
slave-read-only默认值:yes。
从节点是否只读,由于从节点开启写操作容易导致主从节点的数据不一致,因此该配置尽量不要修改。
repl_ping_replica_period默认值是 10 秒。
副本按预先定义的时间间隔向服务器发送 ping 信号。

相关命令

命令描述
info replication查看主从复制信息
info server查看当前服务信息
更新时间:2020-05-25 09:55:28

本文由 工匠猫 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://huasio.com/archives/redis-master-slave-copy
最后更新:2020-05-25 09:55:28

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×