Redis03——RDB 快照

Redis——RDB 内存快照

前言

一篇内容,我们了解了 Redis 避免数据丢失的 AOF 方法。使用 AOF 的好处是,每次执行只需要记录操作命令,需要持久化的数据量不大。通常来说,只要不采用 Always 的持久化策略,都不会对性能造成太大的影响。

但,也正因为记录的是操作命令,而不是实际数据,所以,用 AOF 方法恢复数据的时候,需要把记录的日志信息都逐一执行一遍。如果日志信息过多,那么在恢复的过程中也将会非常耗时,有可能会就影响到正常使用。那么,有没有什么办法既可以保证可靠性,还能在宕机时快速恢复的方法呢?

答案就是——内存快照。按照里面意思来理解就是,指内存中的数据在某一时刻的状态记录。这就类似于照片,当按下快门的时候,就会将那一瞬间给记录下来。

对 Redis 来说,它实现类似于照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件,其中,RDB 就是 Redis DataBase 的缩写。

和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以,在做恢复数据的时候,可以直接把 RDB 文件读入内存,很快就可以完成恢复。但既然内存快照这么好,是不是就可以完全取代 AOF 了呢?肯定不是这样,我们可以尝试从以下两个方面进行考虑:

  • 对哪些数据做快照?
  • 做快照时,数据还能增删改吗?

第一个问题,主要会牵涉到快照的执行效率;后一个则关系到 Redis 是否被阻塞,能否同时正常处理请求。

这么说可能会感觉有点抽象,换个方式理解,还是以拍照的例子来看。

  • 如何取景构图?也就是说,照片中会有哪些元素;
  • 按快门的时候,取景框中的元素有变化,对焦失败怎么办?

下面就针对上面这两个关键问题来进行展开。

数据快照的方式

Redis 的数据都在内容中,为了提供所有数据的可靠性保证,它执行的是全量快照,直接把内存中的所有数据记录到磁盘中,这就类似于给一整个班级的人拍合照,把每一个人都拍进照片里。这样做的好处是,一次性记录了所有数据,一个都不少。

我们知道,当给一个人拍照时,只用注意这一个人就够了,但是,拍多个人合影的时候,就需要协调全部人的位置、状态等等,这也就会更耗时耗力。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。

对于 Redis 而言,它的单线程模式就决定了要尽量避免所有会阻塞主线程的操作。所以,针对任何操作都要考虑:是否会阻塞主线程。这也就牵涉到 RDB 文件的生成是否会降低 Redis 的性能。

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

看见 bgsave 的时候,心里就有底了。我们就可以通过 bgsave 命令来执行全量快照,这既保证了数据的可靠性,又避免了对 Redis 的性能造成影响。

接下来,我们要关注的问题是,在对内存数据做快照时,这些数据还能被修改吗?这个问题非常重要,因为如果数据能被修改,那就意味着 Redis 还能正常处理写操作。否则所有写操作都得等到快照完了才能执行,性能会大打折扣。

快照时数据修改?

还是来看拍照这个例子,在给别人拍照的时候,如果对方在按快门的时候动了,那么这张照片也就拍糊,属于不可用的,就需要重拍。所以,我们当然希望对方保持不动,对于内存快照而言也是同理。

举个例子。我们在时刻 t 给内存做快照,假设内存数据量是 4GB,磁盘的写入带宽是 0.2GB/s,简单来说,至少需要 20s(4/0.2 = 20)才能做完。如果在时刻 t+5s 时,一个还没有被写入磁盘的内存数据 A,被修改成了 A',那么就会破坏快照的完整性,因为 A' 不是时刻 t 时的状态。因此,和拍照类似,我们在做快照时也不希望数据“动”,也就是不能被修改。

但是,如果快照执行期间数据不能被修改,是会有潜在问题的。对于刚刚的例子来说,在做快照的 20s 时间里,如果这 4GB 的数据都不能被修改,Redis 就不能处理对这些数据的写操作,那无疑就会给业务服务造成巨大的影响。

这个时候,你可能会说,不是有 bgsave 避免阻塞的嘛。这就是属于犯了一个经典的错误:避免阻塞和正常处理写操作并不是一回事。此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。

为了快照而暂停写操作,这明显是捡了芝麻丢了西瓜,是不能接受的。所以这个时候,Redis 就会借助操作系统提供的写时复制(Copy-On-Write, COW)技术,在执行快照的同时,正常处理写操作。

简单来说,bgsave 子进程是有主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。

此时,如果主线程对这些数据也都是读操作,比如图中的键值对 A,那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据,像图中的键值对 C,那么这块数据就会被复制一份,生成该数据的副本,也就是键值对 C'。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。

COW

这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

看到这里,我们就解决了对“哪些数据做快照”以及“做快照时数据能否修改”这两大问题:Redis 会使用 bgsave 对当前内存中的所有数据做快照,这操作师子进程在后台完成的,这就允许主线程同时可以修改数据。

快照频率

现在,我们再来看另外一个问题:多久做一次快照?我们在拍照的时候,有时候会使用“连拍”,可以记录人或物连续多个瞬间的状态。那么,快照也适合“连拍”吗?

对于快照来说,所谓“连拍”就是指连续地做快照,这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机,丢失的数据也不会太多。但我们都知道,凡事都有两面性,如果生成快照的频率过快,则会导致性能和存储资源的消耗。所以,快照时间间隔的选取也是很关键的。

如下图所示,先在 T0 时刻做一次快照,然后又在 T0+t 时刻再做一次快照,在这期间,数据块 2 和 6 被修改了。如果在 t 这段时间内,机器宕机了,那么只能按照 T0 时刻的快照进行恢复。但由于数据快 2 和 6 的修改值没有快照记录,就无法恢复了。

Data_Lose

所以,要想尽可能避免数据丢失,t 值就要尽可能小。那么,t 值可以小到什么程度呢?是不是可以每秒做一次快照?毕竟,每次快照都是由 bgsave 子进程在后台执行,也不会阻塞主线程。

但这种想法是错误的。虽然 bgsave 执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销

一方面,频繁将全量数据写入磁盘,会给磁盘带来很大的 IO 压力,多个快照竞争有限的磁盘带宽,前一个快照还没有结束,后面的又来了,容易造成恶性循环。

另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会阻塞主线程,但 fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,就会频繁阻塞主线程了。因为 Redis 中如果有一个 bgsave 在运行,就不会再启动第二个 bgsave 子进程。

那么针对刚才这种情况,有什么比较好的解决办法呢?如果做过数据集成与同步业务的同学,可能已经想到答案了。既然每次全量同步快照的成本开销较大,那么我们可以这样做:做一次全量快照后,之后对于更新修改的数据进行增量快照记录,这样可以避免每次全量快照的开销。

在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,只需要将被修改的数据写入快照文件就行。但这么做的前提是,我们需要弄清楚哪些数据被修改了,这一步骤会使用额外的元数据信息去记录哪些数据被修改了,这也会带来额外的空间开销问题。如下图所示:

Increment_Data

如果每一个键值对的修改动作一个记录,那么,如果有 10w 个被修改的键值对,就需要有 10w 条额外的记录。而且,有的时候,键值对非常小,比如只有 32 字节,而记录它被修改的元数据信息,可能就需要 8 字节,这样一来,为了记录这些修改,引入的额外空间开销就会比较大。这对于非常重视内存资源的 Redis 来说,有些得不偿失。

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

混合使用 AOF 日志和内存快照:简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作。也就是说,不需要记录所有操作,也就不会出现文件过大,可以避免重写带来的开销。

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

AOF_RDB

这个方法既能利用 RDB 文件快速恢复的好处,又能使用到 AOF 只记录操作命令的简单优势。(小孩子才做选择,成年人肯定是“全都要”。)

总结

在本篇博客中,主要讲解了 Redis 使用内存快照避免数据丢失的方法。这个方法的优势在于,可以快速恢复故障数据,也就是只需要把 RDB 文件直接读入内存,这就避免了 AOF 需要顺序、逐一重新执行操作命令带来的低效性能问题。

不过,内存快照也有其局限性。如果每次都是全量同步快照信息,不可避免地会带来大量的资源消耗。虽然 Redis 设计了 bgsave 和写时复制方式,尽可能减少了内存快照对正常读写的影响,但是,频繁进行快照仍然是不太能接受的。而混合使用 AOF 和 RDB,正好可以取两者之长,以较小的性能开销保证数据可靠性和性能。

最后,关于 AOF 和 RDB 的选择可以根据下面三条参考建议:

  • 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
  • 如果允许分钟级别的数据丢失,可以只使用 RDB;
  • 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。