AOF持久化

AOF 日志

存储写命令的流程.png

Redis 日志中只需要存储写命令,不需要存储读命令,因为读命令不会对数据进行修改,没有存储价值。

Redis 存储写命令的时候,会优先写入内存中,接下来才会写入磁盘。内存中的数据访问快,但是如果机器关机,里面存储的数据立马清空;磁盘中的数据访问就比内存慢许多,但存储的数据是持久化的,不会因为计算机重启导致数据丢失。

Redis 并非直接就把用户发送的写命令直接存储起来,而是要先执行用户的指令,如果出现错误将不会存储。这样提前检查命令的可行性,能够保证存储的写命令必然是正确可行,后续无需承担检查命令是否可行的成本。再有一个好处是不会阻塞当前写命令的执行,毕竟只有成功执行写命令之后才有可能写入日志,先后顺序保证着不会阻塞当前写命令的执行。

先执行命令,再写入日志的这两个操作是在主线程中同步执行,只有这两个动作全部完成,才可以继续下一个命令执行和写入日志的操作。

同步执行.png

那么先写入内存,再写入磁盘的这种简单机制肯定是存在问题的:

  1. 如果写入内存之后,还没有来得及写入磁盘,机器宕机,那就意味着刚刚写入内存的指令会丢失。
  2. 写操作成功执行才写入 AOF 日志,不会阻塞主线程,但是下一个命令的执行被阻塞了,因为执行命令和写入日志是同步执行。

如果在将日志内容写入到磁盘时,服务器的硬盘的I/O压力太大,就会导致写磁盘的速度很慢,进而阻塞住了,也就会导致后续的命令无法执行。

你还发现,问题就卡在【写入磁盘】这个位置,这个写入的时机很重要,下面谈一谈三种写回策略。

三种写回策略

三种写回策略,对应两个极端,一个折中,太经典的思考方式了。没有谁对谁错,各有各自的应用场景。如果你熟悉 Linux文件操作中 如何把一个文件写入到磁盘的过程,你并不会对 Redis 的写入策略有何惊叹,别无二致。

Linux 把要写入的数据先写到(fwrite) 用户态缓冲区,用户缓冲区再把里面的数据拷贝(fflush)到内核态缓冲区,内核缓冲区再把数据写入(fsync)磁盘。如果你不调用 fsync 的话,将会由内核自己决定写入时机,否则立即写入磁盘。

fflsh和fsync区别.png

尽管 Redis 写入日志的过程与之极其相似,但还是有必要阐述流程和绘制图形,以便后续逻辑的展开。

  1. Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区。
  2. 然后通过 write 系统调用,将 server.aof_buf 缓冲区 的数据写入到 AOF 文件中,此时的数据并没有写入到磁盘,而是拷贝到内核缓冲区中,等待内核自行决定何时写入磁盘,当然也可以调用 fsync 让内核立即写入磁盘。

Redis写入日志.png

Redis 有三种写入磁盘的策略:两个极端一个折中

  • Always:数据拷贝到内核后,立即写入磁盘。很大程度上保证数据的不丢失,但是如此频繁的写入磁盘会影响到主线程性能。
  • No:数据拷贝到内核后,由内核自行决定写入磁盘时机。对数据的持久性没有太大的保障,完全看内核的心情,但是性能要好很多。
  • Everysec:数据拷贝到内核后,每秒写入一次磁盘。算是一种折中策略,性能和数据的保障介于上面二者之间。

总结.png

AOF 重写机制

AOF 日志本质上就是一个文件,里面记录 Redis 执行的写命令,可是伴随着命令越来越多,文件也会越来越大,因此Redis 提供重写机制。

对于同一个 key,但是有着多个修改 value 的指令,那么就应该存储最后一个写指令(修改指令),因为前面的数据可以被视为无效数据了。这样就起到压缩作用了。

重写.png

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

你可能会问,为什么不复用旧的 AOF 文件(即后面压缩的指令从旧的 AOF 文件开头进行覆盖,而不用新创建 AOF 文件),而是写入到一个新的 AOF 文件,然后再进行替换呢?我不知道你有没有用过 realloc 函数,它用来调整空间的大小,如果失败就会返回 NULL。很多人在使用的时候,会像下面这样:

1
p = (int *)realloc(p, 10 * sizeof(int));

如果 realloc 创建成功会返回内存的首地址,可是你保不准会创建失败,你这个时候就把 源数据 p 用来接收返回值,那不是有把源数据污染的可能吗?你应该创建一个临时指针变量来接收返回值,再判断返回值没有问题之后,再赋值给源数据 p。

回到这里来,如果我们再重写的过程中,机器出现问题,我原有的 AOF 文件亦不会有损。等到我真的压缩成功之后,再删除原来的 AOF 文件也不迟。

AOF 后台重写

AOF 重写是对大文件进行操作,是个相当耗时的操作,绝不可让它在主线程中执行,否则性能大大降低。Redis 是把这个工作交给子进程 bgrewriteaof 来完成的,这样主线程和子线程互不干扰,各司其职。

特别注意是进程,而不是线程,因为进程要比线程稳定,还不用考虑多线程下的并发问题。子进程会拷贝父进程的页表等数据结构,会在发生写操作的时候,触发【写时复制】。那么这里就有个问题,如果父进程的物理页很大怎么办?

其中有两个阶段会导致阻塞父进程:

  • 创建子进程,子进程会复制父进程的页表等数据结构,尽管共享的同一块物理内存。页表等内容越大,阻塞时间越长。
  • 触发写时复制,子进程会拷贝修改部分的物理内存出来,得到属于自己的物理内存,未修改部分继续和父进程共享。拷贝的内存越大,阻塞时间越长。

触发重写机制后,主线程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据(即共享的数据),并逐一把内存数据的键值对转换成一条命令(压缩),再将命令记录到重写日志(新的 AOF 文件)。

但我们现在要明确如下两个观点:

  • 写时复制只会把修改的那块内存复制一份出来,其余没有修改的部分父子进程继续共享。
  • 主进程可以继续执行命令和写入内存,子进程 bgrewriteaof 的任务就是执行重写机制。

那么这里就有几个问题:

  • 写时复制的时候,如果这个阶段修改的是一个 bigkey,也就是数据量比较大的 key-value 的时候,这时复制的物理内存数据的过程会比较耗时,有阻塞主进程的风险。
  • 子进程在重写,但是主进程又写入新的指令,并且还是已重写中的一个指令,这就导致数据不是最新的,出现数据不一致的问题。

Redis 为了解决数据不一致性问题,设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到【AOF缓冲区】和【AOF重写缓冲区】。

重写缓冲区.png

在子进程 bgrewriteaof 执行 AOF 重写期间,主进程要执行下面三个任务:

  1. 执行客户端发来的命令。
  2. 将执行后的写命令追加到【AOF 缓冲区】。
  3. 将执行后的写命令追加到【AOF 重写缓冲区】。

1 和 2 的操作是之前就有,即 执行命令和写入日志。3 是为了解决一致性问题新加入的缓冲区,并且只会在子进程 bgrewriteaof 执行 AOF 重写期间有存在的必要。当子进程完成 AOF 重写工作后,会向主进程发生一条信号(进程间通信的方式,且是异步)。

主进程收到信号后,会调用信号处理函数,这个期间主线程阻塞,即不允许再执行指令和写入指令操作,否则每次都无法保证数据的一致性。整个流程如下:

  • 将 AOF 重写缓冲区中的所有内容追加到 刚刚的 AOF 的文件中(也就是最新的 AOF 文件),使得新旧两个 AOF 文件所保存的数据库状态一致(解决一致性问题)。
  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件(保持最新的数据状态,所以覆盖现有的)。

信号函数执行完成后,主进程就可以继续像往常一样处理命令了。

在整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主线程。


⭐️内容取自《小林Coding》,仅从中取出个人以为需要纪录的内容。不追求内容的完整性,却也不会丢失所记内容的逻辑性。如果需要了解细致,建议访问官方网站