aof 数据持久化

aof 介绍

在redis运行期间,不断将修改命令写入到文件中,实现持久化(注意,只有写命令,才会持久化到文件中,读命令没必要写入到文件中)。在redis重启后,只需要将这些写命令再执行一次,就可以恢复数据。

触发时机

在 redis.conf 配置文件中设置:

# 开启AOF
appendonly yes
 
# 设置AOF文件的写入策略
# appendfsync always # 每个命令都同步
# appendfsync everysec # 每秒同步
appendfsync no # 不同步
 
# AOF文件的名字
appendfilename "appendonly.aof"

当开启 aof 写入功能之后,每条写命令都会追加到一个缓冲区中,然后 redis 在执行完写命令后,(beforeSleep() 函数)将缓冲区数据刷到磁盘中。

具体代码看 server.c 文件,beforeSleep 函数:

void beforeSleep(struct aeEventLoop *eventLoop) {
    ...
    if (server.aof_state == AOF_ON || server.aof_state == AOF_WAIT_REWRITE)
        flushAppendOnlyFile(0);
    ...
}
  • feedAppendOnlyFile() 函数中,将命令写到 aof buffer缓冲区中。
  • flushAppendOnlyFile() 函数将aof buffer缓冲区数据写入到磁盘中。

aof 文件格式

aof 文件是以文本格式存储的,以 resp 协议格式存储每一条命令的命令名,以及所有参数。

命令重写

有些写命令是会返回结果给客户端的,但我们追加命令到 aof 文件,做数据恢复是不需要知道写结果给哪个客户端的。所以会将某些写命令重新改成功能相同,但名字不同的命令,写入到 aof 文件中。比如,spop 命令,写入 aof 的是 srem,而非 spop 命令,这就是实现了更新 aof 写入命令的特性。

同步磁盘策略

aof 写数据到磁盘,主要是通过 write 调用实现的,把数据直接写入到系统内核态,然后再调用 fync 接口将内核态数据刷新到磁盘上。

同步磁盘策划,主要是通过 redis.conf  的 appendfsync 控制的,有三种选项:

  • no 策略:不会主动执行 fsync() 刷盘,而是依赖操作系统自动刷盘的机制。使用该策略的风险是服务宕机时,部分 aof 日志还存储在系统缓冲区中,而未真正落盘,宕机可能会导致 aof 日志丢失部分数据;好处就是少了 fsync() 函数调用,减少 IO 开销,写入 aof 文件的效率高,但丢失数据的风险也相对高些。

  • always 策略:每次 write 调用之后,都会调用 fdatasync() 函数,将数据从内核缓冲区刷到磁盘。好处是保证数据尽可能快的刷新到磁盘,保证数据的一致性,但坏处也很明显,频繁 IO 调用,会降低 redis 性能。
  • everysec 策略:看代码,是每秒刷一次盘,并且使用后台线程 调用 fdatasync() 函数刷盘。好处是如果 aof 文件很大,主线程刷盘的话,会造成卡顿,所以,采用后台线程刷盘,不会影响到主线程处理其他命令请求,这也是 redis 做为 nosql 高性能的原因之一。

异常情况处理

如果内核态缓冲区满了,write 失败,everysec 策略是会稍后定时重试,并且设置 aof_last_write_status 状态为 C_ERR,这样,客户端接下来请求的写命令,都不会被处理了。同样,如果后台刷盘失败,会设置 aof_bio_fsync_status 状态为 C_ERR,也同样是会拒绝客户端请求。如果是 always 策略,无论是写入失败,还是主线程 async 刷新数据到磁盘失败,都是 exit 退出程序。

 aof rewrite 机制

aof 功能开启后,随着命令修改的增多,aof文件也会越来越大,有些命令可能是对同一个 key 进行多次修改,这些多次修改记录也会被记录到 aof 文件中。如果我们只把最后一次 key 修改记录存到 aof 文件中,那么 aof 文件大小可以小很多,在启服时加载 aof 文件,恢复数据也能提升不少效率。redis 会定期对 aof 文件进行压缩瘦身,这一操作被称为aof rewrite,其核心原理是将 aof 文件中无效的命令删除,只保留有效的命令

redis7 之后的版本,会对 aof 文件拆分成多个,每个 aof 文件有不同的类型,不同的类型有不同的职责。

  • Base AOF 文件:redis fork一个子进程,将内存中所有数据转成 命令+参数 的形式存到文件中(当然,也支持转成 rdb 格式)。
  • Incr AOF 文件:redis 主进程在 fork 子进程前开始创建,后续客户端的写命令,都直接追加到这个新创建的增量文件中。
  • History AOF 文件:记录历史版本的 Base AOF 和 Incr AOF 文件。每当 rewrite 之后,之前的 Base,Incr AOF 文件都将变为 History 类型。在之后,Redis 会自动删除 History 类型的 AOF 文件。
  • manifest 文件:为了方便统一管理 Base AOF、Incr AOF 以及 History AOF 文件,还需要有一个 manifest 文件来记录这些文件信息。

 

aof rewrite 触发时机

  • 手动触发,执行 BGREWRITEAOF 命令
  • 通过 redis.conf 配置的 auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage 字段来控制,如果aof 文件大小 以及 aof 文件增长比例(默认是文件大小增加了2倍)满足了条件,就会触发 aof rewrite 机制

上面提到的 aof 文件大小,是指 Base AOF + Incr AOF 两个文件总大小。如果当前已经有一个后台子进程了,那么会判断这个子进程是在处理什么类型的任务,如果是处理 aof rewrite 的,那么就会直接退出,不需要重复执行,如果是正在生成 rdb 文件的,那么会加一个标记,等 rdb 生成之后,再去执行 aof rewrite。如下图所示:

异常情况分析

aof rewrite 机制是主进程先创建一个新的 incr aof 文件,然后才去 fork 一个子进程生成 base aof 文件的,假设此时,base aof 文件生成失败,redis 写入的命令又是追加到新的 incr aof 文件,这样会有什么问题吗。

答案是不会,因为新的 incr aof 信息会记录到 manifest 文件中,此时,incr aof 文件会有多个。在宕机重启后,我们会先读取 manifest 文件清单。先加载旧的 base aof 文件,进行一次全量数据的恢复,然后再根据 incr aof 列表,将多个 incr aof 文件数据加载到内存中,实现增量部分数据恢复。此外,为了防止连续多次 aof rewrite 失败,导致 incr aof 文件过多,redis 会延长触发 aof write 时间,会按照 1、4、8、16、32、60 (单位分钟)这个时间间隔进行延迟,最长延迟上限就是 60 分钟(这个判断代码写在 aofRewriteLimited() 函数中)。

aof rewrite 流程图:

子进程写 base aof 文件流程图:

主进程最后在 backgroundRewriteDoneHandler() 做最后的收尾工作。首先,backgroundRewriteDoneHandler() 在修改 redisServer.aof_manifest 字段之前,先深拷贝一份临时的 aofManifest 实例出来,接下来的修改都是针对这个临时的 aofManifest 实例进行的,修改完成之后,直接替换到 aof_manifest 字段上。这样做的好处就是方便回滚,我们只要不更新 redisServer.aof_manifest 字段,在临时 aofManifest 实例上的修改,对 Redis 来说,都是不可见的。

主进程将子进程 rewrite 产生的 temp-rewriteaof-bg-{子进程pid}.aof 临时文件重命名为正式的文件名格式{appendfilename}.{seq}.base.{rdb/aof},生成正式的 Base AOF 文件名。接下来就是修改 aof 状态等操作。

history 状态的 aof文件好处:

通过修改 redis.conf 配置 aof-disable-auto-gc 选项,可以有机会保留历史状态的 base aof,incr aof 文件,做备份用。

 

posted @ 2023-05-07 22:50  墨色山水  阅读(38)  评论(0)    收藏  举报