说起redis的主从复制,我们可以类比一下mysql的主从复制
mysql为什么要做主从复制,或者说做主从复制有什么好处
主要从两个方面说。一个是安全性,可以做数据冗余,防止误操作下的数据丢失;一个是性能方面,可以减轻主节点的压力
redis做主从复制的好处或者说意义和mysql是一致的。在于:
数据冗余(安全性)
防止误操作下的数据丢失
分散单节点压力(性能方面)
redis是单线程,虽然内存操作很快,但是一旦客户端多了,命令执行多了,一个会阻塞到后面命令的执行,速度会变慢;另一个CPU的损耗集中在一台机器上;
做主从复制+负载均衡就可以解决这个问题
redis部署到多台机器上还因为扩容的需求,即单机的内存是有限的,一旦redis中数据量达到内存上限就需要更大的空间,此时需要将redis部署到多台机器做分布式。
不过分布式是将数据分散到多台机器,而主从复制是每台机器都包含所有数据。
PS:在生产环境中,redis主从复制过程中,多台redis是分布在不同机器上的。如果将多个redis服务部署在一台机器上几乎没意义的,除非只是在做实验。
redis主从复制的两种实现方式
a. 在从节点执行 "slaveof 主节点ip 端口"
该过程是异步的。
取消复制: slaveof no one
取消复制不会清空旧数据
PS: 从节点执行“slaveof 主节点”时,从节点的数据会被清空。因为一个从节点只能保存主节点的数据,不能有自己的数据。当然,这个过程发生了什么后面介绍主从复制的原理时会说明。
b. 通过修改从节点配置
slaveof ip port
slave-read-only yes # 设置从节点只读不能写,目的是维持主从节点数据一致性
小实验:在本机上配置两个redis服务,主节点为 127.0.0.1:6379,从节点 127.0.0.1:6380
实验配置:
# 主节点配置文件
#redis-6379.conf
pidfile /var/run/redis_6379.pid
logfile "6379.log"
#save 900 1 #禁用自动rdb持久化
#save 300 10
#save 60 10000
dbfilename dump-6379.rdb # 修改rdb快照文件名,目的是避免主从共用一个rdb快照,会造成互相覆盖
# 从节点配置文件
#redis-6380.conf
pidfile /var/run/redis_6380.pid
logfile "6380.log"
port 6380 # 从节点监听6380端口
#save 900 1 #禁用自动rdb持久化
#save 300 10
#save 60 10000
dbfilename dump-6380.rdb
replicaof 127.0.0.1 6379
replica-read-only yes # 从节点只读
由于我的redis是最新版的,主从复制的命令变成了 replicaof 。但是slaveof命令依然有效。# 先启动主节点,再启动从节点(此时从节点自动执行slaveof);再打开主节点客户端和从节点客户端分别执行 info replication 查看主从复制状态
redis-server /usr/local/redis/redis-6379.conf
redis-server /usr/local/redis/redis-6380.conf
redis-cli
redis-cli -p 6380 # 从节点redis的客户端
info replication
主节点显示:
role:master #主节点
connected_slaves:1 #连上的从节点数
slave0:ip=127.0.0.1,port=6380,state=online,offset=210,lag=0 # 1号从节点的信息
master_replid:ac37c849937ec9611b561b03d532fd1617df316f
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:210
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:210
从节点显示:
role:slave # 从节点
master_host:127.0.0.1 # 主节点ip
master_port:6379 # 主节点端口
master_link_status:up # 主节点状态
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:406
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:ac37c849937ec9611b561b03d532fd1617df316f
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:406
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:406
主从复制的原理:
在正式介绍原理前,先介绍两个概念。
run_id和偏移量
run_id是redis的一个标识id,每次重启都会重新生成一个run_id
可以通过 info server 查看
偏移量(master_repl_offset)记录的是redis当前执行过的所有写命令的字节数。偏移量是作为主从复制时,主从数据是否达到一致性的一个标准。如果主节点和从节点的偏移量相同表示主从数据一致。当然偏移量会不停在变,所以你来回的去看主从节点的偏移量可能是不同的,但实际上他们是相同的。
在生产环境中,我们很少会去看这个偏移量。
偏移量是部分复制的关键。
下面正式介绍原理:
主从复制有两种:全量复制和部分复制
全量复制的触发条件和过程:
当从节点第一次启动的时候会进行首次同步
从节点会发送一条 psync ? -1 命令给master。
其中psync命令表示从节点需要对主节点进行同步,第二参和第三参分别是主节点的run_id和偏移量。由于第一次进行同步,从节点不知道主节点和run_id和偏移量,所以传参为 ? 和 -1 。此时就会进行全量复制
主节点根据命令返回 FULLRESYNC 表示全量复制
主节点会将其run_id和偏移量发送给从节点,从节点会将其记录下来。下次从节点同步时就知道主节点的run_id和offset,就能进行部分复制。
主节点收到从节点想全量复制的指令后,会执行bgsave生成rdb快照并保存到本地,并将 send rdb 给slave ,slave会flushall清空所有数据再将rdb加载到内存(如果这段时间耗时较长,而主节点的写操作又比较多,会导致复制缓冲区溢出,最后全量同步失败)。
在主节点异步生成rdb,发送rdb和slave加载rdb到内存的过程中,master新执行的写操作会被写入一个复制缓冲区(repl_back_buffer),并在slave加载完rdb后 send buffer 中的数据给slave。
从节点成功加载完 RBD 后,如果开启了 AOF,会立刻做 bgrewriteaof。
全量复制开销(非常大)
1.bgsave 消耗时间
2.rdb文件网络传输时间
3.从节点清空数据的时间
4.从节点加载rdb的时间
5.可能的aof重写的时间(如果开启了aof重写)
由于aof重写是对内存的一个回溯,所以生成的aof文件不会包含从节点的旧数据,因为旧数据已经被flashall掉了。
如过 RDB 文件大于 6GB,并且是百兆网卡,Redis 的默认超时机制(60 秒),会导致全量复制失败。可以通过调大 repl-timeout 参数来解决此问题。
部分复制
当从节点正在复制主节点时,如果出现网络闪断和其他异常,从节点会让主节点补发丢失的命令数据,主节点只需要将复制缓冲区的数据发送到从节点就能够保证数据的一致性,相比较全量复制,成本小很多。
部分复制的过程:
在主从建立连接后,master每一条写命令会保存到一个repl_back_buffer 的复制缓冲区中。这个缓冲区就是为了部分复制而存在的。
当网络闪断恢复时,从节点 发送 psync rund_id offset 指令给master,把slave节点的偏移量告诉master节点,master会检测offset偏移量是否超出了复制缓冲区的范围(master的repl缓冲区是一个先进先出的队列,默认1M),如果超出了范围说明master和slave的数据差异比较大,此时master会发送FULLRESYNC响应,表示进行全量复制。
如果没有超出范围,那么会发送CONTINUE响应,表示进行部分复制,master将offset开始到缓冲区结尾的那部分数据传给slave,让slave达到部分复制的目的。
部分复制的触发是在从节点和主节点已经正常同步时出现网络不稳定,使得从节点不能正常同步,等网络正常了主从恢复连接后,从节点会发送psync rund_id offset从而开始部分复制
主从复制整体过程:
1 从节点执行 slaveof 命令。
2 从节点只是保存了 slaveof 命令中主节点的信息,并没有立即发起复制。
3 从节点内部的定时任务发现有主节点的信息,开始使用 socket 连接主节点。
4 连接建立成功后,发送 ping 命令,希望得到 pong 命令响应,否则会进行重连。
5 如果主节点设置了权限,那么就需要进行权限验证,如果验证失败,复制终止。
6 权限验证通过后,进行数据同步,这是耗时最长的操作,主节点将把所有的数据全部发送给从节点。
7 当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来,主节点就会持续的把写命令发送给从节点(这个不是部分复制,这个是正常的同步,master写一条,slave也写一条,这个过程是异步的不影响客户端其他命令执行),保证主从数据一致性。
主从节点各自复制偏移量:
参与复制的主从节点都会维护自身的复制偏移量。
主节点在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在 info replication 中的 master_repl_offset 指标中。
从节点每秒钟上报自身的的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量。
从节点在接收到主节点发送的命令后,也会累加自身的偏移量,统计信息在 info replication 中。
通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。
主节点复制积压缓冲区:
复制积压缓冲区是一个保存在主节点的一个固定长度的先进先出的队列,默认大小 1MB。
这个队列在 master和slave连接时创建。这时主节点响应写命令时,不但会把命令发送给从节点,也会写入复制缓冲区。
他的作用就是用于部分复制和复制命令丢失的数据补救。通过 info replication 可以看到相关信息。
复制积压缓冲区存储的具体内容是什么?
存的是写命令和写命令每一个字节的偏移量。
如 set name zbp
在复制缓冲区中存储形式为:
偏移量: 10087 10088 10089 10090 10091 10092 ...
字节值: s e t z b ...
所以如果slave节点psync 到master的偏移量offset不在缓冲区所记录的偏移量范围内,就会进行全量复制,否则就是部分复制。
主节点运行 ID:
每个 redis 启动的时候,都会生成一个 40 位的运行 ID。
运行 ID 的主要作用是用来识别 Redis 节点。如果使用 ip+port 的方式,那么如果主节点重启修改了 RDB/AOF 数据,从节点再基于偏移量进行复制将是不安全的。所以,当redis重启,运行 id 变化后,从节点将进行全量复制。也就是说,redis 重启后,默认从节点会进行全量复制。
psync 命令的使用方式:
命令格式为 psync{runId}{offset}
runId:从节点所复制主节点的运行 id
offset:当前从节点已复制的数据偏移量
psync是redis主从复制时自动调用,无需我们手动调用
从节点发送 psync 命令给主节点,runId 就是目标主节点的 ID,如果没有默认为 -1,offset 是从节点保存的复制偏移量,如果是第一次复制则为 -1.
主节点会根据 runid 和 offset 决定返回结果:
如果回复 +FULLRESYNC {runId} {offset} ,那么从节点将触发全量复制流程。
如果回复 +CONTINUE,从节点将触发部分复制。
如果回复 +ERR,说明主节点不支持 2.8 的 psync 命令,将使用 sync 执行全量复制。
心跳
主从节点在建立复制后,他们之间维持着长连接并彼此发送心跳命令。
主从各自模拟成对方的客户端进行通信,通过 client list 命令查看复制信息,主节点的连接状态为 flags = M,从节点的连接状态是 flags = S。
主节点默认每隔 10 秒对从节点发送 ping 命令,可修改配置 repl-ping-slave-period 控制发送频率。
从节点在主线程每隔一秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量。
主节点收到 replconf 信息后,判断从节点超时时间,如果超过 repl-timeout 60 秒,则判断节点下线。
为了降低主从延迟,一般把 redis 主从节点部署在相同的机房/同城机房,避免网络延迟带来的网络分区造成的心跳中断等情况。
主从复制的故障处理
使用sentinel进行故障转移
主从复制的常见问题:主要通过三个方面记忆:数据不一致(延迟),故障,性能问题
1.读写分离
读写分离可以将读流量分摊到从节点。
可能遇到的问题:
a.复制延迟,造成数据不一致。尤其是slave节点阻塞的时候,master传过来的写命令无法在slave中快速执行时。
b.读到过期数据
c.从节点故障
其实在读写分离之前,我们应该尽可能想办法优化主节点。当单节点实在满足不了需求再进行读写分离
2.主从配置不一致
a.maxmemory配置不一致可能导致丢失数据
b.数据结构设置不同导致内存不一致
3.规避全量复制
a.第一次全量复制
不可避免
方案1:小主节点,例如我们把redis分成2G一个节点,这样一来,会加速RDB的生成和同步,同时还可以降低我们fork子进程的开销(master会fork一个子进程来生成同步需要的RDB文件,而fork是要拷贝内存快的,如果主节点内存太大,fork的开销就大)。
方案2:既然第一次不可以避免,那我们可以选在集群低峰的时间(凌晨)进行slave的挂载。
b.节点运行id不匹配会导致全量复制
例如主节点重启(如果是debug reload则可以在不重启的情况下重新读取配置文件),主节点run_id会改变,此时从节点会进行全量复制。
一般来说,redis节点一旦运行是不重启的,除非redis发生故障要进行重启。
此时可以通过故障转移避免,就是将一个从节点变成主节点,其他从节点指向该新的主节点。可通过哨兵实现。
c.复制积压缓冲区不足会导致全量复制
之前我们说过,当出现网络中断后网络恢复,slave发送到master的偏移量超出了复制缓冲区范围(这个时候master和slave的数据差太多了),就会执行全量复制。
解决方法:增大复制缓冲区(rel_backlog_size,加大复制缓冲区的队列大小,默认1M,可以设成10M);
4.规避复制风暴
一个主节点有很多从节点,此时主节点发生故障重启,所有从节点进行全量复制。
虽然主节点只有一个,所以只用生成一个rdb快照,但是要传输给多个从节点,消耗了大量网络资源。
解决方法还是将一个从节点变为主节点(slave晋升为master),让其他从节点指向新主节点,此时就不会全量复制。等到了晚上夜深的时候再重启旧的redis节点
还有一种情况:单机上游多台master节点,机器宕机,恢复后所有master重启,多节点生成rdb(该操作为CPU密集型,消耗CPU;生成rdb文件,消耗IO;生成rdb会fork子进程进行消耗cpu,fork会拷贝主进程内存,所以消耗内存;rdb发送给slave,消耗网络带宽),这个消耗非常巨大。
解决方法是将多台master分散到不同机器或者slave晋升为master