《Redis开发与运维》读书笔记(六)复制

第六章 复制

分布式系统中为了解决单点问题,会把数据复制多个副本部署到其它机器,来满足故障恢复和负载均衡等需求。对于Redis来说,复制功能是高可用的基础。

6.1 配置

6.1.1 建立复制

参与复制的Redis实例划分为主节点和从节点(默认都是主节点)。每个从节点只能有一个主节点,主节点可以同时有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点

配置方式:

  1. 在配置文件中加入 slaveof {masterHost} {masterPort} 随Redis启动生效。
  2. 在 redis-server 启动命令后加入 --slaveof {masterHost} {masterPort} 生效。
  3. 直接使用命令:slaveof {masterHost} {masterPort} 生效。

slaveof配置都是在从节点发起,针对主节点的任何修改都会同步到从节点。

1
2
3
4
5
6
7
8
9
# 6380为从节点,6379为主节点
127.0.0.1:6380>slaveof 127.0.0.1 6379

127.0.0.1:6379>set hello redis
OK
127.0.0.1:6379>get hello
"redis"
127.0.0.1:6380>get hello
"redis"

slaveof本身是异步命令,节点只保存主节点信息后返回,后续复制流程在节点内部异步执行。

查看复制相关状态:

1
127.0.0.1:6379> info replication

6.1.2 断开复制

断开复制不会影响原有数据,命令:

1
slaveof no one

流程:

  1. 断开与主节点的复制关系。
  2. 从节点晋升主节点。

切换主节点会删除原有数据,操作:

1
slaveof {newMasterIp} {newMasterPort}

流程:

  1. 断开与主节点的复制关系。
  2. 与新主节点建立复制关系。
  3. 删除从节点当前所有数据。
  4. 对新主节点进行复制操作。

6.1.3 安全性

设置 requirepass 参数进行密码验证,客户端访问需要使用auth命令进行校验。从节点和主节点的复制连接是通过一个特殊标识的客户端完成,所以需要配置从节点的 masterauth 与主节点密码一致。

6.1.4 只读

默认下,从节点会设置为只读模式:

1
slave-read-only=yes

非必要不要修改从节点数据造成主从不一致。

6.1.5 传输延迟

Redis提供了 repl-disable-tcp-nodelay 参数来控制是否关闭 TCP_NODELAY,默认关闭。

  • 关闭时,主节点产生的命令数据无论大小都会及时的发送给从节点,主从延迟很小,但增加了网络带宽的消耗。适用于主从节点网络连接良好,如同机架或同机房部署。
  • 开启时,主节点会合并较小的TCP数据包来节省带宽。默认发送间隔取决于Linux内核,一般为40毫秒。适用于主从网络环境复杂或带宽紧张的场景,如跨机房部署。

6.2 拓扑

Redis的复制拓扑结构支持单层或多层复制关系。

(1)一主一从结构

  • 最简单,用于主节点宕机时从节点提供故障转移支持。
  • 当写命令并发较高且支持持久化时,可以在从节点开启AOF,这样既保证了数据安全性,同时避免了持久化对主节点的性能干扰。但主节点关闭持久化功能后,脱机时要避免自动重启操作,因为主节点未开启持久化自动重启后数据集为空,此时从节点继续复制会导致原有数据被清空。安全的做法是在从节点执行 slaveof no one 操作断开和主节点的复制关系,再重启主节点

(2)一主多从结构

  • 又叫星型拓扑结构,使应用端可以利用多个从节点实现读写分离。
  • 对于读操作占比较大的场景,可以把读命令发送到从节点来减轻主节点压力。
  • 耗时的读操作也可以放到从节点执行,防止慢查询阻塞主节点。
  • 但对于写操作并发比较高的场景,多个从节点会导致主节点写命令多次发送从而加倍消耗网络带宽,也加重了主节点的负载影响服务稳定性。

(3)树状主从结构

  • 又叫树状拓扑结构,使从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下复制。
  • 引入复制中间层,可以有效降低主节点的负载和需要传送给从节点的数据量。
  • 当主节点需要挂载多个从节点时避免了对主节点的性能干扰。


6.3 原理

6.3.1 复制过程

流程:

  1. 保存主节点信息。

    slaveof执行后,从节点只保存主节点信息便返回,此时复制连接还未建立,info replication:

    1
    2
    3
    4
    master_host:127.0.0.1
    master_port:6379
    # 下线状态
    master_link_status:down

    Redis此时会打印日志:

    1
    SLAVE OF 127.0.0.1:6379 enabled (...)

    通过日志方便定位发送slaveof命令的客户端。

  2. 从节点内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接。

    从节点会建立一个socket,专门用于接受主节点发送的复制命令,连接成功后打印:

    1
    2
    * Connecting to MASTER 127.0.0.1:6379
    * MASTER <-> SLAVE sync started

    若从节点无法建立连接,定时任务会无限重试直到成功或执行取消复制命令。

    连接失败时,可以查看info replication的 master_link_down_since_seconds 指标,其记录了与主节点连接失败的系统时间。从节点也会打印失败日志:

    1
    # Error condition on socket for SYNC: {socket_error_reason}
  3. 发送PING命令。

    连接成功建立后首次通信,目的是:

    • 检测主从之间socket是否可用。
    • 检测主节点当前是否可接受处理命令。

    从节点若没收到pong回复或超时,会断开复制连接,下次定时任务会重连。

    PING命令成功后,打印日志:

    1
    Master replied to PING, replication can continue...
  4. 权限验证。若主节点设置了requirepass参数,从节点需要配置 masterauth 与主节点密码一致。若验证失败则复制过程终止,从节点重新发起复制流程。

  5. 同步数据集。此时复制连接可以正常通信,首次建立复制,主节点会把持有的数据全部发送给从节点,该步骤是最耗时的过程。Redis在2.8版本后采用新复制命令psync进行数据同步,该同步分为全量和部分同步两种情况。

  6. 命令持续复制。完成首次复制后,主节点会持续的把写命令发送给从节点来保证主从数据一致。

6.3.2 数据同步

Redis在2.8版本后使用psync命令来完成主从数据同步,过程分为:

  • 全量复制:一般用于初次复制场景,会把主节点数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大开销。
  • 部分复制:用于处理在主从复制中因网络闪断等原因造成数据丢失的场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。补发的数据远远小于全量数据。

psync命令需要以下组件支持:

  • 主从节点各自复制偏移量。
  • 主节点复制积压缓冲区。
  • 主节点运行id。

(1)复制偏移量

  • 参与复制的主从节点都会维护自身复制偏移量。
  • 主节点处理完写入命令后,会把命令的字节长度做累加记录。通过info replication的 master_repl_offset 指标查看。
  • 从节点每秒上报自身的复制偏移量,因此主节点也会保存。从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量。通过info replication的 slave_repl_offset 指标查看。
  • 通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致

(2)复制积压缓冲区

复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为1MB。当主节点有连接的从节点时创建,主节点响应命令时,不但会发送给从节点,还要写入该缓冲区。

缓冲区本质是先进先出的定长队列,能实现保存最近已复制数据的功能,可以用于部分复制和复制命令丢失的数据补救。通过info replication查看:

1
2
3
4
5
6
7
8
127.0.0.1:6379> info replication
# Replication
role:master
...
repl_backlog_active:1 // 开启复制缓冲区
repl_backlog_size:1048576 // 缓冲区最大长度
repl_backlog_first_byte_offset:7479 // 起始偏移量,计算当前缓冲区可用范围
repl_backlog_histlen:1048576 // 已保存数据的有效长度

可用偏移量范围 = [repl_backlog_first_byte_offset, repl_backlog_first_byte_offset + repl_backlog_histlen]。

(3)主节点运行ID

每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。主要作用是用来唯一识别Redis节点,若只使用ip+port的方式识别主节点,当主节点重启变更了整体数据集(如替换RDB/AOF文件),从节点再基于偏移量复制数据是不安全的,当运行ID发生变化时将做全量复制。

可以通过info server查看当前节点的运行ID(run_id)。Redis关闭再启动会导致运行ID变化。如何在不影响运行ID的情况下重启?比如一些内存相关的配置需要重启Redis才能优化已存在的数据,此时可以使用debug reload命令重新加载RDB并保持运行ID不变。此命令会阻塞Redis节点主线程,阻塞期间会生成本地RDB快照并清空数据后再加载RDB文件。对于大数据量的主节点和无法容忍阻塞的场景需要谨慎使用。

(4)psync命令

命令格式:

1
psync {runId} {offset}
  • runId:从节点复制主节点的运行ID。
  • offset:当前从节点已复制的数据偏移量。

流程:

  1. 从节点发送psync命令给主节点,runId若无则默认为?,参数offset若是第一次参与复制则默认值为-1。
  2. 主节点根据psync参数和自身数据情况决定响应结果:
    • 如果回复 +FULLRESYNC {runId} {offset} ,那么从节点将触发全量复制流程。
    • 如果回复 +CONTINUE,从节点将触发部分复制流程。
    • 如果回复 +ERR,说明主节点版本低于2.8,无法识别psync命令,从节点将发送旧版本的sync命令触发全量复制流程。

6.3.3 全量复制

全量复制的流程:

  1. 发送psync命令进行数据同步,由于是第一次复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync ? -1。

  2. 主节点解析出当前为全量复制,回复 +FULLRESYNC响应。

  3. 从节点接收主节点的响应数据保存运行ID和偏移量offset,该步骤打印日志:

    1
    2
    Partial resynchronization not possible (no cached master)
    Full resync from master: .....
  4. 主节点执行bgsave保存RDB文件到本地,打印日志:

    1
    2
    3
    4
    5
    M * Full resync requested by slave 127.0.0.1:6380
    M * Starting BGSAVE for SYNC with target: disk
    C * Background saving started by pid 32618
    C * RDB: 0 MB of memory used by copy-on-write
    M * Background saving terminated with success

    其中M表示当前为主节点日志,S为从节点,C为子进程日志。

  5. 主节点发送RDB文件给从节点,从节点将其保存在本地并作为从节点的数据文件,打印相关日志:

    1
    16:24:03.057 * MASTER <-> SLAVE sync: receiving 24777842 bytes from master

    如果主节点数据量特别大,该过程会很耗时,上述日志可以看到数据大小,通过Full resync和这一行的时间差也可以算出从创建RDB文件到传输的总耗时。如果耗时超过repl-timeout(默认60秒)配置,从节点会放弃接收RDB文件并清理已下载的内容,从而导致全量复制失败。

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

    例如千兆网卡的机器理论网络带宽峰值为100MB,不考虑其它情况下6GB的文件最少需要60s传输时间。默认配置下很容易出现主从数据同步超时

  6. 从节点接收RDB快照期间,主节点仍然响应读写命令,此期间的命令会写入复制客户端缓冲区内。从节点接收完毕,主节点再发送缓冲区内容,保证主从一致。如果RDB传输时间过长,很容易导致缓冲区溢出,默认配置为 client-output-buffer-limit slave 256MB 64MB 60 ,如果60秒内消耗持续大于64MB或直接超过256MB时,主节点会直接关闭复制客户端连接,导致全量同步失败,对应日志:

    1
    ... # Client ... scheduled to be closed ASAP for overcoming of output buffer limits.

    运维人员需要根据主节点数据量和写命令并发量调整配置。

    主节点发送完数据后打印日志,此时从节点还未完成:

    1
    Synchronization with slave 127.0.0.1:6380 succeeded
  7. 从节点接收完全部数据后会清空自身的旧数据,打印日志:

    1
    ... * MASTER <-> SLAVE sync: Flushing old data
  8. 从节点清空数据后开始加载RDB文件,当文件较大时,该步骤仍会比较耗时。可以通过日志时间戳判断耗时:

    1
    2
    16:24:03.578 * MASTER <-> SLAVE sync: Loading DB in memory
    16:24:06.756 * MASTER <-> SLAVE sync: Finished with success

    读写分离的场景下,从节点也负责影响读命令。若从节点处于全量复制阶段或复制中断,此时响应的读命令可能会读取到过期或错误的数据。Redis提供了参数 slave-serve-stale-data(默认开启),当关闭时从节点除了info或slaveof命令外,其余命令只返回”SYNC with master in progress“信息。

  9. 从节点成功加载完RDB后,若当前节点开启了AOF持久化,会立刻做 bgrewriteaof 操作,从而使全量复制后AOF持久化文件可以立即可用。

全量复制是个十分耗时的操作,主要开销包括:

  • 主节点bgsave的时间
  • RDB文件网络传输的时间
  • 从节点清空数据的时间
  • 从节点加载RDB的时间
  • 可能的AOF重写时间

线上6GB左右数据量的主节点,全量复制总耗时约为2分钟。所以除了首次复制外,应该避免全量复制的发生。

6.3.4 部分复制

使用 psync {runId} {offset} 实现,当从节点正在复制主节点时,若出现网络闪断或命令丢失等异常情况时,从节点会要求主节点补发丢失的命令数据。

流程:

  1. 主从节点之间网络出现中断,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接,打印日志:

    1
    2
    M # Disconnecting timeout slave: 127.0.0.1:6380
    M # Connection with slave 127.0.0.1:6380 lost.

    若从节点没有宕机,也会打印与主节点断开连接的日志:

    1
    2
    S # Connection with master lost.
    S * Caching the disconnected master state.
  2. 主从连接中断期间主节点仍然响应命令,但无法发送给从节点,期间的命令仍会保存在缓冲区,默认最大缓存1MB。

  3. 当主从节点网络恢复后,从节点会再次连接上主节点,打印日志:

    1
    2
    3
    4
    S * Connecting to MASTER 127.0.0.1:6379
    S * MASTER <-> SLAVE sync started
    S * Non blocking connect for SYNC fired the event
    S * Master replied to PING, replication can continue...
  4. 主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID,会将其作为psync参数,向主节点要求做部分复制,打印日志:

    1
    S * Trying a partial resynchronization (request ...).
  5. 主节点接收到psync命令后,首先核对runId是否与自己一致,然后根据参数offset查找缓冲区,若数据存在则对从节点发送 +CONTINUE响应。从节点接收到后打印日志:

    1
    2
    S * Successful partial resynchronization with master.
    S * MASTER <-> SLAVE sync: Master acceptrd a Partial Resynchronization.
  6. 主节点根据偏移量把复制积压缓冲区的数据发送给从节点,发送的数据量可以在日志获取:

    1
    2
    M * Slave 127.0.0.1:6380 asks for synchronization
    M * Partial resynchronization request from 127.0.0.1:6380 accepted. Sending 78 bytes of backlog starting from offset 49769216.

6.3.5 心跳

主从节点建立复制后,彼此会发送心跳命令:

  1. 主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过client list查看复制相关客户端信息,主节点的连接状态为flags=M,从节点则为flags=S。
  2. 主节点默认每隔10s对从节点发送一次PING命令,判断从节点的存活和连接状态,可以通过 repl-ping-slave-period 参数控制。
  3. 从节点在主线程中每隔1s发送 replconf ack {offset} 命令,给主节点上报自己当前的复制偏移量。该命令的作用包括:
    • 实时监测主从节点网络状态。
    • 上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据。
    • 实现保证从节点的数量和延迟性功能,通过 min-slaves-to-write、min-slaves-max-lag 参数配置。

主节点根据replconf命令来判断从节点超时时间,在info replication中的lag信息表示与从节点最后一次通信延迟的秒数,正常应该介于0到1间。如果超过repl-timeout值,则判定从节点下线并断开复制连接。为了降低主从延迟,一般会把主从节点部署在相同的机房区域。

6.3.6 异步复制

写命令发送给从节点是异步完成的,即主节点自身处理完写命令后直接返回客户端,无需等待从节点复制完成。所以从节点的数据相对主节点会有延迟,延迟多少字节可以在主节点执行info replication查看相关指标:

1
2
slave0:ip=127.0.0.1,port=6380,state=online,offset=841,lag=1
master_repl_offset:841

offset是从节点的复制偏移量,master_repl_offset表示当前主节点的复制偏移量,二者之差就是延迟量。


6.4 开发与运维中的问题

6.4.1 读写分离

使用从节点响应读请求时,业务端可能遇到的问题:

  • 复制数据延迟。
  • 读到过期数据。
  • 从节点故障。

(1)复制数据延迟

该延迟因为异步复制的特性无法避免,取决于网络带宽和命令阻塞情况。对于无法容忍大量延迟的场景,可以编写外部监控程序监听主从节点的复制偏移量,延迟较大时触发报警或通知客户端避免读取延迟过高的从节点。

  1. 监控程序定期检查主从节点的偏移量,主节点偏移量在info replication的master_repl_offset指标记录中。从节点偏移量查询主节点的slave0字段的offset指标。两者差值是主从节点延迟的字节量。
  2. 当延迟字节过高时,如超过10MB。监控程序触发报警并通知客户端,可以采用Zookeeper的监听回调机制来实现客户端通知。
  3. 客户端接到具体的从节点延迟通知后,修改读命令路由到其它从节点或主节点上。当延迟恢复后,再次通知客户端,恢复从节点的读命令请求。

该方案成本较高,需要单独修改适配Redis的客户端类库。客户端需要识别读写请求并自动路由,还需要维护故障和恢复的通知。

(2)读到过期数据

主节点存储大量超时的数据时,需要内部维护过期数据的删除策略:

  • 惰性删除:主节点每次处理读取命令都要检查键是否超时,超时则执行del命令,之后del也会异步发送给从节点。为了保证复制的一致性,从节点永不主动删除超时数据。
  • 定时删除:主节点在内部定时任务会循环采样一定数量的键,当发现采样的键过期时,执行del命令,再同步给从节点。如果大量数据超时,主节点采样速度跟不上过期速度,且主节点没有读取过期键的操作,那么从节点将无法收到del命令。3.2版本从节点读取数据之前会检查键的过期时间来决定是否返回数据。

(3)从节点故障

需要在客户端维护可用从节点列表,当从节点故障后立刻切换到其它节点。需要开发者改造客户端类库。

Redis尽量要在主节点先做充分优化,如解决慢查询、持久化阻塞和合理使用数据结构等。主节点优化空间不大时再考虑扩展从节点的优化方案,做读写分离时可以考虑使用Redis Cluster等分布式解决方案,从而不只扩展了读性能,还可以扩展写性能和可支撑的数据规模,并且一致性和故障转移也可以保证,对于客户端的维护逻辑也相对简单。

6.4.2 主从配置不一致

有些配置主从之间是可以不一致的,比如主节点关闭AOF,从节点开启。但对于内存的配置要主从一致,如maxmemory、hash-max-ziplist-entries等。如果从节点maxmemory小于主节点,复制的数据量超过限制,会根据maxmemory-policy策略进行内存溢出控制,导致从节点数据丢失,但主从复制流程正常进行,修复该问题只能手动进行全量复制。压缩列表相关参数不一致时会导致主从节点相同数据内存占用大不相同。

6.4.3 规避全量复制

全量复制的各个场景进行避免:

  • 首次建立复制:该情况无法避免。
    • 在对数据量较大且流量较高的主节点添加从节点时,建议在低峰进行操作,或者干脆规避使用大数量的节点。
  • 节点运行ID不匹配:当主节点故障重启,其运行ID会改变,从节点发现主节点运行ID不匹配会认为自己复制的是一个新节点导致进行全量复制。
    • 避免这种情况需要在架构上规避,如提供故障转移功能,在主节点发生故障后,手动提升从节点为主节点,或采用支持自动故障转移的哨兵或集群方案。
  • 复制积压缓冲区不足:当主从节点网络中断后,从节点再次连接上主节点时会发送 psync 请求部分复制,如果请求的偏移量不在主节点的缓冲区内,则无法提供给从节点数据,导致部分复制退化为全量复制。
    • 避免这种情况需要根据网络中断时长,写命令数据量分析出合理的积压缓冲区大小。
    • 网络中断一般有闪断、机房割接、网络分区等情况。中断时长一般在分钟级别。
    • 写命令数据量可以统计高峰期主节点每秒的master_repl_offset差值获取。
    • 积压缓冲区默认为1MB,对于大流量场景明显不够,加大缓冲区,保证 repl_backlog_size > net_break_time * write_size_per_minute。

6.4.4 规避复制风暴

复制风暴指大量从节点对同一主节点或同台机器的多个主节点短时间内发起全量复制的场景。会导致相应主节点所在机器造成CPU、内存、带宽等大量开销。

复制风暴的场景:

  • 单主节点复制风暴:

    一般发生在主节点挂载多个从节点的场景。主节点重启恢复后,从节点发起全量复制流程,主节点创建RDB快照,在创建完毕前若有多个从节点都尝试进行全量复制,它们会共享这份RDB。但同时向多个从节点发送RDB文件会使带宽消耗严重,导致主节点延迟变大,极端时使主从节点断开连接。

    • 解决方案:首先减少主节点挂载的从节点数量,或采用树状的复制结构,加入中间层从节点保护主节点。网络开销分摊给了中间层,但带来了运维的复杂性,增加了手动和自动处理故障转移的难度。
  • 单机器复制风暴:

    Redis单线程,所以单台机器常常会部署多个实例。如果这台机器出现故障或网络长时间中断,重启恢复后会有大量从节点对其发起全量复制,导致当前机器带宽耗尽。

    解决方案:

    • 应该把主节点尽量分散到多台机器上,避免在单台机器上部署过多的节点。
    • 当主节点所在机器发生故障后,提供故障转移机制。


参考:

🔗 《Redis开发与运维》