redis 主从复制

在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。

配置

建立主从关系

准备2个redis.conf 配置文件,并修改redis 1 的端口为 6379 redis2 的端口为6380。

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@redis-cluster-50 opt]# tree
.
├── redis1
│   ├── appendonly.aof
│   ├── dump.rdb
│   └── redis.conf
└── redis2
├── appendonly.aof
├── dump.rdb
└── redis.conf

2 directories, 6 files
[root@redis-cluster-50 opt]#

配置守护进程

1
daemonize yes

执行

1
2
./redis-server /opt/redis1/redis.conf
./redis-server /opt/redis2/redis.conf

打开2个终端,执行

1
2
redis-cli -h 127.0.0.1 -p 6379
redis-cli -h 127.0.0.1 -p 6380

redis 的复制都是在从节点上进行的,在6380 这个redis 上执行

1
2
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK

在2个终端分别执行 info replication 查看主从复制信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
127.0.0.1:6379> info replication  # 主节点的replication信息
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=106922,lag=0
master_replid:46e7db3cffa10b8de08c7fdb04682a8b3cb8fb39
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:106922
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:106922

127.0.0.1:6380> info replication # 从节点的replication信息
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_repl_offset:106654
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:46e7db3cffa10b8de08c7fdb04682a8b3cb8fb39
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:106654
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:106585
repl_backlog_histlen:70
127.0.0.1:6380>

验证主从是否正常工作

1
2
3
4
5
6
7
8
127.0.0.1:6379> set hello china  # 在主节点 添加一个键值对。
OK
127.0.0.1:6379> get hello
"china"
127.0.0.1:6379>

127.0.0.1:6380> get hello # 在从节点获取,可以看到已经可以正常获取了。
"china"

断开主从关系

要断开主从关系,可以在从节点执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6380> SLAVEOF on one  # 断开主从
(error) ERR value is not an integer or out of range
127.0.0.1:6380> SLAVEOF no one
OK
127.0.0.1:6380> info replication # 从节点升级为master
# Replication
role:master
connected_slaves:0
master_replid:3201cbe916590fa6a33ebe7b4c9bdb616dda4a49
master_replid2:46e7db3cffa10b8de08c7fdb04682a8b3cb8fb39
master_repl_offset:107118
second_repl_offset:107119
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:106585
repl_backlog_histlen:534

从节点断开复制不会抛弃已有数据,只是无法再从主节点获取新数据。

切主操作

通过slaveof命令还可以实现切主操作,所谓切主是指把当前从节点对主节点的复制切换到另一个主节点。执行slaveof{newMasterIp}{newMasterPort}命令即可,例如把6380节点从原来的复制6379节点变为复制6381节点。

切主后从节点会清空之前的所有数据。

只读

默认情况下,从节点使用slave-read-only=yes配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。因此建议线上不要修改从节点的只读模式。

传输延迟

主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis为我们提供了repl-disable-tcp-nodelay参数用于控制是否关闭TCP_NODELAY,默认关闭,说明如下:

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

部署主从节点时需要考虑网络延迟、带宽使用率、防灾级别等因素,如要求低延迟时,建议同机架或同机房部署并关闭repl-disable-tcp-nodelay;如果考虑高容灾性,可以同城跨机房部署并开启repl-disable-tcp-nodelay。

redis 的主从结构

redis 的主从 有一主对一从 一主对多从以及树状结构三种。

redis 的注册复制过程

1.保存主节点信息

2.主从建立socket连接

3.发送ping命令

4.权限验证

5.同步数据集

6.命令持续复制

redis 的数据同步

  • 全量复制:一般用于初次复制场景,缺点 开销大

  • 部分复制:用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。

复制偏移量

参与复制的主从节点都会维护自身复制偏移量。主节点(master)在处369理完写入命令后,会把命令的字节长度做累加记录,统计信息在inforelication中的master_repl_offset指标

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_replid:45c39d0284b377c5d476a57da87a5bd50be2c694
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:107118
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:107118

从节点(slave)每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量,统计指标如

1
2
3
127.0.0.1:6379> info replication
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=107118,lag=1.

从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量。统计信息在info relication中slavereploffset指标

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_replid:f7fa776136b46d706326d5028e217e2c0393ed8c
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:107118
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:106585
repl_backlog_histlen:534

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

复制挤压缓冲区

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

由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,用于部分复制和复制命令丢失的数据补救。

1
2
3
4
repl_backlog_active:0 // 开启复制缓冲区
repl_backlog_size:1048576 // 缓冲区最大长度
repl_backlog_first_byte_offset:1 //起始偏移量,计算当前缓冲区可用范围
repl_backlog_histlen:107118 //已保存的有效长度

主节点运行ID

每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。运行ID的主要作用是用来唯一识别Redis节点,比如从节点保存主节点的运行ID识别自己正在复制的是哪个主节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
127.0.0.1:6379> INFO server
# Server
redis_version:5.0.4
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:803d687d1157c559
redis_mode:standalone
os:Linux 3.10.0-957.10.1.el7.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:4.8.5
process_id:13049
run_id:3caec0db309f595a030f05c04b60f800f99aadfe # 运行ID
tcp_port:6379
uptime_in_seconds:89010
uptime_in_days:1
hz:10
configured_hz:10
lru_clock:12756369
executable:/root/redis-5.0.4/src/./redis-server
config_file:/opt/redis1/redis.conf

redis 关闭后再次重启,运行ID 会变化,当运行ID变化后从节点将做全量复制。

如何在不改变运行ID的情况下重启呢?

当需要调优一些内存相关配置,例如:hash-max-ziplist-value等,这些配置需要Redis重新加载才能优化已存在的数据,这时可以使用debug reload命令重新加载RDB并保持运行ID不变,从而有效避免不必要的全量复制。命令如:

1
2
3
4
5
6
# redis-cli -p 6379 info server |  grep run_id
run_id:3caec0db309f595a030f05c04b60f800f99aadfe
# redis-cli debug reload
OK
# redis-cli -p 6379 info server | grep run_id
run_id:3caec0db309f595a030f05c04b60f800f99aadfe

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

psync

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

1
psync [runid] [offset]
  • runid: 从节点所复制主节点的运行id
  • offset: 当前从节点已复制的数据偏移量

流程说明

  • 从节点发送psync 给主节点
  • 主节点根据psync 参数和自身数情况节点响应结果
    • 如果回复+ FULLRESYNC [runid] [offset],那么从节点将触发全量复制
    • 如果回复+CONTINUE,从节点将触发部分复制流程。
    • 如果回复+ ERR,说明主节点版本低于2.8,无法识别pysnc,从节点将发送旧版的sync命令触发全量复制流程。

全量复制

流程说明

  • 发送psync进行数据同步,由于是第一次,从节点没有复制偏移量和主节点的运行ID,因此发送psync-1
  • 主节点根据psync-1解析出当前为全量复制,回复+FULLRESYNC
  • 从节点收到主节点的响应数据保存runid和offset。
  • 主节点执行bgsave 保存RDB 到本地。

Redis3.0之后在输出的日志开头会有M、S、C等标识,对应的含义是:M=当前为主节点日志,S=当前为从节点日志,C=子进程日志,我们可以根据日志标识快速识别出每行日志的角色信

  • 主节点发送RDB文件给从节点,从节点接收RDB保存本地直接作为从节点的数据文件。此时如果主节点有写入会存储在缓冲区中。
  • 从节点接收完主节点传送来的全部数据后会清空自身旧数据
  • 从节点清空数据后会加载 RDB 文件。如果文件较大,这一步骤会很长。

对于线上做读写分离的场景,从节点也负责响应读命令。如果此时从节点正出于全量复制阶段或者复制中断,那么从节点在响应读命令可能拿到过期或错误的数据。对于这种场景,Redis复制提供了slave-serve-stale-data参数,默认开启状态。如果开启则从节点依然响应所有命令。对于无法容忍不一致的应用场景可以设置no来关闭命令执行,此时从节点除了info和slaveof命令之外所有的命令只返回“SYNC with master in progress”信息

  • 从节点成功加载 RDB 后,如果当前节点开启了AOF会立刻做bgrewriteaof 操作。

部分复制

部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施,使用psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点(master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向
主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。

  • 当主从节点之间网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接。
  • 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB。
  • 当主从节点网络恢复后,从节点会再次连上主节点。
  • 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作。
  • 主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制。
  • 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

心跳

主从心跳判断机制:

1)主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过client list命令查看复制相关客户端信息,主节点的连接状态为flags=M,从节点连接状态为flags=S。

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

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

  • 实时监测主从节点网络状态。-
  • 上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据。
  • 实现保证从节点的数量和延迟性功能,通过min-slaves-to-write、minslaves-max-lag参数配置定

主节点根据replconf命令判断从节点超时时间,体现在info replication统计中的lag信息中,lag表示与从节点最后一次通信延迟的秒数,正常延迟应该在0和1之间。如果超过repl-timeout配置的值(默认60秒),则判定从节点
下线并断开复制客户端连接。即使主节点判定从节点下线后,如果从节点重新恢复,心跳检测会继续进行。

异步复制

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

注意事项

读写分离

可能会遇到的问题

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

读取到过期数据

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

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

从节点故障

对于从节点的故障问题,需要在客户端维护可用从节点列表,当从节点故障时立刻切换到其他从节点或主节点上。

主从配置不一致

主从配置不一致是一个容易忽视的问题。对于有些配置主从之间是可以不一致,比如:主节点关闭AOF在从节点开启。但对于内存相关的配置必须要一致,比如maxmemory,hash-max-ziplist-entries等参数。当配置的maxmemory从节点小于主节点,如果复制的数据量超过从节点maxmemory时,它会根据maxmemory-policy策略进行内存溢出控制,此时从节点数据已经丢失,但主从复制流程依然正常进行,复制偏移量也正常。修复这类问题也只能手动进行全量复制。当压缩列表相关参数不一致时,虽然主从节点存储的数据一致但实际内存占用情况差异会比较大。

规避全量复制

全量复制是一个非常消耗资源的操作,要尽量避免:

  • 第一次建立复制,会触发全量复制。
  • 节点runid不一致: 当节点id不一致会触发全量复制,要进行规避。
  • 复制积压缓冲区不足:对于大流量场景显然不够,这时需要增大积压缓冲区,防止因为请求的偏移量不在主节点的积压缓冲区内,则无法提供给从节点数据,因此部分复制会退化为全量复制。

规避复制风暴

复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点短时间内发起全量复制的过程。复制风暴对发起复制的主节点或者机器造成大量开销,导致CPU、内存、带宽消耗。

单主节点复制风暴

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

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

从节点采用树状树非常有用,网络开销交给位于中间层的从节点,而不必消耗顶层的主节点。但是这种树状结构也带来了运维的复杂性,增加了手动和自动处理故障转移的难度。

单机器复制风暴

出现在一台机器部署多个redis 实例,如果这台机器出现故障或网络长时间中断,当它重启恢复后,会有大量从节点(slave)针对这台机器的主节点进行全量复制,会造成当前机器网络带宽耗。

如何避免?方法如下

  • 应该把主节点尽量分散在多台机器上,避免在单台机器上部署过多的主节点。
  • 当主节点所在机器故障后提供故障转移机制,避免机器恢复后进行密集的全量复