概述
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
数据冗余
主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。故障恢复
当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
主从复制的流程
建立连接阶段
数据同步阶段
主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。
数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。
- 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
- 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。
需要注意的是,在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。
全量 or 部分
由于全量复制在主节点数据量较大时效率太低,因此Redis2.8开始提供部分复制,用于处理网络中断时的数据同步。部分复制的实现,依赖于三个重要的概念:
复制偏移量
主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。复制积压缓冲区
复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。服务器运行id
每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。
判断流程
全量复制
1、主节点接收到全量同步的请求时,fork一个子进程进行bgsave,同时将接下来的写操作保存至复制缓冲区;
2、RDB保存完毕后,向从服务器发送;
3、从服务器清除当前的内存数据
4、阻塞客户端请求,拒绝服务
5、从RDB加载数据,执行这些写命令,将数据库状态更新至主节点的最新状态
6、若开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态
部分复制
在部分复制阶段,从服务器只要拉取并执行部分主服务的写命令即可。
命令传播阶段
当从服务器完成了对主服务器的同步操作,就进入了命令传播阶段。在这个阶段,主服务器会将自己的写命令同步给从服务器,从而保持数据一致性
除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。心跳机制对于主从复制的超时判断、数据安全等有作用。
主 -> 从:PING
每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。
PING发送的频率由repl-ping-slave-period参数控制,单位是秒,默认值是10s。
从 -> 主:REPLCONF ACK
在命令传播阶段,从节点会向主节点发送REPLCONF ACK命令,频率是每秒1次。
命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量。
REPLCONF ACK命令的作用包括:
- 实时监测主从节点网络状态
该命令会被主节点用于复制超时的判断。此外,在主节点中使用info Replication,可以看到其从节点的状态中的lag值,代表的是主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1。 - 检测命令丢失
从节点发送了自身的offset,主节点会与自己的offset对比,如果从节点数据缺失(如网络丢包),主节点会推送缺失的数据(这里也会利用复制积压缓冲区)。注意,offset和复制积压缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。 - 辅助保证从节点的数量和延迟
Redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令;所谓不安全,是指从节点数量太少,或延迟过高。例如min-slaves-to-write和min-slaves-max-lag分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。而这里从节点延迟值的获取,就是通过主节点接收到REPLCONF ACK命令的时间来判断的。
问题
数据一致性问题
数据延迟
由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:(1) 优化主从节点之间的网络环境(如在同机房部署);(2) 监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;(3) 使用集群同时扩展写负载和读负载等。
在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。数据过期
在单机版Redis中,存在两种删除策略。
惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。
Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。故障切换
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
连接超时
意义
- 如果主节点判断连接超时,其会释放相应从节点的连接,从而释放各种资源,否则无效的从节点仍会占用主节点的各种资源(输出缓冲区、带宽、连接等);此外连接超时的判断可以让主节点更准确的知道当前有效从节点的个数,有助于保证数据安全(配合前面讲到的min-slaves-to-write等参数)。
- 如果从节点判断连接超时,则可以及时重新建立连接,避免与主节点数据长期的不一致。
判断机制
主从复制超时判断的核心,在于repl-timeout参数,该参数规定了超时时间的阈值(默认60s),对于主节点和从节点同时有效;主从节点触发超时的条件分别如下:
- 主节点:每秒1次调用复制定时函数replicationCron(),在其中判断当前时间距离上次收到各个从节点REPLCONF ACK的时间,是否超过了repl-timeout值,如果超过了则释放相应从节点的连接。
- 从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:
(1) 如果当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过repl-timeout,则释放与主节点的连接;
(2) 如果当前处于数据同步阶段,且收到主节点的RDB文件的时间超时,则停止数据同步,释放连接;
(3) 如果当前处于命令传播阶段,且距离上次收到主节点的PING命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。
问题
连接超时会使主从进入重连阶段,若超时时间较短,而进入部分同步阶段,此时从服务器阻塞;若超时时间较长,则进入全量同步阶段,主服务器需要消耗大量的内存和cpu时间用于bgsave,同时rdb的传输会占据主服务器的大部分带宽,直接影响了主服务器的吞吐量。在加载RDB阶段,从服务器会阻塞客户端请求,拒绝服务。所以我们应该尽量避免超时问题。
实际问题:
- 数据同步阶段
在主从节点进行全量复制bgsave时,主节点需要首先fork子进程将当前数据保存到RDB文件中,然后再将RDB文件通过网络传输到从节点。如果RDB文件过大,主节点在fork子进程+保存RDB文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时;此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连……这是个悲伤的循环。为了避免这种情况的发生,除了注意Redis单机数据量不要过大,另一方面就是适当增大repl-timeout值,具体的大小可以根据bgsave耗时来调整。 - 命令传播阶段:在该阶段主节点会向从节点发送PING命令,频率由repl-ping-slave-period控制;该参数应明显小于repl-timeout值(后者至少是前者的几倍)。否则,如果两个参数相等或接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点发送数据,则从节点很容易判断超时。
- 慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询(如
keys *
或者对大数据的hgetall
等),导致服务器阻塞;阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。