Redis的主从复制是如何作的?复制过程当中也会产生各类问题?

若是Redis的读写请求量很大,那么单个实例颇有可能承担不了这么大的请求量,如何提升Redis的性能呢?你也许已经想到了,能够部署多个副本节点,业务采用读写分离的方式,把读请求分担到多个副本节点上,提升访问性能。要实现读写分离,就必须部署多个副本,每一个副本须要实时同步主节点的数据。redis

Redis也提供了完善的主从复制机制,使用很是简单的命令,就能够构建一个多副本节点的集群。数据库

同时,当主节点故障宕机时,咱们能够把一个副本节点提高为主节点,提升Redis的可用性。可见,对于故障恢复,也依赖Redis的主从复制,它们都是Redis高可用的一部分。安全

这篇文章咱们就来介绍一下Redis主从复制流程和原理,以及在复制过程当中有可能产生的各类问题。服务器

构建主从复制集群

假设咱们如今有一个节点A,它通过写入一段时间的数据写入后,内存中保存了一些数据。网络

此时咱们再部署一个节点B,须要让节点B成为节点A的数据副本,而且以后与节点A保持实时同步,如何作呢?运维

Redis提供了很是简单的命令:slaveof。咱们只须要在节点B上执行如下命令,就可让节点B成为节点A的数据副本:异步

slaveof 节点A_host:节点A_port

节点B就会自动与节点A创建数据同步,若是节点A的数据量不大,等待片刻,就能看到节点B拥有与节点A相同的数据,同时在节点A上产生的数据变动,也会实时同步到节点B上。ide

经过这样简单的方式,咱们能够很是方便地构建一个master-slave集群,业务能够在master上进行写入,在slave上读取数据,实现读写分离,提升访问性能。性能

那么主从节点的复制是如何进行的?下面咱们就来分析其中的原理。优化

主从复制流程

为了方便下面讲解,咱们这里把节点A叫作master节点,节点B叫作slave节点。

当咱们在slave上执行slaveof命令时,这个复制流程会通过如下阶段:

  • slave发送psync $runid $offset给master,请求同步数据
  • master检查slave发来的runidoffset参数,决定是发送全量数据仍是部分数据
  • 若是slave是第一次与master同步,或者master-slave断开复制过久,则进行全量同步
    • master在后台生成RDB快照文件,经过网络发给slave
    • slave接收到RDB文件后,清空本身本地数据库
    • slave加载RDB数据到内存中
  • 若是master-slave以前已经创建过数据同步,只是由于某些缘由断开了复制,此时只同步部分数据
    • master根据slave发来的数据位置offset,只发送这个位置以后的数据给slave
    • slave接收这些差别数据,更新本身的数据,与maser保持一致
  • 以后master产生的写入,都会传播一份给slave,slave与master保持实时同步

下面分别介绍全量同步和部分同步的详细流程。

全量同步

当咱们在节点B上执行slaveof命令后,节点B会与节点A创建一个TCP链接,而后发送psync $runid $offset命令,告知节点A须要开始同步数据。

这两个参数的具体含义以下:

  • runid:master节点的惟一标识
  • offset:slave须要从哪一个位置开始同步数据

什么是runid?在启动Redis实例时,Redis会为每一个实例随机分配一个长度为40位的十六进制字符串,用来标识实例的惟一性,也就是说,runid就是这个实例的惟一标识。

因为是第一次同步,slave并不知道master的runid,因此slave会r发送psync ? -1,表示须要全量同步数据。

master在收到slave发来的psync后,会给slave回复+fullsync $runid $offset,这个runid就是master的惟一标识,slave会记录这个runid,用于后续断线重连同步请求。

以后master会在后台生成一个RDB快照文件。

RDB文件生成以后,master把这个RDB文件经过网络发送给slave,slave收到RDB文件后,清空整个实例,而后加载这个RDB数据到内存中,此时slave拥有了与master接近一致的数据。

Redis的主从复制是如何作的?复制过程当中也会产生各类问题?

为何是接近一致?由于master在生成RDB和slave加载RDB的过程是比较耗时的,在这个过程当中,master产生新的写入,这些新写入的命令目前在slave上是没有执行的。这些命令master如何与slave保持一致呢?

Redis会把这些增量数据写入到一个叫作复制缓冲区(repl_baklog)的地方暂存下来,这个复制缓冲区是一个固定大小的队列,由配置参数repl-backlog-size决定,默认1MB,能够经过配置文件修改它的大小。

因为是固定大小的队列,因此若是这个缓冲区被写满,那么它以前的内容会被覆盖掉。

注意:不管slave有多少个,master的复制缓冲区只有一份,它实际上就是暂存master最近写入的命令,供多个slave部分同步时使用。

Redis的主从复制是如何作的?复制过程当中也会产生各类问题?

待slave加载RDB文件完成以后,master会把复制缓冲区的这些增量数据发送给slave,slave依次执行这些命令,就能保证与master拥有相同的数据。

以后master再收到的写命令,会实时传播给slave节点,slave与master执行一样的命令,这样slave就能够与master保持实时数据的同步。

部分同步

若是在复制过程当中,由于网络抖动或其余缘由,致使主从链接断开,等故障恢复时,slave是否须要从新同步master的数据呢?

在Redis的2.8版本以前,确实是这么干的,每次主从断开复制,从新链接后,就会触发一次全量数据的同步。

可见,这么作的代价是很是大的,并且耗时耗力。后来在Redis在这方面进行了改进,在2.8版本以后,Redis支持部分同步数据

当主从断开从新创建链接后,slave向master发送同步请求:psync $runid $offset,由于以前slave在第一次全量同步时,已经记录下了master的runid,而且slave也知道目前本身复制到了哪一个位置offset

这时slave就会告知master,以前已经同步过数据了,此次只须要把offset这个位置以后的数据发送过来就能够了。

master收到psync命令以后,检查slave发来的runid与自身的runid一致,说明以前已经同步过数据,此次只须要同步部分数据便可。

可是slave须要的offset以后的数据,master还保存着吗?

前面咱们介绍了master自身会有一个复制缓冲区(repl-backlog),这个缓冲区暂存了最近写入的命令,同时记录了这些命令的offset位置。此时master就会根据slave发来的这个offset在复制缓冲区中查询是否还保留着这个位置以后的数据。

若是有,那么master给slave回复+continue,表示此次只同步部分数据。以后master把复制缓冲区offset以后的数据给slave便可,slave执行这些命令后就与master达到一致。

Redis的主从复制是如何作的?复制过程当中也会产生各类问题?

若是master复制缓冲区找不到offset以后的数据,说明断开的时间过久,复制缓冲区的内容已经被新的内容覆盖了,此时master只能触发全量数据同步。

命令传播

slave通过全量同步或部分同步后,以后master实时产生的写入,是如何实时同步的?

很简单,master每次执行完新的写入命令后,也会把这个命令实时地传播给slave,slave执行与master相同的操做,就能够实时与master保持一致。

须要注意的是,master传播给slave的命令是异步执行的,也就是说在master上写入后,立刻在slave上查询是有可能查不到的,由于异步执行存在必定的延迟。

slave与master创建链接后,slave就属于master的一个client,master会为每一个client分配一个client output buffer,master和每一个client通讯都会先把数据写入到这个内存buffer中,再经过网络发送给这个client。

可是,因为这个buffer是占用Redis实例内存的,因此不能无限大。因此Redis提供了控制buffer大小的参数限制:

# 普通client buffer限制 
client-output-buffer-limit normal 0 0 0
# slave client buffer限制
client-output-buffer-limit slave 256mb 64mb 60
# pubsub client buffer限制
client-output-buffer-limit pubsub 32mb 8mb 60

这个参数的格式为:client-output-buffer-limit $type $hard_limit $soft_limit $soft_seconds,其含义为:若是client的buffer大小达到了hard_limit或在达到了soft_limit并持续了soft_seconds时间,那么Redis会强制断开与client的链接。

对于slave的client,默认的限制是,若是buffer达到了256MB,或者达到64MB并持续了1分钟,那么master就会强制断开slave的链接。

这个配置的大小在某些场景下,也会影响到主从的数据同步,咱们下面会具体介绍到。

心跳机制

在命令传播阶段,为了保证master-slave数据同步的稳定进行,Redis还设计了一些机制维护这个复制链路,这种机制主要经过心跳来完成,主要包括两方面:

  • master定时向slave发送ping,检查slave是否正常
  • slave定时向master发送replconf ack $offset,告知master本身复制的位置

在master这一侧,master向slave发送ping的频率由repl-ping-slave-period参数控制,默认10秒,它的主要做用是让slave节点进行超时判断,若是slave在规定时间内没有收到master的心跳,slave会自动释放与master的链接,这个时间由repl-timeout决定,默认60秒。

一样,在slave这边,它也会定时向master发送replconf ack $offset命令,频率为每1秒一次,其中offset是slave当前复制到的数据偏移量,这么作的主要做用以下:

  • 让master检测slave的状态:若是master超过repl-timeout时间未收到slave的replconf ack $offset命令,则master主动断开与slave的链接
  • master检测slave丢失的命令:master根据slave发送的offset并与本身对比,若是发现slave发生了数据丢失,master会从新发送丢失的数据,前提是master的复制缓冲区中还保留这些数据,不然会触发全量同步
  • 数据安全性保障:Redis提供了min-slaves-to-writemin-slaves-max-lag参数,用于保障master在不安全的状况下禁止写入,min-slaves-to-write表示至少存在N个slave节点,min-slaves-max-lag表示slave延迟必须小于这个时间,那么master才会接收写命令,不然master认为slave节点太少或延迟过大,这种状况下是数据不安全的,实现这个机制就依赖slave定时发送replconf ack $offset让master知晓slave的状况,通常状况下,咱们不会开启这个配置,了解一下就好

可见,master和slave节点经过心跳机制共同维护它们之间数据同步的稳定性,并在同步过程当中发生问题时能够及时自动恢复。

咱们能够能够在master上执行info命令查看当前全部slave的同步状况:

role:master         # redis的角色
connected_slaves:1  # slave节点数
slave0:ip=127.0.0.1,port=6480,state=online,offset=22475,lag=0   # slave信息、slave复制到的偏移位置、距离上一次slave发送心跳的时间间隔(秒)
master_repl_offset:22475    # master当前的偏移量
repl_backlog_active:1       # master有可用的复制缓冲区
repl_backlog_size:1048576   # master复制缓冲区大小

经过这些信息,咱们能看到slave与master的数据同步状况,例如延迟了多大的数据,slave多久没有发送心跳给master,以及master的复制缓冲区大小。

复制过程当中的问题

在整个数据复制的过程当中,故障是时有发生的,例如网络延迟过大、网络故障、机器故障等。

因此在复制过程当中,有一些状况须要咱们格外注意,必要时须要针对性进行参数配置的调整,不然同步过程当中会发生不少意外问题。

主要问题分为如下几个方面,下面依次来介绍。

主从断开复制后,从新复制触发了全量同步?

上面咱们有提到,主从创建同步时,优先检测是否能够尝试只同步部分数据,这种状况就是针对于以前已经创建好了复制链路,只是由于故障致使临时断开,故障恢复后从新创建同步时,为了不全量同步的资源消耗,Redis会优先尝试部分数据同步,若是条件不符合,才会触发全量同步。

这个判断依据就是在master上维护的复制缓冲区大小,若是这个缓冲区配置的太小,颇有可能在主从断开复制的这段时间内,master产生的写入致使复制缓冲区的数据被覆盖,从新创建同步时的slave须要同步的offset位置在master的缓冲区中找不到,那么此时就会触发全量同步。

如何避免这种状况?解决方案就是适当调大复制缓冲区repl-backlog-size的大小,这个缓冲区的大小默认为1MB,若是实例写入量比较大,能够针对性调大此配置。

但这个配置不能调的无限大,由于它会额外占用内存空间。若是主从断开复制的时间过长,那么触发全量复制在所不免的,咱们须要保证主从节点的网络质量,避免频繁断开复制的状况发生。

master写入量很大,主从断开复制?

主从通过全量同步和部分同步后,以后master产生了写入命令,会实时传播给slave节点,若是在这个过程当中发生了复制断开,那么必定是在这个过程当中产生了问题。咱们来分析这个过程是如何处理命令传播的。

上面咱们也提到了,主从创建同步链路后,因为slave也是master的一个client,master会对每一个client维护一个client output buffer,master产生写命令执行完成后,会把这个命令写入到这个buffer中,而后等待Redis的网络循环事件把buffer中数据经过Socket发送给slave,发送成功后,master释放buffer中的内存。

若是master在写入量很是大的状况下,可能存在如下状况会致使master的client output buffer内存持续增加:

  • 主从节点机器存在必定网络延迟(例如机器网卡负载比较高),master没法及时的把数据发送给slave
  • slave因为一些缘由没法及时处理master发来的命令(例如开启了AOF并实时刷盘,磁盘IO负载高)

当遇到上面状况时,master的client output buffer持续增加,直到触发默认配置的阈值限制client-output-buffer-limit slave 256mb 64mb 60,那么master则会把这个slave链接强制断开,这就会致使复制中断。

以后slave从新发送复制请求,可是以上缘由可能依旧存在,通过一段时间后又产生上述问题,主从链接再次被断开,周而复始,主从频繁断开连接,没法正常复制数据

解决方案是,适当调大client-output-buffer-limit的阈值,而且解决slave写入慢的状况,保证master发给slave的数据能够很快得处理完成,这样才能避免频繁断开复制的问题。

添加slave节点,master发生阻塞?

当主从创建同步进行全量同步数据时,master会fork出一个子进程,扫描全量数据写入到RDB文件中。

这个fork操做,并非没有代价的。fork在建立子进程时,须要父进程拷贝一分内存页表给子进程,若是master占用的内存过大,那么fork时须要拷贝的内存页表也会比较耗时,在完成fork以前,Redis整个进程都会阻塞住,没法处理任何的请求,因此业务会发现Redis忽然变慢了,甚至发生超时的状况。

咱们能够执行info能够看到latest_fork_usec参数,单位微妙。这就是最后一次fork的耗时时间,咱们能够根据这个时间来评估fork时间是否符合预期。

对于这种状况,能够优化方案以下:

  • 必定保证机器拥有足够的CPU资源和内存资源
  • 单个Redis实例内存不要太大,大实例拆分红小实例

经过以上方式避免fork引起的父进程长时间阻塞问题。

主从全量同步数据时很慢?

以前咱们已经了解到,主从全量复制会通过3个阶段:

  • master生成RDB文件
  • master把RDB文件发送给slave
  • slave清空数据库,加载RDB文件到内存

若是发现全量同步数据很是耗时,咱们根据以上阶段来分析缘由:

  • master实例数据比较大,而且机器的CPU负载较高时,在生成RDB时耗大量CPU资源,致使RDB生成很慢
  • master和slave的机器网络带宽被打满,致使master发送给slave的RDB文件网络传输时变慢
  • slave机器内存不够用,但开启了swap机制,致使内存不足以加载RDB文件,数据被写入到磁盘上,致使数据加载变慢

经过以上状况能够看出,主从复制时,会消耗CPU、内存、网卡带宽各方面的资源,咱们须要合理规划服务器资源,保证资源的充足。而且针对大实例进行拆分,能避免不少复制中的问题。

总结

这篇文章咱们介绍了Redis主从复制的流程和工做原理,以及在复制过程当中可能引起的问题。

虽然搭建一个复制集群很简单,但其中涉及到的细节也不少。Redis在复制过程也可能存在各类问题,咱们须要设置合适的配置参数和合理运维Redis,才能保证Redis有稳定可用的副本数据,为咱们的高可用提供基础。

相关文章
相关标签/搜索