在分布式系统中为了解决单点问题, 通常会把数据复制多个副本部署到其他机器, 满足故障恢复和负载均衡等需求。 Redis也是如此, 它为我们提
供了复制功能, 实现了相同数据的多个Redis副本。 复制功能是高可用Redis的基础, 哨兵和集群都是在复制的基础上实现高可用的。
·介绍复制的使用方式: 如何建立或断开复制、 安全性、 只读等。
·说明复制可支持的拓扑结构, 以及每个拓扑结构的适用场景。
·分析复制的原理, 包括: 建立复制、 全量复制、 部分复制、 心跳等。
·介绍复制过程中常见的开发和运维问题: 读写分离、 数据不一致、 规避全量复制等

1 配置

1.1 建立复制
参与复制的Redis实例划分为主节点(master) 和从节点(slave) 。 默认情况下, Redis都是主节点。 每个从节点只能有一个主节点, 而主节点可以
同时具有多个从节点。 复制的数据流是单向的, 只能由主节点复制到从节点。 配置复制的方式有以下三种:
1) 在配置文件中加入slaveof{masterHost}{masterPort}随Redis启动生效。
2) 在redis-server启动命令后加入--slaveof{masterHost}{masterPort}生效。
3) 直接使用命令: slaveof{masterHost}{masterPort}生效。

127.0.0.1:6380>slaveof 127.0.0.1 6379

1.2 断开复制
slaveof命令不但可以建立复制, 还可以在从节点执行slaveof no one来断开与主节点复制关系。

从一个redis复制到另外一个redis redis 复制_redis

断开复制主要流程:

1) 断开与主节点复制关系。

2) 从节点晋升为主节点。

通过slaveof命令还可以实现切主操作, 所谓切主是指把当前从节点对主节点的复制切换到另一个主节点。 执行slaveof{newMasterIp}

从一个redis复制到另外一个redis redis 复制_Redis_02

切主操作流程如下:
1) 断开与旧主节点复制关系。
2) 与新主节点建立复制关系。
3) 删除从节点当前所有数据。
4) 对新主节点进行复制操作。

1.4 只读
默认情况下, 从节点使用slave-read-only=yes配置为只读模式。 由于复制只能从主节点到从节点, 对于从节点的任何修改主节点都无法感知, 修改
从节点会造成主从数据不一致。 因此建议线上不要修改从节点的只读模式。
1.5 传输延迟
主从节点一般部署在不同机器上, 复制时的网络延迟就成为需要考虑的问题, Redis为我们提供了repl-disable-tcp-nodelay参数用于控制是否关闭
TCP_NODELAY, 默认关闭, 说明如下:
·当关闭时, 主节点产生的命令数据无论大小都会及时地发送给从节点, 这样主从之间延迟会变小, 但增加了网络带宽的消耗。 适用于主从之间
的网络环境良好的场景, 如同机架或同机房部署。

2 拓扑

Redis的复制拓扑结构可以支持单层或多层复制关系, 根据拓扑复杂性可以分为以下三种: 一主一从、 一主多从、 树状主从结构

一主一从结构

从一个redis复制到另外一个redis redis 复制_偏移量_03

一主多从结构

对于读占比较大的场景, 可以把读命令发送到从节点来分担主节点压力。

对于写并发量较高的场景, 多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽, 同时也加重了主节点的负载影响服务稳定性。

从一个redis复制到另外一个redis redis 复制_Redis_04

树状主从结构

当主节点需要挂载多个从节点时为了避免对主节点的性能干扰, 可以采用树状主从结构降低主节点压力。

从一个redis复制到另外一个redis redis 复制_Redis_05

3 原理

3.1 复制过程
在从节点执行slaveof命令后, 复制过程便开始运作
从图中可以看出复制过程大致分为6个过程:

从一个redis复制到另外一个redis redis 复制_Redis_06


1) 保存主节点(master) 信息。

2) 从节点(slave) 内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后, 会尝试与该节点建立网络连接
如果从节点无法建立连接, 定时任务会无限重试直到连接成功或者执行slaveof no one取消复制
3) 发送ping命令。
4) 权限验证。
5) 同步数据集。
6) 命令持续复制。
3.2 数据同步
Redis在2.8及以上版本使用psync命令完成主从数据同步, 同步过程分为: 全量复制和部分复制。
·全量复制: 一般用于初次复制场景, Redis早期支持的复制功能只有全量复制, 它会把主节点全部数据一次性发送给从节点, 当数据量较大时, 会对主从节点和网络造成很大的开销。
·部分复制: 用于处理在主从复制中因网络闪断等原因造成的数据丢失场景, 当从节点再次连上主节点后, 如果条件允许, 主节点会补发丢失数据
给从节点。 因为补发的数据远远小于全量数据, 可以有效避免全量复制的过高开销。
psync命令运行需要以下组件支持:
·主从节点各自复制偏移量。

通过对比主从节点的复制偏移量, 可以判断主从节点数据是否一致。

可以通过主节点的统计信息, 计算出master_repl_offset-slave_offset字节量, 判断主从节点复制相差的数据量, 根据这个差值判定当前复制的健康
度。 如果主从之间复制偏移量相差较大, 则可能是网络延迟或命令阻塞等原因引起。

·主节点复制积压缓冲区。

由于缓冲区本质上是先进先出的定长队列, 所以能实现保存最近已复制数据的功能, 用于部分复制和复制命令丢失的数据补救。 复制缓冲区相关统计信息保存在主节点的info replication中:

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 // 已保存数据的有效长度。

·主节点运行id。

每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。 运行ID的主要作用是用来唯一识别Redis节点, 比如从节点保存主节
点的运行ID识别自己正在复制的是哪个主节点。
当运行ID变化后从节点将做全量复制。
如何在不改变运行ID的情况下重启呢?
当需要调优一些内存相关配置, 例如: hash-max-ziplist-value等, 这些配置需要Redis重新加载才能优化已存在的数据, 这时可以使用debug reload命令重新加载RDB并保持运行ID不变, 从而有效避免不必要的全量复制。 命令如下:

# redis-cli -p 6379 info server | grep run_id
run_id:2b2ec5f49f752f35c2b2da4d05775b5b3aaa57ca
# redis-cli debug reload
OK
# redis-cli -p 6379 info server | grep run_id
run_id:2b2ec5f49f752f35c2b2da4d05775b5b3aaa57ca

debug reload命令会阻塞当前Redis节点主线程, 阻塞期间会生成本地RDB快照并清空数据之后再加载RDB文件。 因此对于大数据量的主节点和无

法容忍阻塞的应用场景, 谨慎使用。

4.psync命令

从节点使用psync命令完成部分复制和全量复制功能, 命令格式:

psync{runId}{offset}, 参数含义如下:

·runId: 从节点所复制主节点的运行id。

·offset: 当前从节点已复制的数据偏移量。

从一个redis复制到另外一个redis redis 复制_java_07

·如果回复+FULLRESYNC{runId}{offset}, 那么从节点将触发全量复制流程。

·如果回复+CONTINUE, 从节点将触发部分复制流程。

·如果回复+ERR, 说明主节点版本低于Redis2.8, 无法识别psync命令,从节点将发送旧版的sync命令触发全量复制流程。

3.3 全量复制

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

2) 主节点根据psync-1解析出当前为全量复制, 回复+FULLRESYNC响应。

3) 从节点接收主节点的响应数据保存运行ID和偏移量offset,

4) 主节点执行bgsave保存RDB文件到本地

5) 主节点发送RDB文件给从节点,

6) 对于从节点开始接收RDB快照到接收完成期间, 主节点仍然响应读写命令, 因此主节点会把这期间写命令数据保存在复制客户端缓冲区内, 当从节点加载完RDB文件后, 主节点再把缓冲区内的数据发送给从节点, 保证主从之间数据一致性。 如果主节点创建和传输RDB的时间过长, 对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。 默认配置为clientoutput-buffer-limit slave256MB64MB60, 如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时, 主节点将直接关闭复制客户端连接, 造成全量同步失败。

7) 从节点接收完主节点传送来的全部数据后会清空自身旧数据

8) 从节点清空数据后开始加载RDB文件, 对于较大的RDB文件, 这一步操作依然比较耗时

9) 从节点成功加载完RDB后, 如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof操作, 为了保证全量复制后AOF持久化文件立刻可

用。

从一个redis复制到另外一个redis redis 复制_java_08

通过分析全量复制的所有流程, 读者会发现全量复制是一个非常耗时费力的操作。 它的时间开销主要包括:

·主节点bgsave时间。

·RDB文件网络传输时间。

·从节点清空数据时间。

·从节点加载RDB的时间。

·可能的AOF重写时间。

3.4 部分复制

部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施,使用psync{runId}{offset}命令实现。 当从节点(slave) 正在复制主节点

(master) 时, 如果出现网络闪断或者命令丢失等异常情况时, 从节点会向主节点要求补发丢失的命令数据, 如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点, 这样就可以保持主从节点复制的一致性。 补发的这部分数据一般远远小于全量数据, 所以开销很小。

从一个redis复制到另外一个redis redis 复制_java_09

3.5 心跳

主从节点在建立复制后, 它们之间维护着长连接并彼此发送心跳命令,

1) 主从节点彼此都有心跳检测机制, 各自模拟成对方的客户端进行通信,

2) 主节点默认每隔10秒对从节点发送ping命令, 判断从节点的存活性和连接状态。 可通过参数repl-ping-slave-period控制发送频率。

3) 从节点在主线程中每隔1秒发送replconf ack{offset}命令, 给主节点上报自身当前的复制偏移量。 

3.6 异步复制

主节点不但负责数据读写, 还负责把写命令同步给从节点。 写命令的发送过程是异步完成, 也就是说主节点自身处理完写命令后直接返回给客户

端, 并不等待从节点复制完成。

4.1 读写分离

对于读占比较高的场景, 可以通过把一部分读流量分摊到从节点(slave) 来减轻主节点(master) 压力, 同时需要注意永远只对主节点执行

写操作, 如图6-19所示。

从一个redis复制到另外一个redis redis 复制_redis_10

当使用从节点响应读请求时, 业务端可能会遇到如下问题:
·复制数据延迟。
·读到过期数据。
·从节点故障。
1.数据延迟
Redis复制数据的延迟由于异步复制特性是无法避免的, 延迟取决于网络带宽和命令阻塞情况, 比如刚在主节点写入数据后立刻在从节点上读取可
能获取不到。
如果允许不一致性或对延迟不敏感的业务可以忽略, 也可以采用Redis集群方案做水平扩展。
2.读到过期数据
当主节点存储大量设置超时的数据时, 如缓存数据, Redis内部需要维护过期数据删除策略, 删除策略主要有两种: 惰性删除和定时删除,
惰性删除:

主节点每次处理读取命令时, 都会检查键是否超时, 如果超时则执行del命令删除键对象, 之后del命令也会异步发送给从节点。 需要注
意的是为了保证复制的一致性, 从节点自身永远不会主动删除超时数据,
定时删除:

Redis主节点在内部定时任务会循环采样一定数量的键, 当发现采样的键过期时执行del命令, 之后再同步给从节点,

如果此时数据大量超时, 主节点采样速度跟不上过期速度且主节点没有读取过期键的操作, 那么从节点将无法收到del命令。 这时在从节点上可以

读取到已经超时的数据。 Redis在3.2版本解决了这个问题, 从节点读取数据之前会检查键的过期时间来决定是否返回数据, 可以升级到3.2版本来规避这个问题。

3.从节点故障问题

对于从节点的故障问题, 需要在客户端维护可用从节点列表, 当从节点故障时立刻切换到其他从节点或主节点上。 这个过程类似上文提到的针对延迟过高的监控处理, 需要开发人员改造客户端类库。

综上所出, 使用Redis做读写分离存在一定的成本。 Redis本身的性能非常高, 开发人员在使用额外的从节点提升读性能之前, 尽量在主节点上做充分优化, 比如解决慢查询, 持久化阻塞, 合理应用数据结构等, 当主节点优化空间不大时再考虑扩展。 笔者建议大家在做读写分离之前, 可以考虑使用Redis Cluster等分布式解决方案, 这样不止扩展了读性能还可以扩展写性能和可支撑数据规模, 并且一致性和故障转移也可以得到保证, 对于客户端的维护逻辑也相对容易。

4.3 规避全量复制

·第一次建立复制: 由于是第一次建立复制, 从节点不包含任何主节点数据, 因此必须进行全量复制才能完成数据同步。

·节点运行ID不匹配: 当主从复制关系建立后, 从节点会保存主节点的运行ID, 如果此时主节点因故障重启, 那么它的运行ID会改变, 从节点发现主节点运行ID不匹配时, 会认为自己复制的是一个新的主节点从而进行全量复制。 对于这种情况应该从架构上规避, 比如提供故障转移功能。

·复制积压缓冲区不足: 当主从节点网络中断后, 从节点再次连上主节点时会发送psync{offset}{runId}命令请求部分复制, 如果请求的偏移量不在

主节点的积压缓冲区内, 则无法提供给从节点数据, 因此部分复制会退化为全量复制。

4.4 规避复制风暴

复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点短时间内发起全量复制的过程。 复制风暴对发起复制的主节点或者机器造成

大量开销, 导致CPU、 内存、 带宽消耗。

1.单主节点复制风暴

单主节点复制风暴一般发生在主节点挂载多个从节点的场景。 当主节点重启恢复后, 从节点会发起全量复制流程, 这时主节点就会为从节点创建

RDB快照, 如果在快照创建完毕之前, 有多个从节点都尝试与主节点进行全量同步, 那么其他从节点将共享这份RDB快照。 这点Redis做了优化, 有效避免了创建多个快照。 但是, 同时向多个从节点发送RDB快照, 可能使主节点的网络带宽消耗严重, 造成主节点的延迟变大, 极端情况会发生主从节点连接断开, 导致复制失败。

解决方案首先可以减少主节点(master) 挂载从节点(slave) 的数量,或者采用树状复制结构, 加入中间层从节点用来保护主节点,

从一个redis复制到另外一个redis redis 复制_数据_11

2.单机器复制风暴
由于Redis的单线程架构, 通常单台机器会部署多个Redis实例。 当一台机器(machine) 上同时部署多个主节点(master) 时,
如果这台机器出现故障或网络长时间中断, 当它重启恢复后, 会有大量从节点(slave) 针对这台机器的主节点进行全量复制, 会造成当前机器网络带宽耗尽。
如何避免? 方法如下:
·应该把主节点尽量分散在多台机器上, 避免在单台机器上部署过多的主节点。
·当主节点所在机器故障后提供故障转移机制, 避免机器恢复后进行密集的全量复制。
 

备注:文章参考《Redis开发与运维》,作者:付磊,张益军