参考资料更详细,本文只是笔记和个人解读
一 作用和概述
主从复制侧重解决数据的多机热备
。此外,主从复制还可以实现负载均衡(读写分离,延迟是少不了的
)和故障恢复(服务冗余
)、是哨兵和集群能够实施的基础
单向性
- 主从复制是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
- 默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
二 如何使用主从复制
2.1 建立复制
一共有3种方式
2.2 取消复制
三 实现原理
主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段
3.1 连接建立阶段
该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备
。(也就是这个阶段种,不会涉及主从之间的数据复制)
步骤1-保存主节点信息
从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。
需要注意的是,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。
步骤2-建立socket连接
重点
- 从节点
每秒一次
尝试连接 - 连接上后,从节点会有
一个
处理器负责复制工作 - 后面的步骤都是
从节点主动发送命令请求(这样的设计方式比较好,也有事件驱动的感觉,减少了主节点的作用。很像我之前项目中数据同步)
来完成的
步骤3-发送ping命令
就是从节点预先检查
主节点能否处理他的主从复制
步骤4-身份验证
为什么还是要重连呢?
因为管理员可以配置主节点并重启
步骤5-发送从节点端口信息
身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的slave_listening_port字
段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。
该阶段的最后一步了,还是没有任何同步操作
,只是打通了连接。发送自己的端口信息,没什么具体作用😂
3.2 数据同步阶段
初始化阶段(重要),从节点不再只是客户端。两个节点互为客户端(有点像websocket、主节点可以主动推送最新的命令
).具体实现细节,后面说。
3.3 命令传播阶段
数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。由于心跳机制的原理涉及部分复制,因此将在介绍了部分复制的相关内容后单独介绍该心跳机制。
延迟与不一致!!!
命令传播是异步
的过程,即主节点发送写命令后并不会等待从节点的回复
;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay
配置等有关。
repl-disable-tcp-nodelay no:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP_NODELAY;默认no,即不禁止TCP_NODELAY。当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差
;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。(就是像等车一样,现在会尝试等等。这样能少跑几趟
)
一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为yes;多数情况使用默认值no。
四 【数据同步阶段】全量复制和部分复制
在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后,从节点可以发送psync
命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是
全量复制或部分复制。后文介绍以Redis2.8及以后版本为例。
- 全量复制:用于
初次复制或其他无法进行部分复制(有两种可能,不是想部分复制就可以部分复制)
的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。 - 部分复制:用于
网络中断等
情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令(如果主节点不能找到中断后的所有命令
),则无法进行部分复制,仍使用全量复制。
4.1 全量复制
Redis通过psync命令进行全量复制的过程如下:
- 从节点判断无法进行部分复制(
是从节点自己发送全量复制请求
),向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断
无法进行增量复制;具体判断过程需要在讲述了部分复制原理后再介绍。(两边都会判断
) - 主节点收到全量复制的命令后,执行
bgsave
,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区(不是积压))记录从现在开始执行的所有写命令 - 主节点的bgsave执行完成后,将RDB文件发送给从节点(
这样从节点执行这个文件,就能恢复成主节点的快照版本;很明显,即使从节点原来有数据,在这一刻也没了!😐
);从节点首先清除自己的旧数据
,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态 - 主节点将前述复制
缓冲区中的所有写命令
发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态(这个逻辑不难
) - 如果从节点开启了AOF,
则会触发bgrewriteaof
的执行,从而保证AOF文件更新至主节点的最新状态
4.2 部分复制
部分复制的实现,依赖于三个重要的概念:
(1)复制偏移量
主节点和从节点分别维护一个复制偏移量(offset
),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。(两边都有一个offset
)
offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。例如,如果主节点的offset是1000,而从节点的offset是500,那么部分复制就需要将offset为501-1000
的数据传递给从节点。而offset为501-1000的数据存储的位置,就是下面要介绍的复制积压缓冲区。
(2)复制积压缓冲区(就是备用的)
复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列
,默认大小1MB;当主节点开始有从节点时创建(第一个从节点连上来时创建
),其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。
在命令传播阶段,主节点除了将写命令发送给从节点,还会(这说明命令传播阶段只发实时数据
)发送一份给复制积压缓冲区,作为写命令的备份
;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。
由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大
超过缓冲区长度时,将无法执行部分复制,只能执行全量复制
。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。
从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制:
如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。
(3)服务器运行ID(runid)
每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid:
主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点
根据runid判断能否进行部分复制:(前面说过两边都会判断
)
如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
五 【命令传播阶段】心跳机制
在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。心跳机制对于主从复制的超时判断
、数据安全
等有作用。
4.1 主->从:PING
每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。
PING发送的频率由repl-ping-slave-period
参数控制,单位是秒,默认值是10s。
5.2 从->主: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命令的时间
来判断的,即前面所说的info Replication中的lag值
。
六 应用中的问题
6.1 读写分离及其中的问题
延迟与不一致问题
在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数
便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no
数据过期问题
将Redis升级到3.2可以解决数据过期问题(这个很简单,没啥说的
)
故障切换问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低(这个也没啥大问题,因为有结合哨兵可以自动切换
)
总结
在使用读写分离之前,可以考虑其他方法增加Redis的读负载能力(这意思,读写因为有延迟,尽量当热备份?
):如尽量优化主节点(减少慢查询、减少持久化等其他情况带来的阻塞等)提高负载能力;使用Redis集群同时提高读负载能力和写负载能力等。如果使用读写分离,可以使用哨兵,使主从节点的故障切换尽可能自动化
,并减少对应用程序的侵入。
6.2 复制超时问题
主从节点复制超时是导致复制中断的最重要的原因之一
超时判断意义
在复制连接建立过程中及之后(三个阶段都有,这是主从复制
),主从节点都有机制判断连接是否超时,其意义在于:
(1)如果主节点判断连接超时,其会释放
相应从节点的连接,从而释放各种资源
,否则无效的从节点仍会占用主节点的各种资源(输出缓冲区、带宽、连接等);此外连接超时的判断可以让主节点更准确的知道当前有效从节点的个数,有助于保证数据安全
(配合前面讲到的min-slaves-to-write
等参数)。(掉了就把他t了,不会自己去找他
)
(2)如果从节点判断连接超时,则可以及时重新建立连接
,避免与主节点数据长期的不一致。(不然自己掉了都不知道
)
判断机制
主从复制超时判断的核心,在于repl-timeout参数
,该参数规定了超时时间的阈值(默认60s)什么叫阈值,就是可容忍的范围。
,对于主节点和从节点同时有效
;主从节点触发超时的条件分别如下:
- 主节点:每秒1次调用
复制定时函数replicationCron()
,在其中判断当前时间距离上次收到各个从节点REPLCONF ACK
的时间,是否超过了repl-timeout值,如果超过了则释放相应从节点的连接。 - 从节点:从节点对超时的判断同样是在
复制定时函数中(一个函数估计起码有两个分支)
判断,基本逻辑是:- 如果当前处于连接建立阶段,且距离上次收到
主节点的信息
的时间已超过repl-timeout,则释放与主节点的连接(前面说过,断了会重新连
); - 如果当前处于数据同步阶段,且
收到主节点的RDB文件的时间超时(就是超repl-timeout的时!)
,则停止数据同步,释放连接
;(还是重来
) - 如果当前处于命令传播阶段,且距离上次收到主节点的
PING命令
或数据的时间已超过repl-timeout值,则释放与主节点的连接。(都是释放资源,重新来)
- 如果当前处于连接建立阶段,且距离上次收到
坑!!!
面介绍与复制阶段连接超时有关的一些实际问题:
(1)数据同步阶段:在主从节点进行全量复制bgsave时,主节点需要首先fork子进程将当前数据保存到RDB文件中,然后再将RDB文件通过网络传输到从节点。如果RDB文件过大
,主节点在fork子进程+保存RDB文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时
;此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连……这是个悲伤的循环。为了避免这种情况的发生,除了注意Redis单机数据量不要过大(就是前一篇说过的内存不要太大,工作量不能压到一台服务器上
),另一方面就是适当增大repl-timeout值
,具体的大小可以根据bgsave耗时
来调整。
(2)命令传播阶段:如前所述,在该阶段主节点会向===>
从节点发送PING命令,频率由repl-ping-slave-period控制;该参数应明显小于repl-timeout值(后者至少是前者的几倍)。否则,如果两个参数相等或接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点发送数据,则从节点很容易判断超时。
(3)慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询(如keys *或者对大数据的hgetall等),导致服务器阻塞
;阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。(应该是因为redis是单线程处理
)
6.3 复制中断问题
前一个是超时,这个是中断。不能搞混!超时是复制中断的原因之一
,除此之外,还有其他情况可能导致复制中断,其中最主要的是复制缓冲区溢出问题
复制(不是复制积压)缓冲区溢出
缓冲区存放的数据包括了以下几个时间段内主节点执行的写命令(为什么放这么多呢?宁可多,不能少.少了就全量复制
)
- bgsave生成RDB文件(
过程中,因为fork只能拿到快照的那一瞬间.之后的写命令不放到缓冲区,那就丢失了
) - RDB文件由主节点发往从节点、从节点清空老数据并载入RDB文件中的数据(
总结就是从节点复制阶段
)
当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;这种情况可能引起全量复制->复制缓冲区溢出导致连接中断->重连->全量复制->复制缓冲区溢出导致连接中断……的循环!!!
。
复制缓冲区的大小由client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds}
配置,默认值为client-output-buffer-limit slave 256MB 64MB 60,其含义是:如果buffer大于256MB,或者连续60s大于64MB,则主节点会断开与该从节点的连接。该参数是可以通过config set命令动态配置的(即不重启Redis也可以生效)。
需要注意的是,复制缓冲区是客户端输出缓冲区的一种,主节点会为每一个从节点分别分配复制缓冲区;而复制积压缓冲区则是一个主节点只有一个,无论它有多少个从节点(老是搞混!!!
)
6.4 各场景下复制的选择及优化技巧
1)第一次建立复制
此时全量复制不可避免,但仍有几点需要注意:如果主节点的数据量较大,应该尽量避开流量的高峰期
,避免造成阻塞;如果有多个从节点需要建立对主节点的复制,可以考虑将几个从节点错开
,避免主节点带宽占用过大。此外,如果从节点过多,也可以调整主从复制的拓扑结构,由一主多从结构变为树状结构(中间的节点既是其主节点的从节点,也是其从节点的主节点);但使用树状结构应该谨慎:虽然主节点的直接从节点减少,降低了主节点的负担,但是多层从节点的延迟增大,数据一致性变差;且结构复杂,维护相当困难
2)主节点重启
主节点重启可以分为两种情况
来讨论,一种是故障导致宕机,另一种则是有计划的重启。
-
主节点宕机
主节点宕机重启后,runid会发生变化
,因此不能进行部分复制,只能全量复制。
实际上在主节点宕机的情况下,应进行故障转移处理,将其中的一个从节点升级为主节点,其他从节点从新的主节点进行复制;且故障转移应尽量的自动化,后面文章将要介绍的哨兵便可以进行自动的故障转移。 -
安全重启:debug reload
在一些场景下,可能希望对主节点进行重启,例如主节点内存碎片率过高,或者希望调整一些只能在启动时调整的参数。如果使用普通
的手段重启主节点,会使得runid发生变化
,可能导致不必要的全量复制。
为了解决这个问题,Redis提供了debug reload
的重启方式:重启后,主节点的runid和offset都不受影响,避免了全量复制。
debug reload是一柄双刃剑:它会清空当前内存中的数据,重新从RDB文件中加载,这个过程会导致主节点的阻塞,因此也需要谨慎(这个逃不了的,重启内存中的数据肯定没了
)
3)从节点重启
节点宕机重启后,其保存的主节点的runid会丢失
,因此即使再次执行slaveof,也无法进行部分复制。
4)网络中断
如果主从节点之间出现网络问题,造成短时间内网络中断,可以分为多种情况讨论。
第一种情况:网络问题时间极为短暂,只造成了短暂的丢包,主从节点都没有判定超时(未触发repl-timeout);此时只需要通过REPLCONF ACK来补充丢失的数据即可。
第二种情况:网络问题时间很长,主从节点判断超时(触发了repl-timeout),且丢失的数据过多,超过了复制积压缓冲区所能存储的范围;此时主从节点无法进行部分复制,只能进行全量复制。为了尽可能避免这种情况的发生,应该根据实际情况适当调整复制积压缓冲区的大小;此外及时发现并修复网络中断,也可以减少全量复制。
第三种情况:介于前述两种情况之间,主从节点判断超时,且丢失的数据仍然都在复制积压缓冲区中;此时主从节点可以进行部分复制。
(这三种情况要是认真看了文章,肯定懂
)
5) 复制相关的配置
略!偏运维了,这里就不记录了。