AOF持久化
2023-4-2
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

 
Redis还提供了AOF持久化功能,与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化通过保存Redis服务器所执行的写命令来记录数据库状态的:
notion image
 
notion image
  • AOF 文件的内容是操作命令,只会记录写操作命令,读操作命令是不会被记录的,因为没意义。
  • RDB 文件的内容是二进制数据
在Redis恢复数据时, RDB恢复数据的效率会比AOF高些,因为直接将RDB文件读入内存就可以,不需要像AOF那样还需要额外执行操作命令的步骤才能恢复数据。
 
 
在Redis中AOF持久化功能默认是不开启的,需要修改redis.conf 配置文件中的以下参数:
notion image
 
Redis是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做有两个好处:
  1. 避免额外的检查开销。
    1. 因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。
      而如果先执行写操作命令再记录日志的话,只有在该命令执行成功后,才将命令记录到 AOF 日志里,这样就不用额外的检查开销,保证记录在 AOF 日志里的命令都是可执行并且正确的。
  1. 不会阻塞当前写操作命令的执行,因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。
 
当然,AOF 持久化功能也不是没有潜在风险:
  1. 第一个风险,执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险
  1. 第二个风险,由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能会给「下一个」命令带来阻塞风险。因为将命令写入到日志的这个操作也是在主进程完成的(执行命令也是在主进程),也就是说这两个操作是同步的。
    1. notion image
 

AOF持久化的实现

AOF 持久化功能的实现可以分为命令追加(append)、文件写入文件同步(sync)三个步骤。
notion image
 
 
 
  1. Redis执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区
  1. 然后通过write()系统调用,将aof_buf缓冲区的数据写入到 AOF文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区page cache,等待内核将数据写入硬盘
  1. 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定
 

命令追加

AOF持久化功能处于打开状态时, 服务器在执行完一个写命令之后, 会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾:
如果客户端向服务器发送以下命令:
那么服务器在执行这个 SET 命令之后, 会将以下协议内容追加到aof_buf缓冲区的末尾:
*3」表示当前命令有三个部分,每部分都是以「$+数字」开头,后面紧跟着具体的命令、键或值。然后,这里的「数字」表示这部分中的命令、键或值一共有多少字节。例如,「$3 set」表示这部分有 3 个字节,也就是「set」命令这个字符串的长度。
 

写入与同步

Redis 的服务器进程就是一个事件循环(loop), 这个循环中的文件事件负责接收客户端的命令请求, 以及向客户端发送命令回复, 而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
因为服务器在处理文件事件时可能会执行写命令, 使得一些内容被追加到aof_buf缓冲区里面, 所以在服务器每次结束一个事件循环之前, 它都会调用flushAppendOnlyFile函数, 考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,用以下伪代码表示:
flushAppendOnlyFile 函数的行为由服务器配置的appendfsync选项的值来决定, 各个不同值产生的行为。在 redis.conf 配置文件中的appendfsync配置项可以有以下 3 种参数可填,默认值为everysec
notion image
  • Always,意思是「总是」,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘
  • Everysec,意思是「每秒」,每次写操作命令执行完后,先将命令写入到AOF文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘
  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
 
 
 
notion image
 
 
为了提高文件的写入效率, 在现代操作系统中, 当用户调用 write 函数, 将一些数据写入到文件的时候, 操作系统通常会将写入数据暂时保存在一个内存缓冲区里面, 等到缓冲区的空间被填满、或者超过了指定的时限之后, 才真正地将缓冲区中的数据写入到磁盘里面。
这种做法虽然提高了效率, 但也为写入数据带来了安全问题, 因为如果计算机发生停机, 那么保存在内存缓冲区里面的写入数据将会丢失。为此, 系统提供了fsyncfdatasync两个同步函数, 它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面, 从而确保写入数据的安全性。
三种策略只是在控制fsync()函数的调用时机。当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区中,然后排入队列,然后由内核决定何时写入硬盘。
 
 
AOF 持久化的效率和安全性
这 3 种写回策略都无法能完美解决「主进程阻塞」和「减少数据丢失」的问题,因为两个问题是对立的,偏向于一边的话,就会要牺牲另外一边:
  • Always策略可以最大程度保证数据不丢失,但是由于它每执行一条写操作命令就同步将AOF内容写回硬盘,不可避免会影响主进程的性能
  • No策略是交由操作系统来决定何时将AOF日志内容写回硬盘,相比于Always策略性能较好,但是操作系统写回硬盘的时机是不可预知的,如果AOF日志内容没有写回硬盘,一旦服务器宕机,就会丢失不定数量的数据。
  • Everysec策略的话,是折中的一种方式,避免了Always策略的性能开销,也比No策略更能避免数据丢失,当然如果上一秒的写操作命令日志没有写回到硬盘,发生了宕机,这一秒内的数据自然也会丢失。
如果要高性能,就选择No策略、如果要高可靠,就选择 Always 策略、如果允许数据丢失一点,但又想性能高,就选择 Everysec 策略。
notion image
 
 

AOF文件的载入和数据还原

因为AOF文件中存放的是Redis命令,所以Redis恢复数据的时候,只需要将AOF文件中的命令依次执行一遍就可以了。不过在执行AOF中的命令的时候,Redis会创建一个无网络连接的伪客户端,然后使用这个伪客户端发送指令进行执行。
notion image
 

AOF重写

因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着时间的推移,AOF文件会越来越大。不光占用磁盘的空间,而且恢复数据的时候的时间也会相应加长。为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写命令,会对AOF文件进行重写,重写前后都是对数据库当前状态的保存,但是重写后的文件不会包含任何冗余的指令,所以通常重写后的AOF文件体积会减小。

AOF文件重写的实现

虽然Redis将生成新AOF文件替换旧AOF文件的功能命名为”AOF文件重写“,但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。
考虑这样一个情况,如果发服务器对list键执行了以下命令:
那么服务器为了保存当前list 键的状态,必须在AOF文件中写入六条命令。但如果服务器想要用尽可能少的命令来记录list键的状态,那么最简单高效的办法不是去读取和分析现有AOF文件的内容,而是直接从数据库中读取键list的值,然后用一条 rpush list “C” “D” “E” “F” “G”命令来代替保存在AOF文件中的六条命令。
Redis客户端有缓冲区设置,为了避免生成命令的时候造成缓冲区溢出,所以如果键对应的集合元素超过了一定的数量(作者版本的数量是64),命令生成的时候会进行拆分,每次添加规定数量的值到集合中。

AOF后台重写

写入 AOF 日志的操作虽然是在主进程完成的,因为它写入的内容不多,所以一般不太影响命令的操作。
但是在触发 AOF 重写时,比如当 AOF 文件大于 64M 时,就会对 AOF 文件进行重写,这时是需要读取所有缓存的键值对数据,并为每个键值对生成一条命令,然后将其写入到新的 AOF 文件,重写完后,就把现在的 AOF 文件替换掉。
这个过程其实是很耗时的,所以重写的操作不能放在主进程里。所以,Redis 的重写 AOF 过程是由后台子进程bgrewriteaof来完成的,这么做可以达到两个好处:
  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
  • 子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。使用子进程而不是线程,避免使用锁的情况下保证数据安全。
 
 
子进程是怎么拥有主进程一样的数据副本的呢?
主进程在通过fork系统调用生成bgrewriteaof子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
notion image
notion image
这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读
不过,当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制(Copy On Write)」。
写时复制:在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。
 
操作系统复制父进程页表的时候,父进程也是阻塞中的,不过页表的大小相比实际的物理内存小很多,所以通常复制页表的过程是比较快的。不过,如果父进程的内存数据非常大,那自然页表也会很大,这时父进程在通过fork创建子进程的时候,阻塞的时间也越久。
所以,有两个阶段会导致阻塞父进程:
  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长
  • 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长
触发重写机制后,主进程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
但是子进程重写过程中,主进程依然可以正常处理命令。
如果此时主进程修改了已经存在key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的
所以如果这个阶段修改的是一个bigkey,也就是数据量比较大的key-value的时候,这时复制的物理内存数据的过程就会比较耗时,有阻塞主进程的风险。
 
 
AOF 重写缓冲区
子进程在AOF重写期间,父进程服务器对数据库状态进行修改,会使服务器当前状态与重写后AOF状态不一致:
                                     新的AOF文件只保存了k1一个键的数据,而服务器数据库现在有k1、k2、k3、k4四个键
新的AOF文件只保存了k1一个键的数据,而服务器数据库现在有k1、k2、k3、k4四个键
为了解决这种数据不一致问题,Redis服务器设置AOF重写缓冲区这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区:
notion image
notion image
以保证:
  • AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有 AOF 文件的处理工作如常进行
  • 从创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓冲区里
 
 
子进程完成AOF重写工作后,向父进程发送一个信号,父进程接到信号后调用信号处理函数,执行以下工作:
  • AOF重写缓冲区中的所有内容写入到新AOF文件,此时新AOF文件保存的数据库状态将与服务器当前的数据库状态一致;
  • 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换;
这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。在整个AOF后台重写过程中,只有号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。
 
 

混合使用 AOF 日志和内存快照

尽管RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:
  • 如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失
  • 如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销
那有没有什么方法不仅有 RDB 恢复速度快的优点和,又有 AOF 丢失数据少的优点呢?
当然有,那就是将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。
如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:
混合持久化工作在 AOF 日志重写过程
当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
notion image
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失
 
  • Redis
  • RDB持久化事件和调度
    目录