Redis主备复制(replication)

介绍

有些场景,我们需要将相同的数据存放在不同的节点上,当某个节点不可用时,其它节点副本可继续提供服务,提高稳定性。另一方面,同一份数据存放在不同节点上,写入节点和读取节点分离(读写分离),可以提高性能

那么数据存放在多个不同节点上,怎么来保证节点间数据的一致性?

Redis采用了主备复制方式保证一致性,即在所有节点中,有一个主节点(master)对外提供写入服务,之后Redis内部异步地将数据从主节点复制到其它节点(slave)。

主备复制实现

在Redis中,用户可以通过REPLICAOF命令(也就是(SLAVEOF命令)或者设置replicaof选项让从服务器去复制主服务器数据。我们来看下这两种方式,然后具体分析下具体实现。

两种触发方式

REPLICAOF命令(SLAVEOF)

客户端在从服务器执行REPLICAOF命令也就是之前的SLAVEOF命令,使用方式如下:

REPLICAOF <masterip> <masterport>

我们在本地模拟一下这个命令,本地使用的Redis版本为5.0.3,开启6379和6380两个不同端口的redis实例,我们把6379端口的作为主节点。在6380端口的Redis服务器上执行:REPLICAOF 127.0.0.1 6379:

127.0.0.1:6380> REPLICAOF 127.0.0.1 6379
17144:S 10 Oct 2021 09:03:17.792 * Before turning into a replica, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
17144:S 10 Oct 2021 09:03:17.792 * REPLICAOF 127.0.0.1:6379 enabled (user request from 'id=8 addr=127.0.0.1:60790 fd=7 name= age=437 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=44 qbuf-free=32724 obl=0 oll=0 omem=0 events=r cmd=replicaof')
OK
127.0.0.1:6380> 17144:S 10 Oct 2021 09:03:18.117 * Connecting to MASTER 127.0.0.1:6379
                                                                                      17144:S 10 Oct 2021 09:03:18.117 * MASTER <-> REPLICA sync started
                                                                                                                                                        17144:S 10 Oct 2021 09:03:18.118 * Non blocking connect for SYNC fired the event.
                             17144:S 10 Oct 2021 09:03:18.118 * Master replied to PING, replication can continue...
                                                                                                                   17144:S 10 Oct 2021 09:03:18.118 * Trying a partial resynchronization (request 82d6eca4120c2c3308a12dc6f601a356c10d4d45:11772).
                                      17228:M 10 Oct 2021 09:03:18.119 * Replica 127.0.0.1:6380 asks for synchronization
                                                                                                                        17228:M 10 Oct 2021 09:03:18.119 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '82d6eca4120c2c3308a12dc6f601a356c10d4d45', my replication IDs are 'adc7e6a0c9933e21844beefb24ccc7fd5f901e32' and '0000000000000000000000000000000000000000')
                                                                                                                                                                                               17228:M 10 Oct 2021 09:03:18.119 * Starting BGSAVE for SYNC with target: disk
                                                                17228:M 10 Oct 2021 09:03:18.119 * Background saving started by pid 21164
                                                                                                                                         17144:S 10 Oct 2021 09:03:18.120 * Full resync from master: adc7e6a0c9933e21844beefb24ccc7fd5f901e32:11771
                                       17144:S 10 Oct 2021 09:03:18.120 * Discarding previously cached master state.
                                                                                                                    21164:C 10 Oct 2021 09:03:18.122 * DB saved on disk
                                                                                                                                                                       17228:M 10 Oct 2021 09:03:18.222 * Background saving terminated with success
                                       17228:M 10 Oct 2021 09:03:18.222 * Synchronization with replica 127.0.0.1:6380 succeeded
                                                                                                                               17144:S 10 Oct 2021 09:03:18.222 * MASTER <-> REPLICA sync: receiving 234 bytes from master
              17144:S 10 Oct 2021 09:03:18.223 * MASTER <-> REPLICA sync: Flushing old data
                                                                                           17144:S 10 Oct 2021 09:03:18.223 * MASTER <-> REPLICA sync: Loading DB in memory
                                                                                                                                                                           17144:S 10 Oct 2021 09:03:18.223 * MASTER <-> REPLICA sync: Finished with success
复制代码

上面是执行REPLICAOF命令后控制台的打印。从打印信息可以看出同步的大致流程。

设置replicaof选项

我们先将上面replicaof命令设置的主从复制关系解除:

127.0.0.1:6380> REPLICAOF no one
17144:M 10 Oct 2021 11:41:09.395 # Setting secondary replication ID to adc7e6a0c9933e21844beefb24ccc7fd5f901e32, valid up to offset: 18492. New replication ID is 0c18c8ae51afbaf9aac438b29aeaaa956b9e504d
17144:M 10 Oct 2021 11:41:09.395 # Connection with master lost.
17144:M 10 Oct 2021 11:41:09.395 * Caching the disconnected master state.
17144:M 10 Oct 2021 11:41:09.395 * Discarding previously cached master state.
17144:M 10 Oct 2021 11:41:09.395 * MASTER MODE enabled (user request from 'id=8 addr=127.0.0.1:60790 fd=7 name= age=9909 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=36 qbuf-free=32732 obl=0 oll=0 omem=0 events=r cmd=replicaof')
17228:M 10 Oct 2021 11:41:09.395 # Connection with replica 127.0.0.1:6380 lost.
OK
复制代码

接下来设置replicaof选项,放开replicaof 配置,设置为REPLICAOF 127.0.0.1 6379:

# Master-Replica replication. Use replicaof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
#   +------------------+      +---------------+
#   |      Master      | ---> |    Replica    |
#   | (receive writes) |      |  (exact copy) |
#   +------------------+      +---------------+
#
# 1) Redis replication is asynchronous, but you can configure a master to
#    stop accepting writes if it appears to be not connected with at least
#    a given number of replicas.
# 2) Redis replicas are able to perform a partial resynchronization with the
#    master if the replication link is lost for a relatively small amount of
#    time. You may want to configure the replication backlog size (see the next
#    sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
#    network partition replicas automatically try to reconnect to masters
#    and resynchronize with them.
#
# replicaof <masterip> <masterport>
replicaof 127.0.0.1:6380
复制代码

然后重启端口为6380的redis节点,启动后会输出部分日志和replicaof命令执行后一样的日志:

23118:S 10 Oct 2021 11:51:59.727 * Before turning into a replica, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
23118:S 10 Oct 2021 11:51:59.727 * Ready to accept connections
23118:S 10 Oct 2021 11:51:59.727 * Connecting to MASTER 127.0.0.1:6379
23118:S 10 Oct 2021 11:51:59.727 * MASTER <-> REPLICA sync started
23118:S 10 Oct 2021 11:51:59.727 * Non blocking connect for SYNC fired the event.
23118:S 10 Oct 2021 11:51:59.727 * Master replied to PING, replication can continue...
23118:S 10 Oct 2021 11:51:59.727 * Trying a partial resynchronization (request 0c18c8ae51afbaf9aac438b29aeaaa956b9e504d:18545).
17228:M 10 Oct 2021 11:51:59.728 * Replica 127.0.0.1:6380 asks for synchronization
17228:M 10 Oct 2021 11:51:59.728 * Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '0c18c8ae51afbaf9aac438b29aeaaa956b9e504d', my replication IDs are 'adc7e6a0c9933e21844beefb24ccc7fd5f901e32' and '0000000000000000000000000000000000000000')
17228:M 10 Oct 2021 11:51:59.728 * Starting BGSAVE for SYNC with target: disk
17228:M 10 Oct 2021 11:51:59.728 * Background saving started by pid 23119
23118:S 10 Oct 2021 11:51:59.728 * Full resync from master: adc7e6a0c9933e21844beefb24ccc7fd5f901e32:18491
23118:S 10 Oct 2021 11:51:59.728 * Discarding previously cached master state.
23119:C 10 Oct 2021 11:51:59.731 * DB saved on disk
17228:M 10 Oct 2021 11:51:59.745 * Background saving terminated with success
17228:M 10 Oct 2021 11:51:59.746 * Synchronization with replica 127.0.0.1:6380 succeeded
23118:S 10 Oct 2021 11:51:59.746 * MASTER <-> REPLICA sync: receiving 234 bytes from master
23118:S 10 Oct 2021 11:51:59.746 * MASTER <-> REPLICA sync: Flushing old data
23118:S 10 Oct 2021 11:51:59.746 * MASTER <-> REPLICA sync: Loading DB in memory
23118:S 10 Oct 2021 11:51:59.747 * MASTER <-> REPLICA sync: Finished with success
复制代码

日志打印的也就是大致的同步流程。从中我们可以看到一点就是首先会尝试部分重同步,如果部分重同步满足不了,则会进行完全重同步。通过上面两种方式都可以实现主从复制,从节点在此后就不能进行写操作了,就会变成只读节点。例如我们在6380端口的redis节点进行set:

127.0.0.1:6380> set msg "hello world"
(error) READONLY You can't write against a read only replica.
复制代码

会返回一个error,告诉你从节点当前是只读的,不能进行写操作。

通过以上方式复制实现的详细步骤如下所示(Redis 2.8版本或以上):

REPLICAOF命令复制实现步骤.png

其中最后两步在后面会有详细介绍,其它步骤如果有兴趣可以自行研究。

复制模式

从服务器对主服务器的复制可以分为完整重同步和部分重同步两种模式(PSYNC)。在此之前我们先来看下主备复制的主要流程(也就是复制实现详细步骤中的同步和命令传播)。

主备复制主要流程

Redis包含master和slave两种节点:master节点对外提供读写服务slave节点作为master的数据备份,对外不提供写服务。主要流程如下:

redis 主备复制主要流程 (2).png

对于master来说操作大致步骤如下: 1.salve向master发起PSYNC命令。slave启动后触发,master被动将新进的slave加入主备复制集群。

2.master收到SYNC后,开启BGSAVE操作

3.BGSAVE完成,master将快照信息RDB发送给slave

4.发送期间,master收到的新的写命令,除了正常响应外,都再存入一份到backlog队列。

5.快照信息发送完以后,继续发送backlog队列信息。

6.backlog发送完成后,后续的写操作同时发给slave,保持实时的异步复制。

在上图的slave侧,处理逻辑如下:

1.发送完PSYNC后,继续对外提供服务(使用旧版本数据)

2.开始接收master的快照信息,此时,将slave现有数据清空,并将master快照写入自身内存(清空旧数据和将master快照写入内存饿时候不对外提供服务)

3.接收backlog内容并执行它,期间对外提供读服务请求

4.继续接收后续来自master的命令副本并执行,以保持数据和master一致

如果多个slave节点并发发送SYNC命令给master,企图建立主备关系,只要第二个slave的SYNC命令发生在master完成BGSAVE之前,第二个slave将收到和第一个slave相同的快照和后续backlog;否则,第二个slave的PSYNC将触发master的第二次BGSAVE。

SYNC命令(Redis2.8之前使用)是执行完整重同步,PSYNC(Redis2.8之后使用)具有完整重同步和部分重同步两种模式

完整重同步

适应场景如下所示,

初次复制场景:

  • 从服务器未复制其它服务器
  • 从服务器上次复制主服务器和当前要复制的主服务器不是不同

断线重复制场景:

  • 从服务器与主服务器因网络原因断开,然后自动重连接连上了主服务器的时候,主服务器根据从服务器发来的偏移量offset(从服务器最后和主服务器同步数据的偏移量)判断复制积压缓冲区中offset偏移量之后的数据如果已经不存在的话,则会进行完全重同步

完整重同步的主要流程和上面介绍的主备复制主要流程基本相同。

部分重同步

适用场景如下所示,

断线重复制场景:

  • 如果offset(从服务器复制偏移量)之后的数据(偏移量offset + 1开始的数据)仍然存在复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作。

在Redis2.8版本之前,redis并不存在部分重同步的模式,在需要复制的场景都是使用的完全重同步,从服务器向主服务器发送SYNC命令来执行。每次当slave通过SYNC和master同步数据的时候,master都会dump全量数据并发发送。当一个已经和master完成同步并且保持长时间连接的slave,当slave和master断开很短时间再重新连接之后,master和slave的差异数据很少,master再次发送这些全量数据会很低效,导致大量无效开销。这个是需要改进的地方,这种情况最好的解决方式就是master只同步断开期间的少量数据

为了解决Redis2.8之前在处理断线重复制的低效问题,Redis从2.8版本开始。使用PSYNC(Partial Sync)命令替代SYNC命令来执行复制的重同步,做到master-slave基于断点续传的主备同步协议。master-slave两端通过维护一个offset记录当前已经同步过的命令,slave断开期间,master的客户端命令会保存在缓存中,在slave重连后,告知master断开时的最新的offset,master则将缓存中大于offset的数据发送给slave,减少了数据传输开销。

下面我们来看下部分重同步实现细节,先来介绍下部分重同步三个构成部分:

  • 主服务器的复制偏移量(replication offset) 和从服务器的复制偏移量

    主服务器和从服务器分别维护一个复制偏移量(offset),主服务器每次向从服务器传播N个字节数据,会将自身offset + N。从服务器收到主服务器传播的N个字节数据,同样也会将自身offset + N。正常情况主服务器和从服务器的偏移量是相同的,如果不相同则不处于一致状态。

    那么我们来想下,例如从节点断开后重连接主节点,那断开连接所丢失的数据是如何部分重同步到从节点的那?我们知道从节点的偏移量offset,可以将从节点偏移量传给主节点,主节点知道了偏移量是通过什么来获取后面丢失的数据?这个就是接下来要说的主服务器的复制积压缓冲区要发挥的功能了。

  • 主服务器的复制积压缓冲区(replication backlog)

复制积压缓冲区是由主服务器维护的一个固定长度先进先出的队列,默认大小为1MB。

# Set the replication backlog size. The backlog is a buffer that accumulates
# replica data when replicas are disconnected for some time, so that when a replica
# wants to reconnect again, often a full resync is not needed, but a partial
# resync is enough, just passing the portion of data the replica missed while
# disconnected.
#
# The bigger the replication backlog, the longer the time the replica can be
# disconnected and later be able to perform a partial resynchronization.
#
# The backlog is only allocated once there is at least a replica connected.
#
# repl-backlog-size 1mb
复制代码

当主服务器进行命令传播时,不仅把命令传给所有从服务器,还会入队到复制积压缓冲区里面。复制积压缓冲区保存着最近传播的写命令并且记录了每个字节的复制偏移量。

那我们就好理解了,主服务器接收到发来的PSYNC命令获取到从服务器传来的复制偏移量,会到复制积压缓冲区里面检查复制偏移量后面的命令是否存在里面,存在的话则向从服务器发送+CONTINUE命令,来告诉从服务器以部分重同步模式来同步。然后主服务器发送复制积压缓冲区从服务器偏移量后面的数据给到从服务器,从服务器接收并执行。

正确估算和设置复制积压缓冲区大小非常重要。安全起见,通常设置复制积压缓冲区大小为:

2 * second * write_size_per_second

其中second为从服务器断线重新连接主服务器所需的平均时间,write_size_per_second主服务器每秒产生的写命令数据数量。

  • 服务器的运行ID(run ID)

实现复制重同步怎么还和服务器的运行ID有关了那?我们先来介绍下运行ID。

每个Redis服务器都有自己的运行ID,在服务器启动时自动生成,由40个随机的十六进制字符生成。当从服务器对主服务器初次复制的时候,主服务器会将自己的运行ID传给从服务器,从服务器这边会把主节点运行ID保存下来。

后面从服务器断线重连进行同步会把之前保存的主服务器运行ID传给当前主服务器。主服务器就会和自己的运行ID进行判断,如果和自身相同则会判断其它条件是否满足,满足的话则会进行部分重同步。如果和自身不相同的话,则会执行完整重同步。这就是为什么和服务器的运行ID的原因了。

PSYNC命令的实现

PSYNC命令的实现.png

心跳检测

我们再来看下在命令传播阶段,也就是同步之后从服务器接收来自master发来的命令副本阶段。这个时候,从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK <replication_offset>

其中replication_offset为从服务器当前的复制偏移量。

那么心跳检测有什么作用那

  • 检测主从服务器的网络连接状态

    如果主服务器超过一秒钟没有收到从服务器发来的REPLCONF ACK命令,则主服务器就会发现主从服务器之间的连接出现问题了, 通过向主服务器发送INFO replication命令,列出从服务器列表的lag记录了最后心跳检测到现在时长(秒)。一般lag值在0s-1s之间浮动超过1s则说明连接出现故障。

  • 辅助实现min-slaves选项

    防止主服务器在不安全状态下执行写命令。通过下面两个选项来设置拒绝写命令的条件:

#
# The N replicas need to be in "online" state.
#
# The lag in seconds, that must be <= the specified value, is calculated from
# the last ping received from the replica, that is usually sent every second.
#
# This option does not GUARANTEE that N replicas will accept the write, but
# will limit the window of exposure for lost writes in case not enough replicas
# are available, to the specified number of seconds.
#
# For example to require at least 3 replicas with a lag <= 10 seconds use:
#
# min-replicas-to-write 3
# min-replicas-max-lag 10
#
# Setting one or the other to 0 disables the feature.
#
# By default min-replicas-to-write is set to 0 (feature disabled) and
# min-replicas-max-lag is set to 10.
复制代码

min-replicas-to-write和min-replicas-max-lag这俩选项配置。默认是配置:

min-replicas-to-write 0
min-replicas-max-lag 10
复制代码

min-replicas-to-write设置为0或min-replicas-max-lag设置为0则表示禁用该功能。

我们来看一个配置的例子:

min-replicas-to-write 3
min-replicas-max-lag 10
复制代码

上面配置表示正常状态从服务器少于3个,或者三个服务器的延迟(lag)值都大于或等于10s时,主服务器则会拒绝执行写命令。

  • 检测命令丢失

若因网络故障,主节点传给从节点写命令在途中丢失,那么心跳检测发送REPLCONF ACK <replication_offset>命令,主服务器会根据从节点的复制偏移量检测出数据丢失,则会执行部分重同步补发缺失的数据。这是为了保持主从数据的一致性。

总结

1.Redis2.8版本之前不能高效处理断线后重复制,Redis2.8解决了该问题,则建议使用Redis2.8及以上版本。

2.从服务器在同步以后通过主服务器传过来的命令副本执行,保持主从服务器数据一致。从服务器通过向主服务器发送命令进行心跳检测,来检查命令丢失,主从网络连接状态以及主从当前健康状态。

参考书籍:《Redis设计与实现》《深入分布式缓存》

猜你喜欢

转载自juejin.im/post/7017445725973774372