参考:

小林coding https://xiaolincoding.com/redis/storage/aof.html#aof-%E9%87%8D%E5%86%99%E6%9C%BA%E5%88%B6

https://www.cnblogs.com/lovezhr/p/15886823.html

关于 linux 的 fork() 子进程:  https://blog.csdn.net/challenglistic/article/details/123781480

 

AOF(Append Only File)

如果 Redis 每执行一条写操作(不会记录读操作命令)命令,就把该命令 以追加的方式写入到一个文件里然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,以此来恢复数据。

AOF 日志文件其实就是普通的文本,我们可以通过 cat 命令查看里面的内容,有着一定的格式

 

不知道大家注意到没有,Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个好处。

  1. 第一个好处,避免额外的检查开销。如果先执行写操作命令再记录日志的话,只有在该命令执行成功后,才将命令记录到 AOF 日志里,这样就不用额外的检查开销,保证记录在 AOF 日志里的命令都是可执行并且正确的。
  2. 第二个好处,不会阻塞当前写操作 命令的执行,因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

当然,AOF 持久化功能也不是没有潜在风险。

  1. 第一个风险,执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了这个数据就会有丢失的风险。
  2. 第二个风险,前面说道,由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能 会给「下一个」命令带来阻塞风险。因为将命令写入到日志的这个操作也是在主进程完成的(执行命令也是在主进程),也就是说这两个操作是同步的。

 

三种写回策略

和 mysql redo log 的 AOF 策略类似,将文件写到硬盘,需要两次系统调用:

    1. redis 在用户态中将命令追加到程序的 server.aof_buf 缓冲区;
    2. 第一次系统调用是 write() 写到操作系统的文件缓存 pageCache
    3. 第二次系统调用是 fsync() 将 pageCache 的文件缓存正式写到硬盘

Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。

    • Always 策略就是每次 write() 写入 AOF 文件数据到 pageCache 后,就立即执行 fsync() 函数;
    • Everysec 策略就会创建一个异步任务来执行 fsync() 函数;
    • No 策略就是永不执行 fsync() 函数,由操作系统自己来控制写回时机;

 

AOF重写机制

为什么需要 AOF 重写?

Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来 压缩 AOF 文件。

手动触发:直接调用 bgrewriteaof 命令

自动触发:根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数,以及 aof_current_size 和 aof_base_size 状态确定触发时机。

参数查看: config get auto-aof-rewrite-min-size

状态查看: info persistence

当AOFRW被触发执行时,Redis首先会 fork一个子进程进行后台重写操作,该操作会将 执行 fork 那一刻Redis的数据快照(读取键值对即 key 最新的 value ,然后用一条 「set key value」命令记录到新的 AOF 文件)全部重写到一个名为temp-rewriteaof-bg-pid.aof 的临时AOF文件中。 (执行 fork 操作时 主进程是阻塞 的,后面的过程发生写时复制时阻塞,其它不阻塞)

由于重写操作为子进程后台执行,主进程在AOF重写期间依然可以正常响应用户命令。因此,为了让子进程最终也能获取 重写期间主进程产生的增量变化主进程 除了会将执行的写命令写入aof_buf,还会写一份到 aof_rewrite_buf 中进行缓存在子进程重写的后期阶段,主进程会将 aof_rewrite_buf 中累积的数据使用 pipe 发送给子进程,子进程会将这些数据 追加到临时AOF文件中

当主进程承接了较大的写入流量时,aof_rewrite_buf中可能会堆积非常多的数据,导致在重写期间子进程无法将aof_rewrite_buf中的数据全部消费完。此时,aof_rewrite_buf 剩余的数据将在重写结束时由主进程进行处理。当子进程完成重写操作并退出后,主进程会在 backgroundRewriteDoneHandler 中处理后续的事情:

    1. 首先,将重写期间 aof_rewrite_buf 中未消费完的数据追加到临时AOF文件中。
    2. 其次,当一切准备就绪时,Redis会使用 rename 操作 将临时AOF文件原子的重命名为server.aof_filename,此时原来的AOF文件会被覆盖至此,整个AOFRW流程结束。

重写失败的话,就直接删除这个文件就好,不会对现有的 AOF 文件造成影响。

关于 linux 的 fork() 子进程

https://blog.csdn.net/challenglistic/article/details/123781480

子进程是怎么拥有主进程一样的数据副本的呢?

主进程在通过 fork 系统调用生成 bgrewriteaof 子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。

    • 物理内存:就是电脑安装的内存条,如果电脑安装了2GB的内存条,那么系统就用于 0 ~ 2GB 的物理内存空间。
    • 虚拟内存虚拟内存是使用软件虚拟的,在 32 位操作系统中,每个进程都独占 4GB 的虚拟内存空间。

应用程序使用的是 虚拟内存比如 C 语言取地址操作符号 & 所得到的地址就是 虚拟内存地址。而 虚拟内存地址 需要映射到 物理内存地址 才能使用,如果使用没有映射的 虚拟内存地址,将会导致 缺页异常

这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,减少子进程复制数据的时间,页表对应的页表项的属性会标记该物理内存的权限为只读

那当主进程想要修改这部分共享物理内存的数据时该怎么办?

如下图,主进程 P fork() 了子进程Q,当主进程P想要修改 page3 的数据,就会在物理内存复制一份 page3 的数据。子进程想要修改数据时也是同样的方式。这个过程被称为「写时复制(Copy On Write)」。

所以说只要子进程不修改数据,那么它持有的就是主进程在  fork() 时候 的数据快照。

如果父进程的内存数据非常大,那自然页表也会很大,这时父进程在通过 fork 创建子进程的时候,阻塞的时间也越久。

所以,有两个阶段会导致阻塞父进程:

    • 创建子进程 fork() 的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
    • 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

为什么用子进程而不用子线程?

如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能

而使用子进程,创建子进程时,父子进程是共享内存数据的,子进程可以持有主进程在 fork() 时候的数据快照,不用加锁。  

 

 

 

RDB 快照(Redis Database Backup

这两种技术都会用各用一个日志文件来记录信息,但是记录的内容是不同的。

  • AOF 文件的内容是操作命令;
  • RDB 文件的内容是二进制数据。记录某一个瞬间的内存数据,记录的是实际数据。

因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;

Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置:

save 900 1
save 300 10
save 60 10000

别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。

只要满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是:

  • 900 秒之内,对数据库进行了至少 1 次修改;
  • 300 秒之内,对数据库进行了至少 10 次修改;
  • 60 秒之内,对数据库进行了至少 10000 次修改。

Redis 的快照是 全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。

所以执行快照是一个比较重的操作,如果 频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。

执行快照时,数据能被修改吗?

执行 bgsave 过程中Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。

那具体如何做到到呢?关键的技术就在于写时复制技术(Copy-On-Write, COW)。

和前面的 AOF 重写时一样,fork() 创建子进程,子进程持有的就是这一刻主进程的数据快照,主进程和子进程对这些数据都是只读,主进程想要修改其中的数据时,需要再复制一份物理内存。

所以说只要子进程不修改数据,那么它持有的就是主进程在  fork() 时候 的数据快照。

如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据。(从 fork 结束到主进程崩溃期间产生的数据)

 

 

AOF 和 RDB 混合

尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:

  • 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;
  • 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。

那有没有什么方法不仅有 RDB 恢复速度快 的优点和,又有 AOF 丢失数据少 的优点呢?

那就是将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。

如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:

aof-use-rdb-preamble yes

混合持久化工作在 AOF 日志重写过程

当开启了混合持久化时,在 AOF 重写日志时的区别是:

  • 单纯的 AOF 重写是, fork() 出来的重写子进程会将 执行 fork 那一刻Redis的数据快照 以命令的格式(读取键值对即 key 最新的 value ,然后用一条 「set key value」命令记录到新的 AOF 文件),全部重写到一个名为temp-rewriteaof-bg-pid.aof 的临时AOF文件中
  • AOF 和混合重写是,fork() 出来的重写子进程会执行 fork 那一刻 Redis 的数据快照 以 RDB 方式 (实际数据)全部重写到一个名为 temp-rewriteaof-bg-pid.aof 的临时AOF文件中。

然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。

 

 

 

 

 

大 Key 对持久化的影响

1、对 AOF 的影响

Always 策略主线程阻塞同步执行  fsync() 函数的,而 Everysec 策略就会创建一个异步任务来执行 fsync() 函数,No 策略就是永不执行 fsync() 函数由操作系统来控制;

当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。

2、对 AOF 重写和 RDB 快照的影响

由于 fork() 子进程进行持久化,有两个地方都会阻塞主进程:

  1. fork() 的时候子进程要复制父进程的页表(指向同一物理内存)
  2. fork() 后主进程改变 fork() 的数据,需要写时复制,会阻塞主进程。

所以对于这两个阻塞的地方,如果是大 key :

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会 拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。

3、对持久化之外的影响

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。

  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。

  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程这样就没办法处理后续的命令。

  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。