Redis 持久化之 AOF
2024-03-28 10:32:53 # Technical # Redis

由于 RDB 不适合实时持久化存在丢失数据的风险,所以 Redis 还提供了 AOF 持久化的方式来解决

与 RDB 不同,AOF 是换了一个角度来实现持久化,那就是将 Redis 执行过的所有写指令记录下来,在下次 Redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了

AOF 是「事后」日志,Redis 先执行命令,把数据写入内存,然后才会记录日志。日志里记录的是 Redis 收到的每一条命令,这些命令是以文本的形式保存的

ps:大多数的数据库是采用的「事前」日志,如:MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性

先写与后写

Redis 后写 AOF 日志带来了好处,但是也带来了风险

好处

  • 避免额外的检查开销:后写日志可以保证命令都是正确的,不用额外的去检查命令的正确与否
  • 不阻塞命令的执行:命令先于日志执行,当前命令的执行不会被日志阻塞

风险

  • 丢失风险:执行命令和记录日志是两个过程,如果 Redis 命令执行成功,但是当记录日志时发生了宕机,这时数据就会丢失
  • 阻塞命令的风险:由于写操作执行成功后才会记录到 AOF 日志,所以不会阻塞当前命令,但是会有阻塞下一条命令的风险

开启 AOF

在 Redis 中 AOF 持久化功能默认是不开启的,需要修改 redis.conf 配置文件中的以下参数

AOF 相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# AOF 开关
appendonly yes
# 指定更新日志文件名,默认为 appendonly.aof
appendfilename appendonly.aof

# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的
dir ./

# 同步策略
# appendfsync always
appendfsync everysec
# appendfsync no

# aof重写期间是否同步
no-appendfsync-on-rewrite no

# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 加载aof出错如何处理
aof-load-truncated yes

# 文件重写策略
aof-rewrite-incremental-fsync yes

appendfsync:这个参数项是 AOF 功能最重要的设置项之一,主要用于设置「真正执行」操作命令向 AOF 文件中同步的策略

真正执行:为了保证操作系统中 I/O 队列的操作效率,应用程序提交的 I/O 操作请求一般是被放置在「linux Page Cache」中的,然后再由 Linux 操作系统中的策略自行决定正在写到磁盘上的时机。而 Redis 中有一个 fsync() 函数,可以将 Page Cache 中待写的数据真正写入到物理设备上,而缺点是频繁调用这个 fsync() 函数干预操作系统的既定策略,可能导致 I/O 卡顿的现象频繁

no-appendfsync-on-rewritealwayseverysec 的设置会使系统频繁 I/O,会出现频繁卡顿的现象,为了尽量缓解这种情况,Redis 提供这个选项来保证在执行 fsync() 时,不会将这段时间内发生的操作放入 Page Cache

auto-aof-rewrite-percentage:如果当前 AOF 文件的大小超过了上次重写后 AOF 文件的百分之多少后,就再次开始重写 AOF 文件。例如该参数值的默认设置值为 100,意思就是如果 AOF 文件的大小超过上次 AOF 文件重写后的 1 倍,就启动重写操作

auto-aof-rewrite-min-size:启动 AOF 文件重写操作的最小大小,如果 AOF 文件大小低于这个值,则不会触发重写操作

在生产环境下,技术人员不可能随时随地使用 BGREWRITEAOF 命令去重写 AOF 文件,更多时候需要依靠 Redis 中对 AOF 文件的自动重写策略。所以 Redis 中对触发自动重写 AOF 文件的操作提供了上面两个设置

开启后,可以查看 AOF 文件内容

1
2
3
4
5
6
7
*3
$3
set
$5
hello
$5
world
  • 「*3」:表示有 3 个部分
  • 「$3」:表示这个部分有 3 个字节

AOF 的写回策略

AOF 的执行过程分为:命令追加(append)、文件写入(write)、文件同步(sync)

  • 命令追加:当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区
  • 文件写入和同步:关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略
    • Always:同步写回,每个写命令完成,立马同步地将日志写回磁盘。优点:可靠性高数据基本不丢失;缺点:每个写命令都要写回磁盘,性能影响较大
    • Everysec:每秒写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘。优点:性能适中;缺点:宕机时丢失 1 秒内的数据
    • No:操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。优点:性能好;缺点:宕机时丢失数据较多

AOF 的重写机制

AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小会越来越大

如果当 AOF 日志文件过大就会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢

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

AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件

这里使用新文件覆盖的方式重写 AOF 文件,是为了避免重写的过程发生未知异常,导致重写失败,如果直接使用原文件,会导致 AOF 文件被污染,无法用于恢复

重写工作完成后,就会将新的 AOF 文件覆盖现有的 AOF 文件,这就相当于压缩了 AOF 文件,使得 AOF 文件体积变小了

不同于 RDB 的 LZF 算法压缩,AOF 重写主要通过精简操作达到压缩 AOF 文件的目的。比如:A - B - A,原本 AOF 会记录 3 次操作,但重写后只有一次 set 操作

AOF 后台重写

写入 AOF 日志时,因为内容不多,所以放在主进程执行

AOF 重写会对所以的数据进行操作,整个过程是很耗时的,所以需要放到子进程中完成,这样在子进程重写 AOF 的时候,主进程任然可以正常处理命令

这里后台重写 AOF 通过 bgrewriteaof 命令完成

后台重写所带来的问题与解决方式与 RDB 类似

虽然子进程重写可以解决阻塞主进程的问题,但也会出现主进程在子进程重写期间发生新的写操作,这时会出现两个进程操作同一个资源进行操作的情况

简单化处理可以加锁,但这样会大大降低性能,所以这里采用了和 RDB 相同的方式——Copy-On-Write

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

操作系统复制主进程页表的时候,主进程也是阻塞的,只不过页表的大小比物理内存要小很多。如若内存数据非常大,那么自然页表数据也会随之很大,然后主进程阻塞的时间也会随之变久

bgrewriteaof fork

fork 操作是操作系统提供的系统调用,用于创建一个新的进程,通常由操作系统内核实现和管理。在 Unix 和类 Unix 操作系统中,fork 是一个标准的系统调用,用于创建一个与父进程相同的子进程,其中子进程继承了父进程的内存映像、文件句柄、CPU 寄存器和其他资源。这种子进程的复制是一种快速的进程创建方式

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

当父进程或者子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作

这样可以防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致主进程长时间阻塞的问题

Copy-On-Write

fork 出来的子进程和主进程共享物理内存,但子进程是只读的,子进程会读取所有数据,然后逐一把内存数据的键值对转换为一条命令,然后记录到新的 AOF 文件中

AOF 重写时修改数据

在子进程重写 AOF 的过程中,主进程是仍然可以正常处理命令的

如果此时主进程修改了已存在的 k-v,那就会发生 Copy-On-Write,但这里只会复制主进程修改的物理内存数据,没有修改的数据的物理内存还是与子进程共享的

如果这里修改的是一个 bigkey,这时复制物理内存就会比较耗时,从而有阻塞主进程的风险

要是这个 k-v 是新的,此时主进程和子进程的内存数据就可能不一致了

为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用

在重写 AOF 期间,当主进程执行完一个命令后,它会同时将这个命令写入到「AOF 缓冲区」和「AOF 重写缓冲区」

AOF 缓冲区 用于缓冲将要写入 AOF 文件的命令的内存区域,在 Redis 接收到写命令时,这些命令首先会被追加到 AOF 缓冲区中,然后按回写策略写入磁盘。不管 AOF 的回写策略是否为 Always,Redis 都会使用 AOF 缓冲区来临时存储写命令,只是 Always 策略会立即将缓冲区内容写入磁盘

可通过 aof-rewrite-buffer-size 配置 AOF 缓存区的大小

bgrewriteaof Copy-On-Write

上图中的主线程是 Redis 的操作线程,是一个单独线程,fork 出来的也是只有一个线程的 bgrewriteaof 进程。这里主要是说明重写进程并不会阻塞主进程

从上可知,在 bgrewriteaof 执行 AOF 重写的过程中,主进程会执行三个任务

  • 执行客户端的操作命令
  • 将执行后的写命令追加到「AOF 缓冲区」
  • 将执行后的写命令追加到「AOF 重写缓冲区」

当子进程完成 AOF 重写工作(扫描完数据库中所有数据,逐一把内存数据的键值对转换为一条命令,再将目录记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的

主进程收到信号后,调用信号处理函数

  • 将 AOF 重写缓存区中所有的内容追加到新的 AOF 文件中,使新旧 AOF 文件中的数据一致
  • 将新的 AOF 文件 rename,并覆盖旧文件

信号处理函数执行完后,主进程就继续正常处理命令

所以,在 AOF 重写的过程中,除了 fork 会阻塞主进程外,这里的信号函数处理过程也会阻塞主进程

AOF 重写过程中产生的阻塞

  • fork 子进程时,由于要复制主进程的页表,阻塞时间与页表的大小有关,页表越大阻塞时间越久
  • fork 完成后,子进程读取数据期间,主进程修改共享数据,此时发生 Copy-On-Write,拷贝物理内存,内存越大,阻塞时间越久
  • 子进程重写完毕,主进程追加 AOF 重写缓冲区时会阻塞主进程

AOF 何时重写

自动触发(两个条件同时满足):

  • auto-aof-rewrite-min-size:最小 AOF 文件大小,默认为 64MB

  • auto-aof-rewrite-percentage:当前 AOF 文件超过上次 AOF 文件的百分比后才进行持久化操作,默认为 100

手动触发:

  • bgrewriteaof

PS:回写策略与缓冲区

前面有提及,当 AOF 的回写策略为 Always 时,Redis 会将命令立即写入磁盘,那这时是否还会将命令写入缓冲区呢?

这里分别问了 ChatGPT 和 Claude,但给出了不同的答案,这一点暂时存疑

Chat GPT

Claude

Thanks