跳至主要內容

AOF 日志

AruNi_Lu数据库Redis约 2768 字大约 9 分钟

本文内容

前言

我们都知道,Redis 是 内存数据库,而内存有个很明显的缺陷,就是 数据断电即失,所以一旦 Redis 服务器宕机,其中的数据将全部丢失。

虽然我们通常只是用 Redis 来做一层缓存,真正的数据还是保存到具有持久化机制的后端数据库中的,但 如果从后端数据库中恢复缓存中的所有数据,会给数据库带来很大的压力,而且速度也慢,这会使得客户端响应速度变慢

而且对于一些对数据没有那么敏感的程序,把数据只保存到 Redis 中也不是没有,因此 Redis 的持久化尤为重要。

Redis 持久化机制主要有两个,分别是 AOF 日志和 RDB 快照。本文就先来介绍第一个 AOF 日志。

1. 什么是 AOF 日志

AOF(Append Only File)日志 是一种 写后日志,即 先执行命令,把数据写入内存,然后再记录日志

可以发现,AOF 刚好和 MySQL 中的 rego log 相反,redo log 是写前日志(Write Ahead Log),把修改的数据记录到日志文件后,修改操作就算完成了,就算期间出现了故障,也能进行恢复,即 crash safe 能力

Redis 开启 AOF 日志

Redis 默认不开启 AOF 日志,修改 redis.conf 以开启:

appendonly		yes
appendfilename	"appendonly.aof"

那 AOF 为什么要选择先执行命令再记录日志呢?

AOF 中记录的是收到的每一条命令,即逻辑日志。比如 Redis 收到一条 set testkey testvalue 命令,所记录的 AOF 内容如下:

img

其中:*3 表示这条命令有三个部分。每个部分是以 $+数字 开头,后面紧跟具体的命令、键或值。数字 表示这部分的命令、键或值一共有多少字节。

Redis 记录 AOF 日志时,不会先对这些命令进行语法检查,这样能 减少这一额外开销。而先记录日志,再执行命令,如果命令有错误,那日志中就记录了错误的命令。

写后日志就可以先让系统执行命令,只有执行成功的命令,才会被记录到日志中,否则就直接给客户端报错了。而 MySQL 有专门的解析器,可以事先对语法进行分析。

写后日志还有一个好处就是 不会阻塞当前的写操作,因为它是在命令执行后才记录日志。

不过这也随之带来了两个潜在的风险:

  • 执行完命令,还未来得及记录日志就宕机,那么就会 丢失数据
  • AOF 虽然不会阻塞当前的写操作,但是可能会 阻塞下一个操作,因为写 AOF 日志也是由主线程完成的。

可以发现,这两个风险都与命令执行完后,AOF 日志写入磁盘的时机 有关,Redis 也提供了不同的配置参数来决定。

2. AOF 写回磁盘的时机

Redis 提供了三种方案,在配置文件中通过 appendfsync 的值来选择:

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

注意:AOF 文件的内存缓冲区,只是内核的缓冲区 Page Cache,还并没有写入磁盘。

不过,针对 减少数据丢失和避免主线程阻塞 问题,这三个策略都无法做到两全其美,原因如下:

  • Always 可以做到数据基本不丢失,但每次命令执行完后,都要先有个慢速的刷盘操作,所以不可避免阻塞主线程;
  • No 可以做到写完缓冲区后就继续执行后续的命令,但刷盘时机已经由操作系统决定了,如果此时操作系统宕机,那数据也就丢失了;
  • Everysec 相当于做了个折中,避免了立马刷盘的主线程阻塞,但可能会丢失 1 秒内的数据。

其实这三种策略只是调用操作系统 fsync() 函数的时机不同。一般在向文件写入数据时,内核会先将数据放在内核缓冲区,至于何时刷盘,由内核自己决定。如果想要手动控制刷盘时机,就可以调用 fsync() 函数。

总结如下:

img

所以,想要获得高性能,就选择 No 策略;想要得到高可用,就选择 Always 策略;允许丢失 1 秒的数据,又不希望性能受到太大影响,就选择 Everysec 策略。

3. AOF 文件太大怎么办

因为 AOF 日志是采用的 追加写 的方式 记录所有的写命令,随着服务器执行的命令越来越多,AOF 文件就会越来越大

AOF 文件太大后,会有三个方面的问题:

  • 文件系统本身对文件的大小就有限制,无法保存太大的文件;
  • 文件太大,之后再追加写命令效率也会变低,因为可能涉及到磁盘的寻道时间更长;
  • 需要使用 AOF 文件恢复数据时,文件太大会导致恢复时间过长

为了避免上述问题,Redis 在文件到达一定大小时,就会 将 AOF 日志进行重写

可通过 auto-aof-rewrite-min-sizeauto-aof-rewrite-percentage 配置 AOF 重写的触发时机。

AOF 重写机制会 根据 Redis 数据库现状创建一个 AOF 文件,即读取数据库中的所有键值对,然后对每个键值对用 一条命令 记录它的写入,之后替换掉旧的 AOF 文件即可。

可以发现,AOF 重写具有 压缩命令记录 的效果,因为它会 把每个键值对都只用一条命令来记录。具体来说,就是原本这一个键值对可能经历过了多次修改,AOF 文件由于是追加写的方式,所以其中记录了多次修改,重写能把这多次修改只保留最新的那次,删掉了很多历史命令。如下所示:

img

所以当 AOF 重写完成后,将旧的大的 AOF 文件替换掉,就起到了压缩文件的效果。

至于为什么不复用原来的 AOF 文件,而是替换,因为 写同一个文件会产生竞态,而且如果 重写失败,将会污染原来的 AOF 文件

但是,AOF 重写是需要读取数据库中所有最新的数据的,这是一个非常耗时的过程,这个过程肯定不能让主线程阻塞的,否则就影响到 Redis 的正常使用了。

因此,AOF 重写其实是由后台子进程来完成的

4. AOF 后台重写

AOF 日志虽然是由主线程写回的,但重写耗时还是比较长的,为了避免阻塞,重写过程是由后台子进程 bgrewriteaof 完成的

下面来看看 AOF 重写的过程:

  • 主线程 fork 出后台子进程,fork 采用的是操作系统提供的 写时复制(Copy On Write)机制,只用 拷贝内存页表 而不用拷贝所有的内存数据,这样也能达到共享内存的效果(子进程与父进程指向相同的内存地址空间)。此时子进程就可以在不影响主线程的情况下,把数据写到新的 AOF 文件中了;

  • 在 AOF 重写过程中,主线程还是可以继续处理新请求的。如果有写操作,Redis 会将新的写操作写入两个日志缓冲区中,如下图所示:

    • 原来的 AOF 缓冲区
    • AOF 重写缓冲区

    img

    这样才能保证原来的日志不会丢失最新数据(若 AOF 重写失败,还是需要原来的 AOF 文件,所以也要保持最新状态),AOF 重写的日志也不会丢失最新数据。

    注意:如果写操作一个 已经存在的 key,为了防止影响子进程的重写,此时主线程会发生该 key 真正的内存拷贝,从此时开始 主线程和子进程拥有各自独立的内存空间,主线程在新的内存中修改,子进程还是读取原来的内存。

    这也是为什么要 使用子进程而不是子线程的原因,多线程之间虽然可以直接共享内存,但在修改共享内存时,需要通过加锁等机制来保证数据的读写安全,会带来额外的开销,影响性能;而使用子进程和写时复制的方式,既可以实现内存的共享,也能避免额外的消耗。

  • 重写完成后,AOF 重写缓冲区中的数据也会写入到新的 AOF 文件,保证重写过程中的新操作不会丢失。然后替换掉原来旧的 AOF 文件即可。

在上面的过程中,可以发现还有两个 阻塞主线程 的地方:

  • 主线程 fork 子进程,fork 这一瞬间会阻塞主线程,数据越多、页表越大,阻塞越久;

  • 重写过程中发生了写时复制,即主线程修改了已存在的 key,这时会导致主线程重新拷贝该 key 的内存。

    操作系统的内存分配是以页为单位的,默认 4K,所以 如果操作的是一个 bigkey,重新申请内存的耗时就会更长,主线程阻塞时间也就越长。

    还有,如果操作系统开启了内存大页机制(Huge Page,页面大小为 2M),也会导致申请内存的耗时更长,所以在 Redis 实例上最好关闭 Huge Page 机制。

    Huge Page 主要是用于减少页表的内存消耗和 TLB(虚拟-物理内存映射关系的缓存)的失效情况,因为页面越大,所需要的映射数量就可以越少。

5. 参考文章

  • 《Redis 核心技术与实战》
上次编辑于: