【Redis 高可用】主从复制
Redis 主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
主从复制的作用主要包括:
-
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
-
故障转移(failover with replication):当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复,实际上是一种服务的冗余。
-
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
-
高可用(high availability):主从复制还是哨兵和集群能够实施的基础,因此,主从复制是Redis高可用的基础。
主从库之间采用的是读写分离的方式。
-
读操作:主库、从库都可以接收;
-
写操作:首先到主库执行,然后,主库将写操作同步给从库。
即,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。
主从复制原理
每个 Redis 主节点启动时都会有一个唯一 复制ID(Replication ID):它是一个很大的伪随机字符串,用于唯一地标记这个实例。同时复制时,每个主节点还使用了一个复制偏移量(replication offset),复制偏移量会随着它生成的复制比特流增加而递增,用于记录复制的进度。
全量重新复制(Full-ReSync)
全量重新复制的触发时机:
-
slave 首次连接 master
一般发生在第一次同步时,当我们启动多个Redis实例时,他们之间就可以通过
REPLICAOF
命令执行全量复制。 -
master 与 slave 之间的状态差异过大
确立主从关系
REPLICAOF 命令
REPLICAOF
命令的语法:
REPLICAOF host port
如果参数使用了正确的主机名称和端口,它会使当前节点成为另一个Redis节点的副本,并监听它的 hostname 和 端口号。如果参数是NO ONE
,则会停止复制,并将当前的副本节点成为 MASTER 节点。
监听 MASTER 节点及对应端口,并执行复制:
> REPLICAOF 172.16.19.3 6799
"OK"
停止复制,并将当前节点升级成 MASTER 节点:
> REPLICAOF NO ONE
"OK"
注意,REPLICAOF
命令的参数不一定是主节点,也可以使用从节点的主机名和端口号,即从节点也可以成为其他从节点的副本,这样可以减少主节点因主从全量复制的性能压力。
设置从节点
假设,我们有两个节点:节点1 (172.16.19.3)和节点2(172.16.19.5),在节点2上执行如下命令,即可将节点2设置为副本:
> REPLICAOF 172.16.19.3 6799
"OK"
全量复制的三个阶段
第一阶段:主从库间建立连接、协商同步
在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。具体来说,从库给主库发送 PSYNC 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。
The PSYNC command is called by Redis replicas for initiating a replication stream from the master.
PSYNC 命令语法:
PSYNC replicationid offset
当从库和主库第一次复制时,:
PSYNC ? -1
其中,当从库和主库第一次复制时,因为不知道主库的 replicationid,所以,replicationid=?
,并且设置 offset=-1
,表示第一次复制。
主库收到 PSYNC 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 replicationid 和主库目前的复制进度 offset,返回给从库。
这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。
第二阶段:主服务器同步数据给从服务器
主库执行 bgsave 命令,生成 RDB 文件,然后,将文件发给从库。从库接收到 RDB 文件后,为了避免之前数据的影响,会先清空当前数据库,然后加载 RDB 文件。
注意,主服务器生成 RDB 这个过程是不会阻塞主线程的,因为 bgsave 命令是产生了一个子进程来做生成 RDB 文件的工作,是异步工作的,这样 Redis 依然可以正常处理命令。
但是,这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。为了保证主从服务器的数据一致性,主服务器在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里:
- 主服务器生成 RDB 文件期间;
- 主服务器发送 RDB 文件给从服务器期间;
- 从服务器加载 RDB 文件期间;
第三阶段:主服务器发送新写操作命令给从服务器
在主服务器生成的 RDB 文件发送完,从服务器收到 RDB 文件后,丢弃所有旧数据,将 RDB 数据载入到内存。完成 RDB 的载入后,会回复一个确认消息给主服务器。
接着,主服务器将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,从服务器执行来自主服务器 replication buffer 缓冲区里发来的命令,这时主从服务器的数据就一致了。
至此,主从服务器的第一次同步的工作就完成了。
命令传播
主从服务器在完成第一次同步后,双方之间就会维护一个TCP长连接。
后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使其与主服务器的数据库状态相同。
而且这个连接是长连接的,避免频繁的 TCP 连接和断开带来的性能开销。
上面的这个过程被称为基于长连接的命令传播,通过这种方式来保证第一次同步后的主从服务器的数据一致性。
增量复制
主从服务器在完成第一次同步后,就会基于长连接进行命令传播。期间,如果主从服务器间的网络连接断开了,那么就无法进行命令传播了,这时从服务器的数据就没办法和主服务器保持一致了,客户端就可能从「从服务器」读到旧的数据。
在网络恢复后,为了保持数据一致性,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。增量复制过程如下图:
主要有三个步骤:
-
从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
-
主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
-
然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。
这里,需要了解两个概念:
-
repl_backlog_buffer:是一个环形缓冲区,用于主从服务器断连后,从中找到差异的数据;
在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此 这个缓冲区里会保存着最近传播的写命令。
-
复制偏移量(replication offset):标记环形缓冲区的同步进度,主从服务器都会记录各自的偏移量,其中,主服务器使用 master_repl_offset 来记录它写到的位置,从服务器使用 slave_repl_offset 来记录它读到的位置。
在网络断开后,当从服务器重新连上主服务器时,从服务器会通过 PSYNC
命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:
-
如果从服务器要读取的数据还在 repl_backlog_buffer 缓冲区里,那么主服务器将采用增量同步的方式同步;
-
如果从服务器要读取的数据已经不在 repl_backlog_buffer 缓冲区里,那么主服务器将采用全量同步的方式。
当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,它会缓存将要传播给从服务器的命令,如下图所示:
其中,repl_backlog_buffer 缓行缓冲区的默认大小是 1M,并且,由于它是一个环形缓冲区,所以,当缓冲区写满后,主服务器继续写入数据,就会覆盖之前的数据。因此,当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据很快就会被覆盖。
在网络恢复时,如果从服务器想读的数据已经被覆盖了,主服务器就会采用全量同步,这个方式比增量同步的性能损耗要大很多。
因此,为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,需要调整 repl_backlog_buffer 缓冲区大小,使其尽可能的大一些,减少出现从服务器要读取的数据被覆盖的概率,从而使得主服务器采用增量同步的方式。
可以通过调整 redis.conf
配置文件中的 repl-backlog-size
参数,修改 repl_backlog_buffer 缓冲区的大小:
# The backlog is only allocated if there is at least one replica connected.
repl-backlog-size 1mb
深入理解
master 关闭持久化时复制的安全性
在使用 Redis 复制的设置中,强烈建议在主服务器和副本服务器中打开持久性。
将持久化关闭并且将主服务器配置为自动重启是危险的,例如,下面的故障模式会导致主服务器及其所有副本中的数据被擦除:
-
假设我们有3个节点:节点 A 充当主节点,并且持久性被关闭,节点 B 和 C 从节点 A 复制;
-
某个时刻,节点 A 崩溃了,但是配置了自动重启,可以自动重启进程,但是,由于持久性已关闭,因此,节点A会以空数据集重新启动;
-
此时,节点 B 和 C 将从空的节点 A 进行复制,因此它们将会销毁所有副本上的数据。
当 Redis Sentinel 用于高可用性时,同时关闭 master 上的持久化,并且配置自动重启是危险的。例如,master 可以足够快地重新启动,以导致 Sentinel 无法检测到故障,从而发生上述故障模式。
因此,当配置了主从复制时,如果没有开启持久化功能,应该禁用主节点的自动重启功能。
为什么主从全量复制使用RDB而不使用AOF?
RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量复制的成本最低。
假设要使用AOF做全量复制,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。
读写分离及其中的问题
在主从复制基础上实现的读写分离,可以实现Redis的读负载均衡:由主节点提供写服务,由一个或多个从节点提供读服务(多个从节点既可以提高数据冗余程度,也可以最大化读负载能力)。
在读负载较大的应用场景下,可以大大提高Redis服务器的并发量。下面介绍在使用Redis读写分离时,需要注意的问题。
延迟与不一致问题
因为主从节点间的命令复制是异步进行的,所以,无法实现主从节点数据的强一致性。如果业务对数据不一致的接受程度程度较低,可以通过以下措施优化:
-
优化主从节点之间的网络环境:主从节点在相同的机房部署;
-
通过监控主从节点延迟(主从偏移量)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据,这样可以减少客户端读到数据不一致数据的情况;
-
使用集群同时扩展写负载和读负载等。
在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。
数据过期问题
单机部署的 Redis 节点,存在两种删除策略:
-
惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
-
定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。当主节点执行命令删除了一个 key 或者通过淘汰算法淘汰了一个 key ,主节点就会模拟一条 del 命令发送给从节点,从节点收到该命令后,就进行删除 key 的操作。
由于主节点的惰性删除和定期删除策略,不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过 Redis 从节点读取数据时,很容易读取到已经过期的数据。
故障转移问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
参考: