前言
所谓快照,就是记录某一瞬间的内存数据,因此,不同于AOF日志记录的是操作命令,RDB文件的内容是二进制数据。 RDB 就是 Redis DataBase 的缩写。 因此在Redis恢复数据时,采用RDB的效率会更高,直接将RDB文件读入内存即可,不需要像AOF那样还需要额外执行命令来恢复数据
RDB实现原理
Redis提供了两个命令来生成RDB文件,区别在于是否在主进程里执行:
- save:在主进程里生成RDB文件,由于和执行操作命令在同个进程,所以如果写入时间过长,会阻塞主进程
- bgsave:创建一个子进程来生成RDB文件,避免阻塞主进程
Redis并没有提供专门用于加载RDB文件的命令,是在服务器启动时自动执行RDB文件的加载
不过,如果采用的是bgsave命令,可以通过配置来实现命令自动执行,默认配置如下:
save 900 1
save 300 10
save 60 10000
解释:
- 900秒之内,对数据库进行了至少1次修改,就会执行bgsave
- 300秒之内,对数据库进行了至少10次修改,就会执行bgsave
- 60秒之内,对数据库进行了至少10000次修改,就会执行bgsave
性能分析
注意:Redis的快照是全量快照,即每次执行快照时,都是把内存中的所有数据记录到磁盘中。
可以认为执行快照是一个比较重的操作,如果太频繁可能对Redis产生性能影响;但是如果频率太低,发生故障时,丢失的数据会更多。
通常设置至少5分钟才保存一次RDB快照,这时如果Redis发生宕机,最多丢失5分钟数据
这就是RDB快照的缺点,在发生服务器故障时,丢失的数据会比AOF持久化方式更多,因为RDB快照是采用全量快照的方式,因此执行频率不能太频繁,否则影响Redis性能,而AOF日志可以秒级记录操作命令,相对丢失的数据更少。
bgsave写时复制
执行bgsave过程中,Redis 主线程依然可继续处理命令,主进程会通过fork()创建子进程,此时父子进程共享同一片内存数据,页表执行同个物理内存空间。
发生修改内存数据时,物理内存会被复制一份(仅复制被修改的部分,未修改的部分仍共享)
例如,如果主线程(父进程)要修改共享数据里的某一块数据(比如键值对 C)时,发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 C'),然后主线程在这个数据副本(键值对 C')进行修改操作,与此同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入到 RDB 文件。
而这次修改的结果只能等到下一次记录快照才会存入RDB文件中。
写时复制的目的是减少创建子进程的性能损耗,从而加快创建子进程速度,毕竟创建子进程是会阻塞主进程的。
写快照间隔
bgsave过程中,如果主进程修改了共享数据,发生写时复制后,RDB快照保存的是原来的内存数据,而主进程刚修改的数据,只能交由下一次的bgsave命令。
如果恰好系统在RDB快照文件创建完毕后宕机,那么Redis将丢失主进程此前在该写快照期间修改的数据。
因此,要想尽可能保证恢复数据,写快照间隔就要尽可能小,间隔 t 越小,就越像“连拍”。那么,t 值可以小到什么程度呢,比如说是不是可以每秒做一次快照?
虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销:
- 频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
- bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程(需要复制页表),而且主线程的内存越大,阻塞时间越长。所以,如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。
在 Redis 中,如果有一个 bgsave 子进程在运行,就不会再启动第二个 bgsave 子进程。
增量快照可行性分析
为了避免频繁地执行全量快照,我们可以考虑做增量快照。也就是做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样就可以降低开销。
但是,这么做的前提是,我们需要记住哪些数据被修改了,它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。
如果我们对每一个键值对的修改都做个记录,那么,如果有 1 万个被修改的键值对,我们就需要有 1 万条额外的记录。而且,有的时候,键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能就需要 8 字节。为了“记住”修改,引入的额外空间开销比较大。这对于内存资源宝贵的 Redis 来说,有些得不偿失。
那还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法,简称混合持久化。
简单来说,做了第一次全量快照后,后续 bgsave子进程记录内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
这样一来,写RDB快照不用很频繁地执行,这就避免了频繁 fork 子进程对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现AOF文件过大的情况了,也可以避免AOF重写开销。
等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。
内存占用激增
在Redis执行RDB持久化期间,刚fork时,主进程和子进程共享同一物理内存数据,但途中主进程处理了写操作,修改了共享内存,于是当前被修改数据的物理内存就被复制一份。
极端情况下,如果所有的共享内存都被修改了,此时的内存占用将是原来的2倍。
因此,针对写操作多的场景,要留意下RDB快照记录过程的内存变化,防止OOM。
小结
RDB快照跟 AOF日志相比,快照的恢复速度快,但是,快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销
经验总结
- 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
- 如果允许分钟级别的数据丢失,可以只使用 RDB;
- 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。
参考
《小林coding》