复制功能是高可用 Redis 的基础,同时也是 Redis 日常运维的常见问题。深刻理解复制的工作原理与使用姿势对我们的日常开发和运维都非常重要。

本文从同步的过程讲起,重点是全量同步的可能问题和运维方法。

本文大部分摘录自:Redis开发与运维

同步的过程

slave 的 slaveof 命令执行之后,同步过程便开始了:

1. 保存主节点的信息

执行 slaveof 后,slave 只保存 master 的地址信息便直接返回,这时复制流程开没有开始。

在 slave 上执行 info replication 可以看到如下信息:

master_host: 127.0.0.1
master_port: 6379
master_link_status: down

注意这个 master_link_status 是下线的状态。

2. 主从建立 socket 连接

slave 内部通过每秒运行的定时任务维护复制的逻辑。当定时任务发现新的 master 之后(先抑制住内心的激动:终于不用干活了),会尝试与 master 建立连接。

如果 slave 建立连接失败,定时任务会无限重试直到成功或者 slave no one 命令。

slave 的 info replication 输出的 master_link_down_since_seconds 会记录与 master 连接失败的系统时间。

3. 发送 ping 命令

连接建立成功后,slave 会发送 ping 请求进行首次通信,就像加了微信之后先发个“在吗”一样,检测一下 socket 是否可用以及 master 当前能否处理请求。

如果 slave 没有收到 pong 的回复,就会很知趣地断开复制连接,不再死缠烂打。

4. 权限验证

如果 master 设置了 requirepass 参数,则 slave 必须配置 masterauth 参数才能通过验证。

5. 同步数据集

主从正常通信后,对于首次建立连接的场景,master 会把全部的数据发送给 slave。Redis 在 2.8 之后采用新的复制命令:psync ,原来的 sync 仍然支持,只是为了保持兼容性。新版同步分两种,即全量同步和部分同步,本文的重点就是全量同步。

6. 命令持续复制

当 master 把当前的数据发给 slave 之后,便完成了复制的建立流程。接下来,master 会持续地把写命令发送给 slave,保证主从数据的一致性。

全量同步和部分同步

Reids 使用 psync 命令完成同步,同步过程分为两种:

  • 全量同步:一般用于初次复制的场景。
  • 部分同步:用于处理在主从复制中因为网络闪断等原因造成的数据丢失。当 slave 再次连上 master 之后,尽可能地补发丢失部分的数据给 slave,以此来避免全量同步的开销。

psync

slave 使用 psync 命令完成部分同步和全量同步,psync 命令需要以下组件的支持:

  • 主从节点的各自复制偏移量
  • master 复制积压缓冲区
  • master 的运行 ID

psync 命令的格式如下 psync {runid} {offset} ,参数的含义如下:

  • runid :slave 要复制 master 的运行的 ID。
  • offset :当前 slave 已经复制的数据偏移量。

流程说明:

  1. slave 发送 psync 命令给 master。
  2. master 根据 psync 参数和自身情况决定相应结果。
    • +FULLRESYNC {runid} {offset} 表示 slave 将触发全量同步。
    • +CONTINUE 表示 slave 将触发部分同步。
    • +ERR 表示 master 版本低于2.8,无法识别 psync 命令。

全量同步

全量同步的流程说明:

  1. 发送 psync 命令进行数据同步,如果是第一次进行复制,slave 没有复制偏移量和主节点的运行 ID,只能发送 psync -1
  2. master 根据 slave 的 psync 命令解析出全量同步的请求,并回复 +FULLRESYNC
  3. slave 接到 master 的响应数据,保存运行 ID 和偏移量,并打印日志:
Partial resynchronization not possible (no cached master)
Full resync from master: xxxxx
  1. master 执行 bgsave 保存 RDB 文件到本地。
  2. master 发送 RDB 文件给 slave,slave 把接收到的 RDB 保存在本地,接受完 RDB 后,slave 的日志中可以看到:
16:24:01.057 * MASTER <-> SLAVE sync: receiving 2778899 bytes from master
  1. slave 开始接收 RDB 文件到接收完成这u但时间,master 仍然相应读写命令,并将它们保存在客户端缓冲区。slave 加载 RDB 完成后,master 把缓冲区中的数据再发给 slave。
  2. slave 接收完 master 发来的全部数据后,会清空自身旧数据,对应日志如下:
16:24:02:034 * MASTER <-> SLAVE sync: Flushing old data
  1. slave 清空数据后,开始加载 RDB 快照,对应日志如下:
16:24:03:034 * MASTER <-> SLAVE sync: Loading DB in memory
16:24:04:034 * MASTER <-> SLAVE sync: Finished with sucess
  1. slave 加载 RDB 完成后,如果当前节点开启了 AOF,会立刻做 bgrewriteaof 操作,为了保证全量同步后 AOF 文件立即可用。

全量同步的可能问题

全量同步是一个非常消耗资源的操作,它的时间开销包括:

  • master bgsave 的时间
  • RDB 文件的网络传输时间
  • slave 清空数据时间
  • slave 加载 RDB 的时间
  • 可能的 AOF 重写时间

除此之外,全量同步可能引发的问题还有:

网络传输超时

RDB 文件超过 6GB 以上时,要格外小心,传输文件会非常耗时。通过分析 Full resync 和 MASTER <-> SLAVE 这两行日志之间的时间差,可以算出 RDB 文件的传输时间。

如果传输时间超过 repl-timeout 配置的值(默认 60 秒),slave 将放弃接收 RDB 文件并清理已下载的临时文件,导致同步失败。此时打印的日志:

#Timeout receiving bulk data from MASTER...If the problem persists try to set the 'repl-time' parameter in redis.conf to a larger value.

缓冲区溢出

如果 master 创建和传输 RDB 文件的时间较长,对于高流量的写入场景,容易造成客户端缓冲区的溢出。

默认配置为 client-output-buffer-limit slave 256MB 64MB 60 ,如果 60 秒内缓冲区消耗持续大于 64MB 或直接超过 256MB,master 将关闭客户端连接。

对应日志如下:

M 27 May 12:13:33.669 # Client id=2 addr=127.0.0.1:24555 age=1 idle=1 flags=S qbuf=0 qbuf-free=0 obl=18824 oll=21382 omem=26844240 events=r cmd=psync scheduled to be closed ASAP for overcoming of output buffer limits.

这时候要根据 master 数据量和写命令并发量调整 client-output-buffer-limit slave 参数。

延迟问题

全量同步期间可能会引发性能问题,来源可能有两个:

  • fork 操作
  • swap

为了在后台生成 RDB 文件,master 必须 fork 一个子进程来做这件事。fork 是一个很消耗资源的操作,涉及到进程间的拷贝。在涉及到虚拟内存的页表操作时,延迟尤甚。

更多的内容可以看这里:Latency generated by fork

另外,如果页被频繁在内存和磁盘之间 swap,也会引发延迟。RDB 和 AOF 都可能产生大文件,而大量的 I/O 需要用到系统缓存,导致了 swap。之前的这篇 页错误引发的 Redis 延迟 说的就是这个事儿。

规避全量同步

如何规避全量同步是需要重点关注的运维点,下面针对需要全量同步的场景逐个分析。

第一次建立连接:这时必须进行全量同步,无法避免。可以在低峰时操作,或者避免使用大数据量的 Redis 节点。

节点运行 ID 不匹配:主从关系建立后,slave 会保存 master 的运行 ID。如果 master 发生故障重启,运行 ID 会改变。slave 发现该变化之后,会认为自己从一个新 master 复制数据。对于这种情况,可以手动提升 slave 为主节点。采用 Sentinel 或集群方案也是不错的选择。

复制缓冲区不足:主从失联之后,slave 再次连上 master,会请求部分同步。但是,如果请求的偏移量不在 master 的缓冲区里,那么部分同步将退化为全量同步。为避免这种情况,要根据写的数据量,合理规划缓冲区的大小。之前的这篇 Redis 同步引发的系统过载 讲述的就是由此而引发的系统过载问题。