Bootstrap

🕋【Redis干货领域】从底层彻底吃透AOF重写(原理篇)

🕋 每日一句

不要轻易去依赖一个人,它会成为你的习惯,当分别来临,你失去的不是某个人,而是你精神的支柱。无论何时何地,都要学会独立行走,它会让你走得更坦然些。

🕋 前提介绍

为了保证缓存数据的完整性和可靠性,Redis提供两种持久化机制:

  • RDB: 将数据库的快照以二进制的方式保存到磁盘;

  • AOF: 将所有写入命令及相关参数以协议文本的方式写入文件并持久保存磁盘;

本文只关心AOF,Redis Server将所有写入的命令转换成RESP协议文本的方式写入AOF文件。

🕋 AOF的实现

Redis的AOF是类似于log的机制,每次写操作都会写到硬盘上,当系统崩溃时,可以通过AOF来恢复数据。每个带有写操作的命令被Redis服务器端收到运行时,该命令都会被记录到AOF文件上。由于只是一个append到文件操作,所以写到硬盘上的操作往往非常快。

其实Redis AOF 机制包括了两件事,Rewrite和AOF,因为AOF主要采用的方式属于Append Of File的方式追加是文档存储,本质内容暂时不考虑,本章重点内容是Rewrite机制

🕋 AOF执行案例

Redis Server收到的的写入命令,Redis server会进行以下几步操作:

  • 将命令转换成协议文本,转换后的结果:

*3
$3
SET
$3
KEY
$5
VALUE
  • 将协议文本追加到aof缓存,也就是aof_buf

  • 根据sync策略调用fsync/fdatasync

  • 到目前为止已经成功保存数据,如果想要还原AOF,只需要将AOF里命令读出来并重放就可以还原数据库。

🕋 重写的介绍
  • AOF持久化机制存在一个致命的问题,随着时间推移,AOF文件会膨胀,如果频繁写入AOF文件会膨胀到无限大,当server重启时严重影响数据库还原时间,影响系统可用性。

  • 为解决此问题,系统需要定期重写AOF文件,目前采用的方式是创建一个新的AOF文件,将数据库里的全部数据转换成协议的方式保存到文件中,通过此操作达到减少AOF文件大小的目的,重写后的大小一定是小于等于旧AOF文件的大小

🕋 重写的实现
  • rewrite类似于普通数据库管理系统日志恢复点,当AOF文件随着写命令的运行膨胀时,当文件大小触碰到临界时,rewrite会被运行。

  • rewrite(bgrewriteaof相似)会像replication一样,fork出一个子进程,创建一个临时文件,遍历数据库内存数据,将每个key、value对输出到临时文件。输出格式就是Redis的命令(RESP),但是为了减小文件大小,会将多个key、value对集合起来用一条命令表达。

  • rewrite期间的写操作会保存在内存的rewrite buffer中,rewrite成功后这些操作也会复制到临时文件中(指令传播),在最后临时文件会代替AOF文件。以上在AOF打开的情况下,如果AOF是关闭的,那么rewrite操作可以通过bgrewriteaof命令来进行。

🕋 重写的类型

本文只关心BGREWRITE的问题,因此只介绍此命令的实现机制。

  • REWRITE: 在主线程中重写AOF,会阻塞工作线程,在生产环境中很少使用,处于废弃状态;

  • BGREWRITE: 在后台(子进程)重写AOF, 不会阻塞工作线程,能正常服务,此方法最常用。

🕋 重写的流程
🕋 实现关键点
  • 由于写操作通常是有缓冲的,所以有可能AOF操作并没有写到硬盘中,一般可以通过fsync()来强制输出到硬盘中。而fsync()的频率可以通过配置文件中的flush策略来指定,可以选择每次事件循环写操作都强制fsync或者每秒fsync至少运行一次。

  • 当rewrite子进程开始后,父进程接受到的命令会添加到aof_rewrite_buf_blocks中,使得rewrite成功后,将这些命令添加到新文件中。在rewrite过程中,原来的AOF也可以选择是不是继续添加,由于存在性能上的问题,在rewrite过程中,如果fsync()继续执行,会导致IO性能受损影响Redis性能。所以一般情况下rewrite期间禁止fsync()到旧AOF文件。这策略可以在配置文件中修改。

  • 在rewrite结束后,在将新rewrite文件重命名为配置中指定的文件时,如果旧AOF存在,那么会unlink掉旧文件。这是就存在一个问题,处理rewrite文件迁移的是主线程,rename(oldpath, newpath)过程会覆盖旧文件,这是rename会unlink(oldfd),而unlink操作会导致block主线程。这时,我们就需要类似libeio(http://software.schmorp.de/pkg/libeio.html)这样的库去进行异步的底层IO。作者在bio.c有一个类似的机制,通过创建新线程来进行异步操作。

🕋 异步重写的支持

Redis Server收到BGREWRITE命令或者系统自动触发AOF重写时,主进程创建一个子进程并进行AOF重写,主进程异步等待子进程结束(信号量),此时主进程能正常接收处理用户请求,用户请求会修改数据库里数据,会使得当前数据库的数据跟重写后AOF里不一致,需要有种机制保证数据的一致性。当前的做法是在重写 AOF 期间系统会新开一块内存用于缓存重写期间收到的命令,在重写完成以后再将缓存中的数据追加到新的AOF。

在处理命令时既要将命令追加到 aof_buf,也要追加到重写AOF Buffer。

🕋 Rewrite存在的问题

重写AOF Buffer是个不限大小的buffer,但用户写入的数据量较多时会出现以下两个问题:

🕋 自动触发条件

AOF里存放了所有的redis 操作指令,文件达到条件或者手动bgrewriteaof命令都可以触发rewrite。

rewrite之后aof文件会保存keys的最后的状态,清除掉之前冗余的,来缩小这个文件.

long long growth =(server.appendonly_current_size*100/base) - 100; 
if (growth >=server.auto_aofrewrite_perc)

 在配置文件里设置过:

auto-aof-rewrite-percentage 100 (当前写入日志文件的大小超过上一次rewrite之后的文件大小的百分之100时就是2倍时触发Rewrite)

此外那个64m的代码我就不列举了 我相信大家都知道 哈哈,以后的篇章我会从源码去分析介绍的,期待吧 哈哈。

🕋 后台Rewrite问题解决方案

🕋 官方解决方案(AOF_BUFFER_BLOCK不进行阻塞)

主要思路是AOF重写期间,主进程跟子进程通过管道通信,主进程实时将新写入的数据发送给子进程,子进程从管道读出数据交缓存在buffer中,子进程等待存量数据全部写入AOF文件后,将缓存数据追加到AOF文件中(不是主进程写入),此方案只是解决阻塞工作线程问题,但占用内存过多问题并没有解决。

🕋 新解决方案(buffer配对文件机制)

主要思路是AOF重写期间,主进程创建一个新的aof_buf,新的AOF文件用于接收新写入的命令,sync策略保持不变,在AOF重写期间,系统需要向两个aof_buf,两个AOF文件同时追加新写入的命令。当主进程收到子进程重写AOF文件完成后,停止向老的aof_buf,AOF文件追加命令,然后删除旧的AOF文件(流程跟原来保持一致);将将子进程新生成的AOF文件重命名为appendonly.aof.last,具体流程如下:

系统运行期间同时存在两个AOF文件,一个是当前正在写的AOF,另一个是存量的AOF数据文件。因此需要修改数据库恢复相关逻辑,加载AOF时先要加载存量数据appendonly.aof.last,再加载appendonly.aof。