为何需要主从复制
Redis作为开发过程中经常用到的缓存数据库,我们在开发过程中可能会遇到各种各样的问题,比如:
Redis的读并发量太大怎么办?
单机版的Redis挂掉怎么办?
我们不仅需要并发,更需要安全!
主从复制的概念
官网说明
在Redis复制的基础上(不包括由Redis Cluster或Redis Sentinel作为附加层提供的高可用性功能),有一个非常简单的使用和配置领导者跟随者(主从)复制:它允许从属Redis实例连接主实例的副本。每次链接断开时,从站将自动重新连接到主站,并且无论主站发生什么情况,它都会尝试成为它的精确副本。
定义
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用
主从复制的作用主要包括:
数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
该系统使用三种主要机制:
1、当主实例和从属实例连接良好时,主设备通过向从设备发送命令流来保持从设备更新,以便复制对主设备端发生的数据集的影响,原因是:客户端写入,密钥已过期或驱逐,更改主数据集的任何其他操作。
2、当主设备和从设备之间的链路中断时,对于网络问题或者由于主设备或从设备中检测到超时,从设备重新连接并尝试继续部分重新同步:这意味着它将尝试仅获取部件它在断开连接时错过的命令流。
3、当无法进行部分重新同步时,从站将要求完全重新同步。这将涉及一个更复杂的过程,其中主机需要创建其所有数据的快照,将其发送到从机,然后在数据集更改时继续发送命令流。
Redis默认使用异步复制,即低延迟和高性能,是绝大多数Redis用例的自然复制模式。
但是,Redis从站异步确认它们与主站定期收到的数据量。因此主设备不会每次等待从设备处理命令,但是如果需要,它知道哪个从设备已经处理了什么命令。这允许具有可选的同步复制。
总结:
复制,也就是我们说的主从复制,主机数据更新后根据配置和策略,自动同步到从机的master/slaver机制,Master以写为主,Slave以读为主。
使用主从复制可以实现读写分离、容灾恢复。
主从复制原理
1.连接建立阶段(准备工作)
该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。
这是复制之前的准备工作。
步骤1:保存主节点信息
当执行命令slaveof时,会将主节点的ip和port信息存储到从节点中。
从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。
需要注意的是,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。
步骤2:建立socket连接
从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。如果连接成功,则:
从节点:为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。
主节点:接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行
步骤3:发送ping命令
从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。
从节点发送ping命令后,可能出现3种情况:
(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。
(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。
(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。
步骤4:身份验证
如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。
如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。
步骤5:发送从节点端口信息
身份验证之后,从节点会向主节点发送其监听的端口号,主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。
2. 数据同步阶段
主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。
需要注意的是:
在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。
原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。
3. 命令传播阶段
数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。由于心跳机制的原理涉及部分复制,因此将在介绍了部分复制的相关内容后单独介绍该心跳机制。
数据同步阶段详解
在主从复制的过程中,第二步数据同步无疑是比较重要的,它是redis实现数据同步的核心。数据同步大致可以分为两类,全量复制和部分复制。
Redis主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
如果多个Slave断线了,需要重启的时候,因为只要Slave启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机。
全量复制
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。
下面的全量复制原理图可以让我们更直观的理解数据同步期间,结合图例,我们来看一下redis是如何工作的。
- Redis 内部会发出一个同步命令,刚开始是 Psync 命令,Psync ? -1表示要求 master 主机同步数据,?表示主机运行id,这时还不知道。-1代表进行全量复制。
- 主机会向从机发送 runid (redis-cli info server)和 offset,因为 slave 并没有对应的 offset,所以是全量复制。从机slave会保存主机master的基本信息save masterinf
- 主节点收到全量复制的命令后,执行bgsave(异步执行),在后台生成RDB文件(快照)
- 主机send RDB发送RDB文件给从机
- 在RDB文件生成并让从节点进行复制的过程中,使用一个缓冲区(称为复制缓冲区,也称客户端缓冲区)记录从现在开始执行的所有命令
- 刷新旧的数据,从节点在载入主节点的数据之前要先将老数据清除,加载RDB文件将数据库状态更新至主节点执行bgsave时的数据库状态
- 发送缓冲区数据,缓冲区数据加载,以此保证主从节点数据一致性
- 如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof操作, 为了保证全量复制后AOF持久化文件立刻可用
全量复制的开销
bgsave时间
RDB文件网络传输时间
从节点清空数据的时间
从节点加载RDB的时间
全量复制的极易失败原因
如果主节点创建和传输RDB的时间过长, 对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。
全量复制的时间阈值默认在60s
客户端缓冲区默认配置为clientoutput-buffer-limit slave 256MB 64MB 60,
如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,或者60s内未完成全量复制, 主节点将直接关闭复制客户端连接, 造成全量同步失败。 对应日志如下:
M 27 May 12:13:33.669 # Client id=2 addr=127.0.0.1:24555 age=1 idle=1 flags=S
qbuf=0 qbuf-free=0 obl=18824 oll=21382 omem=268442640 events=r cmd=psync
scheduled to be closed ASAP for overcoming of output buffer limits.
运维人员需要根据主节点数据量和写命令并发量调整clientoutput-buffer-limit slave配置, 避免全量复制期间客户端缓冲区溢出。
例如我们线上数据量在6G左右的主节点, 从节点发起全量复制的总耗时在2分钟左右。
部分复制
因此当数据量达到一定规模之后, 由于全量复制过程中将进行多次持久化相关操作和网络数据传输, 这期间会大量消耗主从节点所在服务器的CPU、 内存和网络资源。
所以除了第一次复制时采用全量复制在所难免之外, 对于其他场景应该规避全量复制的发生。 正因为全量复制的成本问题, Redis实现了部分复制功能。
部分复制是 Redis 2.8 以后出现的,之所以要加入部分复制,除了因为全量复制会产生很多问题,比如像上面的时间开销大、无法隔离等问题, Redis 希望能够在 master 出现抖动(相当于断开连接)的时候,可以有一些机制将复制的损失降低到最低。
部分复制原理图
-
如果网络抖动(连接断开 connection lost)
-
主机master 还是会写 replbackbuffer(复制积压缓冲区)
-
从机slave 会继续尝试连接主机
-
从机slave 会把自己当前 runid 和偏移量传输给主机 master,并且执行 pysnc 命令同步
-
如果 master 发现你的偏移量是在缓冲区的范围内,就会返回 continue 命令
-
同步了 offset 的部分数据,所以部分复制的基础就是偏移量 offset。
确定进行部分复制后,Redis如何具体执行?
repl_backlog_buffer:它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。如果从库断开时间太久,repl_backlog_buffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。而在repl_backlog_buffer中找主从差异的数据后,如何发给从库呢?这就用到了replication buffer。
replication buffer:Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个 内存buffer进行数据交互,客户端是一个client,从库也是一个client,我们每个client连上Redis后,Redis都会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。
正常情况下Redis是如何决定全量复制还是部分复制(增量复制)
主节点通过两个数据进行判断:runid和offset
-
主节点根据runid判断能否进行部分复制;
如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况)
如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制 -
对runid进行判断后,要结合offset和复制积压缓冲区进行判断
从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制
如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤压),则执行全量复制
每个从库会记录自己的slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过psync命令把自己记录的slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。
缓冲区大小调节:
由于缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲去的小打(通过配置repl_backlog_size)来设置;例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100kb,则复制积压缓冲区的平均需求为6MB,保险起见可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。
主从复制的一些问题
1.当主服务器不进行持久化时复制的安全性
在进行主从复制设置时,强烈建议在主服务器上开启持久化,当不能这么做时,比如考虑到延迟的问题,应该将实例配置为避免自动重启。
为什么不持久化的主服务器自动重启非常危险呢?
我们设置节点A为主服务器,关闭持久化,节点B和C从节点A复制数据。
这时出现了一个崩溃,但Redis具有自动重启系统,重启了进程,因为关闭了持久化,节点重启后只有一个空的数据集。
节点B和C从节点A进行复制,现在节点A是空的,所以节点B和C上的复制数据也会被删除。
当在高可用系统中使用Redis Sentinel(哨兵),关闭了主服务器的持久化,并且允许自动重启,这种情况是很危险的。比如主服务器可能在很短的时间就完成了重启,以至于Sentinel都无法检测到这次失败,那么上面说的这种失败的情况就发生了。
如果数据比较重要,并且在使用主从复制时关闭了主服务器持久化功能的场景中,都应该禁止实例自动重启。
2.为什么主从全量复制使用RDB而不使用AOF?
1、RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量复制的成本最低。
2、假设要使用AOF做全量复制,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。
3.为什么还有无磁盘复制模式?
Redis 默认是磁盘复制,但是如果使用比较低速的磁盘,这种操作会给主服务器带来较大的压力。Redis从2.8.18版本开始尝试支持无磁盘的复制。使用这种设置时,子进程直接将RDB通过网络发送给从服务器,不使用磁盘作为中间存储。
无磁盘复制模式:master创建一个新进程直接dump RDB到slave的socket,不经过主进程,不经过硬盘。适用于disk较慢,并且网络较快的时候。
使用repl-diskless-sync配置参数来启动无磁盘复制。
使用repl-diskless-sync-delay 参数来配置传输开始的延迟时间;master等待一个repl-diskless-sync-delay的秒数,如果没slave来的话,就直接传,后来的得排队等了; 否则就可以一起传。
4.为什么还会有从库的从库的设计?
主从架构可以有多种形式,一主一从,一主多从,树状主从。
通过分析主从库间第一次数据同步的过程,你可以看到,一次全量复制中,对于主库来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件。
如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量复制。fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。此外,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。那么,有没有好的解决方法可以分担主库压力呢?
其实是有的,这就是“主 - 从 - 从”模式。
在刚才介绍的主从库模式中,所有的从库都是和主库连接,所有的全量复制也都是和主库进行的。现在,我们可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
这样一来,这些从库就会知道,在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力,如下图所示:
级联的“主-从-从”模式好了,到这里,我们了解了主从库间通过全量复制实现数据同步的过程,以及通过“主 - 从 - 从”模式分担主库压力的方式。那么,一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。
5.读写分离及其中的问题
在主从复制基础上实现的读写分离,可以实现Redis的读负载均衡:由主节点提供写服务,由一个或多个从节点提供读服务(多个从节点既可以提高数据冗余程度,也可以最大化读负载能力);在读负载较大的应用场景下,可以大大提高Redis服务器的并发量。下面介绍在使用Redis读写分离时,需要注意的问题。
延迟与不一致问题
前面已经讲到,由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:
优化主从节点之间的网络环境(如在同机房部署);
监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;
使用集群同时扩展写负载和读负载等。
在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。
数据过期问题
在单机版Redis中,存在两种删除策略:
惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。
Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。
故障切换问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
总结
在使用读写分离之前,可以考虑其他方法增加Redis的读负载能力:
如尽量优化主节点(减少慢查询、减少持久化等其他情况带来的阻塞等)提高负载能力;
使用Redis集群同时提高读负载能力和写负载能力等。
如果使用读写分离,可以使用哨兵,使主从节点的故障切换尽可能自动化,并减少对应用程序的侵入。