Redis - 持久化

2021/10/02 Redis 共 5322 字,约 16 分钟
Bob.Zhu

在运行情况下, Redis 以数据结构的形式将数据维持在内存中, 为了让这些数据在 Redis 重启之后仍然可用,Redis 分别提供了 RDB 和 AOF 两种持久化方法:

  • RDB(Redis 数据库):RDB 持久性以指定的时间间隔执行数据集的时间点快照。
  • AOF(Append Only File):AOF 持久化记录服务器收到的每个写操作,在服务器启动时会再次播放,重建原始数据集。命令使用与 Redis 协议本身相同的格式以仅附加的方式记录。当日志变得太大时,Redis 能够在后台重写日志。

RDB

在 Redis 运行时, RDB 程序将当前内存中的数据库快照以 二进制 的方式保存到磁盘文件中, 在 Redis 重启动时,RDB 程序可以通过载入 RDB 文件来还原数据库的状态。

Redis-持久化-RDB

RDB 功能最核心的是 rdbSave 和 rdbLoad 两个函数, 前者用于生成 RDB 文件到磁盘, 而后者则用于将 RDB 文件中的数据重新载入到内存中。

SAVE 和 BGSAVE 两个命令都会调用 rdbSave 函数,但它们调用的方式各有不同:

  • SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。
  • BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。因为 rdbSave 在子进程被调用,所以 Redis 服务器在 BGSAVE 执行期间仍然可以继续处理客户端的请求。

Redis-持久化-RDB-流程

由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程 可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。 那么如何保证数据一致性呢?

RDB中的核心思路是Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在 内存中不会发生变化。在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门来做这个事情, 这样保证了Redis服务不会停止对客户端包括写请求在内的任何响应。另一方面这段时间发生的数据变化会以 副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。

举个例子:如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份, 生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中, 主线程仍然可以直接修改原来的数据。

RDB优势

  • RDB 是 Redis 数据的非常紧凑的单文件时间点表示。RDB 文件非常适合备份。例如,您可能希望在最近 24 小时内每小时存档一次 RDB 文件,并在 30 天内每天保存一个 RDB 快照。这使您可以在发生灾难时轻松恢复不同版本的数据集。
  • RDB 非常适合灾难恢复,它是一个可以传输到远程数据中心或 Amazon S3(可能已加密)的紧凑文件。
  • RDB 最大限度地提高了 Redis 的性能,因为 Redis 父进程为了持久化需要做的唯一工作是派生一个将完成所有其余工作的子进程。父实例永远不会执行磁盘 I/O 或类似操作。
  • 与 AOF 相比,RDB 允许更快地重新启动大数据集。
  • 在副本上,RDB 支持重启和故障转移后的部分重新同步。

总的来说,就是 文件紧凑、子线程效率高、重新加载快

RDB 的缺点

  • 如果您需要在 Redis 停止工作(例如断电后)时将数据丢失的可能性降至最低,那么 RDB 并不好。您可以在生成 RDB 的地方配置不同的保存点(例如,在对数据集进行至少 5 分钟和 100 次写入之后,但您可以有多个保存点)。但是,您通常会每五分钟或更长时间创建一个 RDB 快照,因此如果 Redis 因任何原因没有正确关闭而停止工作,您应该准备好丢失最近几分钟的数据。
  • RDB 经常需要 fork() 以便使用子进程在磁盘上持久化。如果数据集很大,Fork() 可能会很耗时,如果数据集很大且 CPU 性能不是很好,则可能导致 Redis 停止为客户端服务几毫秒甚至一秒钟。AOF 也需要 fork() 但你可以调整你想要重写日志的频率,而不会对持久性进行任何权衡。

AOF

RDB 将数据库的快照(snapshot)以二进制的方式保存到磁盘中。 AOF 则以协议文本的方式, 将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件,以此达到记录数据库状态的目的。

Redis-持久化-AOF

AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。

  • 命令追加:当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
  • 文件写入和同步:关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:

Redis-持久化-AOF-写入策略

  • Always:同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
  • Everysec:每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  • No:操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

Redis-持久化-AOF-流程

AOF优势

  • 使用 AOF Redis 更持久:你可以有不同的 fsync 策略:根本没有 fsync,每秒 fsync,每次查询 fsync。使用 fsync 每秒写入性能的默认策略仍然很棒(fsync 是使用后台线程执行的,当没有 fsync 正在进行时,主线程将努力执行写入。)但您只能丢失一秒钟的写入。
  • AOF 日志是仅附加日志,因此在断电时不会出现寻道或损坏问题。即使日志由于某种原因(磁盘已满或其他原因)以半写命令结束,redis-check-aof 工具也能够轻松修复它。
  • 当 AOF 变得太大时,Redis 能够在后台自动重写。重写是完全安全的,因为当 Redis 继续追加到旧文件时,会使用创建当前数据集所需的最少操作集生成一个全新的文件,一旦第二个文件准备就绪,Redis 就会切换这两个文件并开始追加到新的那一个。
  • AOF 以易于理解和解析的格式包含所有操作的日志。您甚至可以轻松导出 AOF 文件。例如,即使您不小心使用FLUSHALL命令刷新了所有内容,只要在此期间没有重写日志,您仍然可以通过停止服务器、删除最新命令并重新启动 Redis 来保存您的数据集再次。

AOF的缺点

  • AOF 文件通常比相同数据集的等效 RDB 文件大。
  • AOF 可能比 RDB 慢,具体取决于确切的 fsync 策略。一般来说,将 fsync 设置为每秒性能仍然非常高,并且禁用 fsync 后,即使在高负载下,它也应该与 RDB 一样快。即使在写入负载巨大的情况下,RDB 仍然能够提供更多关于最大延迟的保证。
  • 过去我们在特定命令中遇到过罕见的错误(例如有一个涉及阻塞命令的错误,如BRPOPLPUSH) 导致生成的 AOF 在重新加载时无法重现完全相同的数据集。这些错误很少见,我们在测试套件中进行了测试,自动创建随机复杂数据集并重新加载它们以检查一切正常。但是,使用 RDB 持久性几乎不可能出现此类错误。为了更清楚地说明这一点:Redis AOF 通过增量更新现有状态来工作,就像 MySQL 或 MongoDB 那样,而 RDB 快照一次又一次地从头开始创建所有内容,这在概念上更加健壮。但是- 1)需要注意的是,每次Redis重写AOF时,它都是从数据集中包含的实际数据开始从头开始重新创建,与始终附加的 AOF 文件(或重写读取旧 AOF 而不是读取内存中的数据)相比,对错误的抵抗力更强。2) 我们从未收到过用户关于在现实世界中检测到的 AOF 损坏的单一报告。

AOF 重写

有些被频繁操作的键, 对它们所调用的命令可能有成百上千、甚至上万条, 如果这样被频繁操作的键有很多的话, AOF 文件的体积就会急速膨胀, 对 Redis 、甚至整个系统的造成影响。虽然操作过程有很多步骤,但是 我们需要的只是最后一次操作的结果,那么读取当前Redis中的值,作为重现的值,删除其他所有的操作记录, 就可以大大减少AOF文件大小。

作为一种辅佐性的维护手段, Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到(后台)子进程里执行, 这样处理的最大好处是:

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
  • 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。

不过, 使用子进程也有一个问题需要解决: 因为子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对现有的数据进行修改, 这会让当前数据库的数据和重写后的 AOF 文件中的数据不一致。

为了解决这个问题, Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用, Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外, 还会追加到这个缓存中: Redis-持久化-AOF-重写

深入理解AOF

AOF重写会阻塞吗?

AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程, fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后, bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。

所以aof在重写时,在fork进程时是会阻塞住主线程的。

AOF日志何时会重写?

有两个配置项控制AOF重写的触发: auto-aof-rewrite-min-size:表示运行AOF重写时文件的最小大小,默认为64MB。 auto-aof-rewrite-percentage:这个值的计算方式是,当前aof文件大小和上一次重写后aof文件大小的差值, 再除以上一次重写后aof文件大小。也就是当前aof文件比上一次重写后aof文件的增量大小,和上一次重写后aof文件大小的比值。

重写日志时,有新数据写入咋整?

重写过程总结为:“一个拷贝,两处日志”。在fork出子进程时的拷贝,以及在重写时,如果有新数据写入, 主线程就会将命令记录到两个aof日志内存缓冲区中。如果AOF写回策略配置的是always, 则直接将命令写回旧的日志文件,并且保存一份命令至AOF重写缓冲区,这些操作对新的日志文件是不存在影响的。 (旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof进程使用的日志文件)

而在bgrewriteaof子进程完成会日志文件的重写操作后,会提示主线程已经完成重写操作, 主线程会将AOF重写缓冲中的命令追加到新的日志文件后面。这时候在高并发的情况下, AOF重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过Linux管道技术让aof重写期间就能同时进行回放, 这样aof重写结束后只需回放少量剩余的数据即可。

最后通过修改文件名的方式,保证文件切换的原子性。

在AOF重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。

主线程fork出子进程的是如何复制内存数据的?

fork采用操作系统提供的写时复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成阻塞。 fork子进程时,子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表), 而不会拷贝物理内存。这个拷贝会消耗大量cpu资源,并且拷贝完成前会阻塞主线程,阻塞时间取决于内存中的数据量, 数据量越大,则内存页表越大。拷贝完成后,父子进程使用相同的内存地址空间。

但主进程是可以有数据写入的,这时候就会拷贝物理内存中的数据。如下图(进程1看做是主进程,进程2看做是子进程): Redis-持久化-AOF-COPY-ON-WRITE

在主进程有数据写入时,而这个数据刚好在页c中,操作系统会创建这个页面的副本(页c的副本), 即拷贝当前页的物理数据,将其映射到主进程中,而子进程还是使用原来的的页c。

在重写日志整个过程时,主线程有哪些地方会被阻塞?

  • fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。
  • 主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。
  • 子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞。

为什么AOF重写不复用原AOF日志?

两方面原因:

  • 父子进程写同一个文件会产生竞争问题,影响父进程的性能。
  • 如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。

RDB和AOF混合方式(4.0版本)

Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行, 在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录 两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了, 也可以避免重写开销。

参考资料

文档信息

Search

    Table of Contents