Redis主从复制基础概念
Redis主从复制主要有以下作用:
- 数据冗余:将数据热备份到从节点,即使主节点由于故障而丢失数据。从节点依然保留了数据副本;
- 读写分离:可以由主节点提供写服务,从节点提供读服务,提高Redis服务的整体吞吐量;
- 故障恢复:主节点故障下线后,可以手动将从节点切换为主节点,继续提供服务;
- 高可用基础:主从复制机制是Sentinel和Cluster机制的基础,两者都实现了故障转移,即主节点故障后,Redis负责选择一个从节点切换为主节点,继续提供服务。
主从复制依赖以下三个重要机制:
- 当从节点与主节点之间的连接正常时,主节点会把自己执行过的写命令发送给从节点(包括客户端的写操作、key过期或被驱逐等会改变主节点数据集的动作);
- 当从节点与主节点之间的连接由于网络故障或者超时机制断开时,从节点会自动重连并尝试进行部分同步(用于获取连接中断期间复制失败的命令);
- 如果从节点在断开重连后尝试进行部分同步未成功,就会向主节点要求进行一次全量同步。主节点会通过生成快照的方式将全库的RDB文件发送给从节点,并在全量同步完成后继续把自己新执行的写命令发送给从节点。
Redis主从复制默认采用异步复制,即主节点在发送命令给从节点后并不会等待从节点返回ACK确认。异步复制的优点是低延迟、高性能,其缺点则是主从节点的数据在短期内有可能不一致。从节点会异步周期性地向主节点告知自己接收到的数据量,所以主节点可以知道哪些从节点接收到了哪些命令。
主从复制的几个要点
- Redis主从复制是异步的,从节点向主节点返回ACK确认接收到的数据量也是异步的。
- 一个主节点可以有多个从节点作为副本。
- 除了一主多从的复制架构,级联复制(cascade)也是可行的。即一个从节点可以连接到另一个从节点,从而实现
从从节点->从节点->主节点
的复制架构。 - 主从复制在主节点上是非阻塞的。无论是全量同步还是部分同步期间,主节点都能正常对外提供查询服务。
- 主从复制在从节点上大多数情况下也是非阻塞的。从节点在进行初始的全量同步时,根据
redis.conf
中的配置,依然可以处理对旧版本数据的查询(也可以配置为直接向客户端返回报错)。在初始的全量同步完成后,会进行旧数据集的删除以及新数据集的加载。在此过程中,从节点会阻塞新连接的建立,阻塞的时间根据数据量的大小可以长达数秒。 - 主从复制可以用于提升数据安全性和系统高可用性,也可以用于实现读写分流,比如将时间复杂度为O(N)的读操作下放到从节点进行。
- 主从复制可以用来节省主节点将整个数据集持久化的存储开销(不推荐)。通过配置
redis.conf
来关闭主节点的持久化,然后将一个开启了AOF持久化的从节点连接到主节点来保存复制的数据。这种配置存在的风险在于,主节点重启后本身内存中的数据会被清空,如果这个空的数据集被同步到从节点,会导致从节点原来保存的数据被清空。
主节点关闭持久化的风险
在Redis主从同步架构中,强烈建议主节点和从节点同时开启持久化。如果不能配置持久化,Redis实例应该配置成在服务器重启后禁用(实例)自动重启。
考虑下面的场景:
- 节点A作为Redis主从复制中的主节点,关闭了持久化。节点B和C是从节点。
- 主节点A崩溃后自动重启了实例,由于关闭了持久化,重启时A节点的数据集为空。
- 从节点B和C在恢复连接后,重新与主节点A建立复制同步关系,从节点A复制过来的空数据集会导致从节点原有的数据被清空。
主从复制的实现原理
每个Redis主节点都有一个复制ID(Replication ID)。复制ID是一个很大的伪随机字符串,用于标识一个给定的数据集。每当主节点上产生了新的可以同步给从节点的数据变化时,主节点就会记录一个递增的偏移量(offset),用于将同样的变化应用到从节点来保持数据同步。即使没有任何从节点连接,主节点也会一直记录数据集的偏移量。也就是说,一对(repliaction_ID, offset)
正好标识了主节点数据集的一个准确的版本。
当从节点连接到主节点时,会通过PSYNC命令把它们已经接收到的、最新的复制ID和偏移量发送给主节点。主节点只要根据从节点发送过来的复制ID和偏移量,进行对应数据集版本的部分(增量)同步即可。如果主节点缓冲区的backlog空间不足、或者从节点返回的复制ID时间太久已经无从查找,此时主节点就会对从节点进行全量同步。
发生全量同步时,主节点会启动一个后台进程来将数据集保存为RDB文件,同时将客户端发起的写命令都保存到缓冲区。后台的保存进程结束后,主节点会把RDB文件发送给从节点。从节点将接收到的RDB文件保存到磁盘中,然后加载到内存中。最后主节点再把缓冲区中的写命令同步到从节点。
如果同时收到多个并发的从节点全量同步请求,主节点只需在后台进行一次全量同步的RDB文件生成。
复制ID是什么
如果两个Redis实例具有相同的复制ID和复制偏移量,那么它们的数据集就是完全相同的。每当一个实例作为主节点从零开始启动时,或者作为从节点被提升为主节点时,该实例就会生成一个新的复制ID。连接到主节点的从节点在握手后会继承主节点的复制ID。复制ID相同的实例拥有相同的数据集,但是可能分别对应该数据集在不同时间的不同版本。
假设实例A和实例B的复制ID相同,但是前者的复制偏移量是1000,而后者的是1023,则说明实例A还缺少一些命令没有应用到数据集上。通过应用这些命令,实例A的数据集可以达到实例B的状态。
Redis实例有两个复制ID:主ID(main ID)和辅助ID(secondary ID)。在发生故障转移切换时,被提升为新主节点的从节点依然需要保留原来旧主节点的复制ID。新的主节点会把原来旧主节点的复制ID设置为辅助ID,同时生成一个新的复制ID,并记录下发生ID切换时的复制偏移量。当其他从节点连接到新的主节点时,主节点会将它们返回的复制ID和偏移量同时与主ID和辅助ID做对比,这样新的主节点与从节点进行同步时只需要进行部分同步即可,而无需进行全量同步。
发生故障转移切换时,为什么要生成一个新的复制ID?这么做是为了避免由于网络原因发生了主从切换,而实际上旧的主节点依然在正常运行,如果不生成新的复制ID,新的主节点的复制ID就会与旧的主节点冲突,而实际上它们代表的数据集已经是不一样的了。
无盘复制
通常情况下,全量同步需要在磁盘中生成一个RDB文件,传输到从节点后,从节点再从磁盘中把数据加载到内存中。如果磁盘的性能较差,这个过程会给主节点带来较大的压力。
Redis从2.8.18
版本起开始支持无盘复制,支持通过一个子进程将RDB数据直接发送给从节点,无需在主节点落盘。
主从复制配置
主从复制的配置较为简单,只需在从节点的配置中加上下面一行:
replicaof 192.168.1.1 6379 # 主节点的IP和端口
repl-backlog-size
参数用于控制缓冲区中backlog区域的大小,默认为1MB。当从节点与主节点的连接意外断开时,主节点上新生成的副本数据会存放在backlog区域中,这样等到从节点恢复正常连接时,就可以通过部分同步把backlog中的数据复制给从节点,从而避免了全量同步。
repl-backlog-ttl
参数用于控制backlog区域中RDB数据保留的时间,默认为3600秒。也就是说,从最后一个从节点断开的时间开始算起,经过1小时候,主节点的backlog缓冲区就会被释放。如果不想因为超时而释放backlog区域,可以将该参数设置为0。
repl-diskless-sync
参数用于开启无盘复制。repl-diskless-sync-delay
则定义了在第一个从节点准备好接收RDB文件传输时主节点会额外等待的秒数(以便有更多的从节点可以加入进来),默认为5秒。
只读副本
Redis从2.6版本起开始支持只读副本模式。可以通过配置文件中的replica-read-only
或者使用CONFIG SET
命令来将从节点配置为只读模式。
从节点可写会导致主节点和从节点之间的数据不一致。主节点执行的数据修改命令会通过主从复制传播到从节点。当主节点上有key过期时,会被转化为DEL命令传播给从节点。如果同一个key在主节点上存在,但是(由于从节点可写)在从节点上被删除、过期、或者类型变了,那么在主节点上对该key执行的操作,在从节点上就可能会失败或者产生不一样的效果。
如果不得不使用可写的从节点,最好注意以下两点:
- 对于同时存在于主节点和从节点的key,不要在从节点对其进行写操作;
- 对运行中的一组实例进行升级时,不要将实例配置为可写副本作为中间步骤。为了保证数据一致性,如果从节点能够被提升为主节点,则不要将其配置为可写副本。
由于历史原因,存在一些使用场景中可以用到可写副本。不过这些使用场景到Redis 7.0时都有其他方法可以实现。
- 使用
SUNIONSTORE
和ZUNIONSTORE
等命令来进行比较费时的集合(或有序集合)运算。可以用SUNION
和ZUNION
命令来替代。 - 使用
SORT
命令进行排序。由于支持STORE选项将排序结果存储到List中,SORT命令被认为是一个写命令,因此不能在只读副本上执行。可以用只读模式下的排序命令SORT_RO
代替。 EVAL
和EVALSHA
命令也被认为是写命令,因为两者调用的Lua脚本中可能会使用写命令。在只读副本模式下,可以使用对应的EVAL_RO
和EVALSHA_RO
命令来替代。
从Redis 4.0开始,从节点上的写操作都是本地的,在级联复制架构中不会传播到连接到该从节点的从从节点。
同步密码配置
如果主节点有密码保护,从节点在开启同步前需要通过密码认证。
对于运行中的实例,在redis-cli中执行以下命令来配置:
config set masterauth <password>
或者在配置文件中添加:
masterauth <password>
最小副本数配置
从Redis 2.8开始,你可以将主节点配置成:只有在至少N个从节点连接到主节点时,主节点才会接受写操作。由于Redis主从复制是异步复制,不能保证从节点已经接收到了复制过去的写命令,因此总是会存在数据丢失的可能性。
最小副本数的实现机制如下:
- 从节点每秒都会ping一次主节点,确认处理了多少复制流(replication stream);
- 主节点会记住上一次从每个从节点接收到ping的时间;
- 用户可以配置延迟不小于一定秒数的最小从节点数目。
最小副本数的配置由以下两个参数控制:
min-replicas-to-write
:主节点能够允许写操作时的最小从节点连接数,默认值为0;min-replicas-max-lag
:距离主节点上一次从每个从节点收到ping后允许经历的最大延迟,默认为10秒。
如果有min-replicas-to-write
个从节点连接到了主节点,并且主节点接收到每个从节点发送ping的间隔都没有超过min-replicas-max-lag
秒,主节点就能够正常接受写操作。否则,主节点就会返回报错,并且拒绝执行写命令。
键过期的处理
Redis通过使键过期来限制其存活时间(time to live, TTL)。Redis从节点能够准确复制有过期时间的key,即使这些key被Lua脚本修改过。
Redis中key的过期机制依赖于实例的计时。从节点准确复制会过期的key不是靠主从节点之间同步时钟来实现的,而是主要依靠以下三个机制:
-
从节点上的key不会过期,但是它们会等主节点上的key过期。当主节点上有key过期(或者被LRU算法淘汰出内存)时,主节点会将其转化为DEL该key的命令复制给所有从节点。
-
有时候由于主节点没有及时传递DEL命令,会导致从节点内存中依然存在逻辑上已经过期的key。为了解决这个问题,在不违反数据一致性的前提下,从节点会利用自己的逻辑时钟向那些读操作返回(已经在主节点过期但是还存在于从节点内存中的)key不存在的消息,以避免读到已过期的key。
-
在执行Lua脚本期间,key不会过期。运行Lua脚本时,主节点上的时间可以被认为是“冻结”的。所以在Lua脚本执行期间,key要么一直存在,要么一直不存在。
当从节点被提升为主节点后,它拥有的key就可以独立地过期了,而不需要再借助于旧的主节点。
INFO和ROLE命令
INFO和ROLE命令可以提供当前主从复制中主从实例的复制参数配置的相关信息。
执行INFO replication
命令来获取主从复制相关的信息。
执行ROLE
来获取当前节点在主从复制中的角色(主节点/从节点)、以及复制偏移量等信息。
故障转移后的部分同步
从Redis 4.0开始,当有从节点在故障转移后被提升为主节点时,它可以与旧主节点的从节点进行部分同步(partial resynchronization)。新的主节点需要记录旧主节点的复制ID和偏移量。
新主节点的复制ID与旧主节点的复制ID不相同。原因是旧主节点在故障转移后的短时间内可能依然会处于可写状态,这会导致新旧主节点的数据集有所差别,所以不能用相同的复制ID来表示。
当从节点被优雅地关机重启(使用SHUTDOWN
命令)时,从节点会在RDB文件中存储与主节点同步所需要的信息。
使用AOF进行持久化的从节点无法在重启后进行部分同步。为了避免这个问题,可以在关机前将实例的持久化方式改为RDB,然后在重启改回AOF持久化。
从节点的maxmemory
从节点默认会忽略maxmemory
参数(除非被提升为主节点)。这意味着从节点上key的驱逐淘汰由主节点控制(通过向从节点发送DEL命令)。这么做是为了保证主从节点的数据一致性。
如果从节点可写,或者要给从节点不同的内存配置,并且你能保证从节点上的写操作都是幂等的(idempotent),你也可以让从节点不忽略maxmemory
参数。只需在从节点上进行如下配置:
replica-ignore-maxmemory no
由于从节点自己不会因为内存使用率高而淘汰key,所以最终从节点使用的内存可能会超过maxmemory
参数设定的值。因此最好对从节点的内存使用状况做好监控,确保从节点上有足够可用的内存,避免在主节点内存使用达到maxmemory
之前从节点出现OOM现象。
References
【1】https://redis.io/docs/management/replication/
【2】https://raw.githubusercontent.com/redis/redis/6.0/redis.conf