AOF(Append Only File):增量持久化方式
与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的,如下图:
被写入AOF文件的所有命令都是以Redis的命令请求协议格式(RESP协议REdis Serialization Protocol)保存的,RESP2是纯文本格式。
1、AOF持久化的实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。
(1)命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以RESP协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾:
struct redisServer {
sds aof_buf; // 使用sds数据结构实现AOF缓冲区
}
命令追加在执行完命令之后,表明AOF是写后日志,采用写后日志的好处:
I) 为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。
II) 除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。但会阻塞之后的写操作。
(2)AOF文件的写入与同步
Redis的服务器主进程就是一个事件循环:
Redis的命令追加在主循环(aeMain函数)中的每次处理完写命令的执行后,通过propagate函数触发。
propagate方法将当前命令的内容append到redisServer对象的aof_buf变量中。主循环在下一个迭代进入多路复用的select方法前,Redis会通过flushAppendOnlyFile方法将aof_buf的内容write到AOF对应的文件中。但write操作只是将数据写到缓存中,什么时候从缓存真正落地到磁盘上,取决于操作系统
。只有显示调用fsync()方法才能强制地让操作系统落地数据到磁盘。
AOF写入与同步的语义
写入AOF文件指的是:调用write()写到操作系统缓存
同步AOF文件指的是:调用fsync()强制缓存落地到磁盘文件
flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项决定:
- always:主循环的每个迭代的flushAppendOnlyFile函数中都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且直接触发fsync方法同步AOF文件,强制数据落地到磁盘。由于每个命令都在写入磁盘后才返回,故容错能力最高,即使出现故障宕机,也只会丢失一个事件循环中所产生的命令数据。
- everysec:服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且每隔一秒异步地触发一次fsync方法。fsync方法的执行者是bio线程池中的某个线程。
flushAppendOnlyFile函数只是作为生产者将fsync任务放入队列,由bio线程消费并执行
。 - no:将aof_buf缓冲区中的所有内容写入到AOF文件,但不显示调用fsync,由操作系统决定什么时候落地磁盘。这种模式下,Redis无法决定增量的落地时间,因此容错能力不可控。
综上,以上3种策略将aof_buf缓冲区中的所有内容写入到AOF文件的时机都一样,不同的是fsync强制落盘的时机
。由名字可以看出,appendfsync
是同步AOF文件策略。
2、AOF文件的载入与数据还原
Redis会创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令。
3、AOF重写
(1)为了解决AOF文件体积膨胀的问题
AOF 文件过大会带来性能问题。这里的“性能问题”,主要在于以下三个方面:
- 一是,文件系统本身对文件大小有限制,无法保存过大的文件;
- 二是,如果文件太大,之后再往里面追加命令记录的话,效率也会变低;
- 三是,如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。
虽然Redis将生成新的AOF文件替换旧AOF文件的功能命名为“AOF文件重写”,但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据器状态来实现的。因为生成的新AOF文件只包含还原当前数据库状态所必须的命令,所以新AOF文件不会浪费任何硬盘空间。
(2)作为一种辅佐性的维护手段,Redis不希望AOF重写造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程里执行,这样做可以同时达到两个目的:
- 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性
。
(3)由于子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,这就可能导致重写后的AOF文件和服务器的当前数据库状态不一致。为了解决此问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,如下图:
这也就是说,在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:
- 执行客户端发来的命令
- 将执行后的写命令追加到AOF缓冲区
- 将执行后的写命令追加到AOF重写缓冲区
这样一来,从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。
(4)当子进程完成AOF重写工作后,它会向父进程发送一个信号
,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
- 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
4、何时触发AOF重写?
- 手动发送“bgrewriteaof”指令;
- 有两个配置项在控制AOF重写的触发时机:
1)auto-aof-rewrite-min-size: 表示运行AOF重写时文件的最小大小,默认为64MB
2)auto-aof-rewrite-percentage: 这个值的计算方法是:当前AOF文件大小和上一次重写后AOF文件大小的差值,再除以上一次重写后AOF文件大小。也就是当前AOF文件比上一次重写后AOF文件的增量大小,和上一次重写后AOF文件大小的比值。
AOF文件大小同时
超出上面这两个配置项时,会触发AOF重写。
参考资料
1、redis设计与实现
2、redis深度历险