redis:持久化之RDB内存快照

RBD

为什么需要持久化

  • 持久化功能能有效的避免因为进程退出造成的数据丢失问题
  • 当下次重启的时候利用之前持久化的文件即可实现数据恢复

什么叫做RDB

RDB持久化是把当前进程数据生成快照保存到硬盘的过程

  • 在redis执行【写】指令过程中,内存数据会一直变化。所谓的内存快照,指的就是redis内存中的数据在某一刻的状态数据
  • 好比时间定格在某一刻,当我们拍照时,通过照片就能把某一刻的瞬间画面完全记录下来
  • redis跟这个类似,就是把某一刻的数据以文件的形式拍下来,写到磁盘上。这个快照文件叫做RDB文件,RDB 就是 Redis DataBase 的缩写。
    在这里插入图片描述
  • 在做数据恢复时,直接将RDB文件读入文件完成恢复1

触发机制

触发RDB持久化过程分为手动触发和自动触发
在这里插入图片描述

手动触发

手动触发有两种方式: save和bgsave

save命令:
  • save是一个同步操作,当客户端向服务器发送save命令请求进行持久化时,服务器会阻塞save命令之后的其他客户端的请求,直到数据同步完成。
  • 如果数据量太大,同步数据会执行很久,而这期间Redis服务器也无法接收其他请求,所以,不建议使用
  • 命令:
# 同步数据到磁盘上
> save 
  • 运行save命令对应的日志如下:
* DB saved on disk

在这里插入图片描述

bgsave命令:
  • 对于bgsave,redis在持久化时,调用glibc函数fork产生一个子进程,快照持久化,完全交给子进程处理,父进程继续处理客户端请求。
  • 子进程做数据持久化,不会修改现有的内存数据结构,只是对数据结构进行遍历读取,序列化,写入到磁盘中。
  • 相比save,bgsave不会影响父进程执行处理客户端请求,但是产生子进程,增加服务器内存的开销。产生fork进程,会造成在复制父进程中,存在秒级的不可用
  • 命令
# 异步保存数据集到磁盘上
> bgsave
  • 运行bgsave命令的日志如下
* Background saving started by pid 3151
* DB saved on disk
* RDB: 0 MB of memory used by copy-on-write
* Background saving terminated with success

很明显,bgsave就是为了解决save的阻塞问题而出现的,所以涉及到RDB持久化的,Redis内部都是使用bgsave的方式。
在这里插入图片描述

自动触发

除去手动触发之外,还有自动触发RDB持久化的方式,例如以下的场景:

  • 在配置文件中使用了save的相关配置,比如save m n。表示m秒内数据集存在n次修改,自动触发bgsave
  • 如果节点执行全量复制操作,主节点自动执行gbsave生成RDB文件并发送给从节点
  • 执行debug reload命令重新加载redis时,也会自动触发save操作
  • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave
    例如我们可以在配置文件redis.conf指定如下的选项:
# 900s内至少达到一条写命令
save 900 1
# 300s内至少达至10条写命令
save 300 10
# 60s内至少达到10000条写命令
save 60 10000

之后在启动服务器时加载配置文件。

# 启动服务器加载配置文件
redis-server redis.conf

这种通过服务器配置文件触发RDB的方式,与bgsave命令类似,达到触发条件时,会forks一个子进程进行数据同步,不过最好不要通过这方式来触发RDB持久化,因为设置触发的时间太短,则容易频繁写入rdb文件,影响服务器性能,时间设置太长则会造成数据丢失。

原理

问题提出

我们知道redis是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写,这时候有两个问题:

  • 在服务线上请求的同时,redis还需要进行内存快照,内存快照要求redis必须进行文件IO操作,可而文件IO操作会严重拖累服务器的性能
  • 还有个重要的问题。为了不阻止线上的业务,redis就需要一边持久化,一边响应客户端的请求。持久化的同时,内存数据结构还在改变,比如一个大型的hash字典正在持久化,结果一个请求过来把它给删掉了,开始正持久化到一半呢。

针对上面问题难点,redis使用操作系统的多进程COW(Copy On Write)机制来实现快照持久化。

流程说明

bgsave是主流的RDB持久化方式,下面是它运作的流程:
在这里插入图片描述

  • 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
  • 父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
  • 父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
  • 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。
  • 子进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的rdb_*相关选项。

写时复制

redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化完全交给这个子进程来处理,父进程继续处理客户端请求。

子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这时你可以把父子进程想象成一个连体婴儿,它们在共享身体。这是Linux操作系统的机制。 为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。
在这里插入图片描述

  • 子进程做数据持久化,不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中
  • 但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改

这个时候就会使用操作系统的COW机制来进行数据段页面的分离,如下图:

  • 数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改
  • 这时子进程相应的页面是没有变化的,还是进程产生的那一瞬间的数据

随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长,但是也不会超过原有数据内存的两倍大小

另外,redis实例里冷数据占的比例也是比较高的,所以很少会出现所有的页面都被分离的情况。被分离的往往只有一部分页面。每个页面的大小只有4KB,一个Redis实例里面一般都有成千上万的页面
在这里插入图片描述
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么redis的持久化叫做“快照”的原因。

接下来子进程就可以非常安心的遍历数据,进行序列化写磁盘了

RDB文件的处理

在这里插入图片描述
前面介绍了三种让服务器生成rdb文件的方式,无论是由主进程生成还是子进程来生成,其过程如下:

  • 生成临时rdb文件,并写入数据。
  • 完成数据写入,用临时文代替代正式rdb文件。
  • 删除原来的db文件。

RDB默认生成的文件名为dump.rdb,当然,我可以通过配置文件进行更加详细配置,比如在单机下启动多个redis服务器进程时,可以通过端口号配置不同的rdb名称,如下所示:

# 是否压缩rdb文件
rdbcompression yes
# rdb文件的名称
dbfilename redis-6379.rdb
# rdb文件保存目录
dir ~/redis/

保存

  • RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定
  • 可以通过执行config set dir {newDir} 和 config set dbfilename {newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。
  • 当遇到坏盘或磁盘写满等情况时,可以通过config set dir{newDir}在线 修改文件路径到可用的磁盘路径,之后执行bgsave进行磁盘切换,同样适用 于AOF持久化文件

压缩

  • Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的 文件远远小于内存大小,默认开启,可以通过参数config set rdbcompression{yes|no}动态修改
  • 虽然压缩RDB会消耗CPU,但可大幅降低文件的体积,方便保存到硬盘 或通过网络发送给从节点,因此线上建议开启。

校验

  • 如果Redis加载损坏的RDB文件时拒绝启动,并打印如下日志:
# Short read or OOM loading DB. Unrecoverable error, aborting now.
  • 这时可以使用Redis提供的redis-check-dump工具检测RDB文件并获取对应的错误报告。

优缺点

优点

  • RDB文件小,非常适合定时备份,用于灾难恢复
  • 因为RDB文件中直接存储的是内存数据,而AOF文件中存储的是一条条命令,需要应用命令。Redis加载RDB文件的速度比AFO快很多。

缺点

  • RDB持久化方式不能做到实时/秒级持久化。
    • 实时持久化要全量刷内存到磁盘,成本太高。
    • 每秒fork子进程也会阻塞主进程,影响性能。
  • 使用 RDB 方式实现持久化,一旦 Redis 异常退出,就会丢失最后一次快照以后更改的所有数据。
    • 这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受范围。
    • 如果数据相对来说比较重要,希望将损失降到最小,则可以使用 AOF 方式进行持久化。
  • RDB文件是二进制文件,随着Redis不断迭代有多个rdb文件的版本,不支持跨版本兼容。老的Redis无法识别新的RDB文件格式。

针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。


问题

做快照时是否阻塞

  • save:在主线程中执行,必定会阻塞
  • bgsave:创建子进程,用于写入RDB文件,避免了主线程的阻塞,这也是默认配置

RDB给哪些数据做快照呢?

RDB执行的是全量快照,也就睡把内存中的所有数据都记录在磁盘中
在这里插入图片描述

  • 给内存做全量快照时,RDB文件越大,往磁盘写数据的时间开销也就会越大
  • 每次快照持久化都是将内存数据完整写入到磁盘一次,并不 是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘io操作,可能会严重影响性能。

AOF方式仅对数据的变化进行存储,类似于日志文件。AOF日志文件存储的是redis服务器的顺序指令序列,AOF日志只记录内存改变的指令。
在这里插入图片描述

做快照时能否修改数据

Redis 使用操作系统的多进程写时复制技术 COW(Copy On Write)

  • Redis 在持久化时会调用 glibc 的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。
  • 当主线程执行写指令修改数据的时候,这个数据就会复制一份副本, bgsave 子进程读取这个副本数据写到 RDB 文件。
    在这里插入图片描述
  • 注意,在RDB的过程中,读操作父进程和子进程互不影响,比如说下图中父进程与子进程共同读取数据块A。但是如果父进程需要修改某一块的数据时就会触发COW,比如说下图中的客户端如果发送了一条写操作的命令(set c)给Redis父进程,C就会被复制一份供子进程使用。
    在这里插入图片描述
  • 这样就保证了Redis在做快照是可以进行写操作,也避免了对业务的影响。

总结: save不能,bgsave能

多久做一次快照

先来了解下RDB做快照时,应该多久做一次,拍照都有连拍,Redis可以嘛?如果Redis也有向连拍一样的机制,那RDB的间隙不就是很短了(间隙是指上一次RDB和下一次RDB文件之间的时刻)。

如下图所示的那样,在时刻1做了一次快照,然后时刻1+N又做了一次快照,中间修改了A、B。如果在此之间也就是时刻1后,Redis宕机了,那么重启服务只能以时刻1来恢复数据了,A、B就丢失了。
在这里插入图片描述
要想数据丢失的少,就要像相机连拍一样,让RDB每秒执行一次,那么RDB可以每秒执行一次持久化吗?

当然不能,因为这是存在一定风险的,就算执行bgsave让子线程去执行也是不行的,如果频繁的执行全量快照也有一定的开销

  • 频繁快照会给磁盘带来很大的压力,而且每秒一次,前一个没做完后一个就开始了,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环
  • 另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越
    大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了

为此,我们引入了增量快照

增量快照

所谓增量快照,就是指,就是做了一次全量快照以后,后面的记录只对修改的数据进行快照,大大的减少了开销

  • 在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。但是,这么做的前提是,我们需要记住哪些数据被修改了。也就是需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。如下图所示:
    在这里插入图片描述
  • 如果我们对每一个键值对的修改,都做个记录,那么,如果有 1 万个被修改的键值对,我们就需要有 1 万条额外的记录。而且,有的时候,键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能就需要 8 字节,这样的画,为了“记住”修改,引入的额外空间开销比较大。这对于内存资源宝贵的 Redis 来说,有些得不偿失。

可以看出,虽然跟 AOF 相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销,那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?

为此,redis在4.0的时候提出了混合使用AOF日志和内存快照的方法

AOF与RDB的混合模式

简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作

  • 这样快照就不用很频繁的进行,避免了频繁fork和磁盘开销。
  • 而且AOF日志文件也只需要记录两次快照之间的操作,也就不需要每一个命令都要记录了,也就是避免了重写操作
  • 结合后AOF的数据回复慢也被解决,RDB快照丢失数据问题也被解决,混合体,利用了RDB的快,跟AOF的追加和全量,将老的数据RDB到老的AOF文件中,再有新的操作的时候增量指令的方式Append到AOF

如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
在这里插入图片描述

这样两种持久化模式结合,从两个模式中取长补短,来弥补各方的缺点,简直不要太舒服。

在Redis.conf配置文件中修改此参数aof-use-rdb-preamble yes即可打开混和模式,默认开启。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值