当前位置: 首页 > 科技观察

就是这样?Redis持久化策略——AOF

时间:2023-03-21 23:33:57 科技观察

今天介绍Redis的另一种持久化策略——AOF。什么是AOF?男孩“醒了”,忘记了对女孩的承诺。这时,女孩将彼此的录音一一播放给男孩听,帮助他“恢复记忆”。“男孩醒了”貌似Redis关机重启,女孩记录的是Redis的AOF日志。AOF(AppendOnlyFile)通过将所有对数据库的写命令以文本的形式记录在AOF文件中来记录数据库的状态(文本格式是Redis自定义的,后面会提到)。注意:AOF文件只会记录Redis写命令,因为读命令对数据恢复没有意义。Redis默认不开启AOF功能。在redis.conf配置文件中,AOF的相关配置如下:#是否开启AOF功能(on:是,off:否)appendonlyyes#生成的AOF文件名appendfilename6379.aof#AOF回写策略appendfsynceverysec#当前AOF文件大小与上次rewrite后大小之比>=指定增长百分比,则rewrite#例如100表示??当前AOF文件大小是上次rewrite前rewrite大小的两倍auto-aof-rewrite-percentage100#AOF文件的最小重写大小,只有当AOF文件的大小大于这个值时,才有可能重写,4.0的默认配置是64mb。auto-aof-rewrite-min-size64mbAOF日志格式下面通过一个例子来看看AOF机制是如何保存我们的操作日志的。我们在Redis上执行以下操作。127.0.0.1:6379[3]>RPUSHlist12345(整数)5127.0.0.1:6379[3]>LRANGElist0-11)"1"2)"2"3)"3"4)"4"5)"5"127.0.0.1:6379[3]>RPOPlist"5"127.0.0.1:6379[3]>LPUSHlist0(整数)5127.0.0.1:6379[3]>KEYS*1)"list"127.0.0.1:6379[3]>LRANGElist0-11)"0"2)"1"3)"2"4)"3"5)"4"Redis会把上面写的命令全部保存到AOF文件中,如下:RPUSHlist12345RPOPlistLPUSHlist0当然是AOF文件它不直接以指令的格式存储。我们以第一条指令RPUSHlist12345为例,看看AOF文件实际保存指令的格式。*2$6SELECT$13*7$5RPUSH$4list$11$12$13$14$15除了SELECT命令是AOF程序自己添加的,其他命令都是我们之前在终端执行的命令。这条指令是自动加上的,因为Redis在恢复数据的时候,需要知道要恢复的数据属于哪个数据库。其中,*2表示当前命令有2个部分,每个部分以$+数字开头,后面跟着具体的命令、键或值。该数字表示此部分中的命令、键或值总共有多少字节。例如$6SELECT表示这部分有6个字节,是SELECT命令。AOF日志的生成过程从发送写入命令开始,到将命令保存到AOF文件中。它需要经过4个步骤,分别是命令传播、命令追加、文件写入和文件同步。命令传播命令传播:Redis将执行的命令、命令参数、命令参数个数等信息发送给AOF程序。大家有没有注意到其中一个细节,AOF日志是在Redis执行命令成功之后写入的,为什么要写在执行之后而不是执行之前呢?有两个原因:首先,想象一下,如果我们不小心输入了错误的Redis命令,然后Redis将命令保存在AOF文件中,这可能会导致Redis在恢复数据时出错。因此,这种写后日志形式可以避免指令的语法检查,避免记录错误的指令。其次,先执行命令后保存日志,不会阻塞当前的写操作。但是,AOFpost-writelogs也有两个风险。第一个风险是,如果Redis在写操作成功后突然关机,以后还没有写入AOF日志,可能会导致命令和相关参数丢失。第二个风险是AOF虽然避免阻塞当前操作,但可能会阻塞下一个操作。因为保存AOF日志的部分工作也是由主线程完成的(下面会详细介绍),所以Redis的内存运行速度和文件写入速度简直天差地别。如果主线程在保存文件的过程中耗时过长,势必会阻塞后续操作。分析发现,第一个风险与AOF回写磁盘的时机有关。回写到磁盘的频率越高,数据丢失的可能性就越小。第二个风险与文件写入方法和时机有关。如果Redis每次成功执行命令时都尝试将当前命令同步到AOF文件中,那么开销必然很大。因此,Redis引入了缓冲区的概念。buffer对应文件的写入方式(不要求一步完成,允许渐进写入),什么时候把buffer的内容完全同步到文件就涉及到AOF的同步策略(什么时候回写到磁盘)。CommandAppending启用AOF后,Redis会将执行成功的写命令按照我们上面提到的协议格式追加到Redis的aof_buf缓冲区中。structredisServer{//...//AOF缓冲区sdsaof_buf;//...};aof_buf缓冲区存储所有等待写入AOF文件的协议文本。至此,将命令追加到缓冲区的步骤就完成了。文件写入文件的写入和同步操作往往是一起介绍的。之所以在这里分开,是想向读者强调文件的写入和同步是两个不同的操作。为了提高文件的写入效率,当用户调用write函数向文件写入数据时,操作系统内核会先将数据保存在内存缓冲区中,等待缓冲区空间被填满或达到某个值一定时间,内核会将数据同步到磁盘。这种同步过于依赖操作系统内核,时序失控。为此,操作系统提供了两个同步函数fsync和fdatasync,可以强制内核立即将缓冲区中的数据同步到磁盘中。Redis的主要服务进程本质上是一个死循环。在循环中,有逻辑负责接受客户端的请求并向客户端发送回执。我们称之为文件事件。开启AOF功能后,文件事件会将执行成功后的写命令追加到aof_buf缓冲区中。在主服务进程死循环结束时,会调用flushAppendOnlyFile函数,将aof_buf中的数据写入内核Buffer,然后判断是否进行同步。伪代码如下:voideventLoop{while(true){//...//文件事件,接受命令请求,返回客户端回执//根据aof功能是否开启,决定是否追加写入命令aof_bufbufferhandleFileEvents();//将aof_buf数据写入内核缓冲区//判断数据是否需要同步到磁盘flushAppendOnlyFile();//...}};是否同步由Redis配置中的appendOnlyFile选项决定。文件同步redis.conf配置文件中的appendOnlyFile选项有三个可选值,对应三种AOF同步策略,分别是:No:同步的时机由内核决定。everysec:每秒同步一次。Always:每次执行命令时同步。否同步时序由操作系统内核决定。每次执行写命令后,先将日志写入AOF文件的内核缓冲区,并不会立即进行同步。在这种模式下,只有在以下任何一种情况下才会进行同步:Redis被关闭AOF功能被关闭系统的写缓存被刷新(可能是缓存满了,或者进行了周期性的保存操作)这三种同步操作在这两种情况下都会导致Redis主进程阻塞。Everysec如果用户没有为appendOnlyFile指定值,则默认值为Everysec。每秒同步一次。每次执行写命令后,首先将日志写入AOF文件的内核缓冲区。理论上,缓冲区中的内容每1秒同步一次到磁盘,同步操作由单独的子线程执行。所以主进程没有被阻塞。需要注意的是,我们使用了“理论上”这个词。在实际运行中,这种模式下fsync或fdatasync的调用不是每秒一次,而是与调用flushAppendOnlyFile函数时Redis的状态有关。每当调用flushAppendOnlyFile函数时,可能会出现以下四种情况:子线程正在执行同步,本次同步执行时间不超过2秒,则程序直接返回;本次同步已经执行了2秒以上,然后程序执行了write传入的操作,但是没有执行新的同步操作。但是此时的写操作必须先等待子线程完成原来的同步操作,所以这里的写操作会比平时阻塞更长时间。如果子线程没有在执行同步,距离上次成功执行同步不到1秒,则程序执行写入,但不执行同步;上次成功执行同步已经超过1秒,程序执行写入和同步。这四种情况可以用一个流程图来表示:在Everysec模式下,如果系统在情况1中崩溃,我们最多会在2秒内丢失所有数据。如果情况2出现崩溃,那么我们可能会丢失超过2秒的数据。因此,AOF在Everysec模式下只会丢失1秒数据的说法其实并不准确。每次执行写命令后,始终将日志同步写回磁盘。在这种模式下,同步操作是由Redis主进程执行的,因此在同步执行过程中,主进程会被阻塞,无法接受命令请求。AOF同步策略总结对于三种AOF同步模式,它们对Redis主进程的阻塞条件如下:不同步(No):写入和同步都由主进程进行,两种操作都会阻塞主进程;每秒同步一次(Everysec):写操作由主进程执行,阻塞主进程。同步操作由子线程执行,不会直接阻塞主进程,但同步操作完成的快慢会影响写操作的阻塞时长;每次执行命令时(Always):同模式1。由于阻塞操作会阻止Redis主进程继续处理请求,一般来说,阻塞操作执行的次数越少,完成的越快,Redis的性能就越好是。No的同步操作只有在AOF关闭或Redis关闭时才会执行,或者由操作系统内核触发。一般来说,这种模式只需要阻塞进行写入,因此其写入性能高于后两种模式,但这种性能提升是以降低安全性为代价的:在这种模式下,如果发生宕机,丢失的数据量由操作系统内核的缓存刷新策略决定。Everysec在性能上优于Always,一般情况下该模式最多丢失不超过2秒的数据,因此其安全性高于No,是一种兼顾性能和安全性的存储方案。Always的安全性最高,但性能也是最差的,因为Redis必须阻塞,直到命令信息写入同步到磁盘后才能继续处理请求。三种AOF模式的特点可以总结为下表AOF生成过程总结最后总结一下AOF文件的生成过程。下面的步骤都是在开启AOF的前提下进行的。Redis成功执行写操作命令,然后将写入的命令以自定义格式追加到aof_buf缓冲区,即第一个缓冲区;Redis主进程将aof_bufbuffer中的数据写入内核缓冲区,也就是第二个缓冲区;根据AOF同步策略,及时将内核缓冲区中的数据同步到磁盘,进程结束。AOF文件加载和数据恢复AOF文件包含了所有可以重建数据库的写入命令,因此可以将所有命令读入并顺序执行,以恢复Redis之前的数据状态。Redis读取AOF文件并恢复数据库的详细步骤如下:创建一个没有网络连接的假客户端。在假客户端上执行命令的效果与在有网络连接的客户端上执行命令的效果完全相同;读取AOF保存的文本,根据内容恢复命令、命令参数和命令条数;使用伪客户端根据命令、命令参数等信息执行命令。执行2和3,直到执行完AOF文件中的所有命令。注意:为避免影响数据的完整性,在服务端加载数据的过程中,只有与数据库无关的发布和订阅功能才能正常使用,其他命令都会返回错误。AOF重写AOF的作用是帮助我们恢复Redis的数据状态,其中包括所有的写操作,但是一般情况下,客户端会对同一个KEY进行多次不同的写操作,如下:127.0.0.1:6379[3]>SETnamechanmufeng1OK127.0.0.1:6379[3]>SETnamechanmufeng2OK127.0.0.1:6379[3]>SETnamechanmufeng3OK127.0.0.1:6379[3]>SETnamechanmufeng4OK127.0.0.1:6379[3]>SETnamechanmufengOK例子名称的数据被写入5次。其实我们只需要最后一条指令,但是AOF会把这5条指令都记录下来。更极端的情况是,一些频繁操作的按键可能会调用数百、数千甚至数万条命令。如果这样频繁操作的键很多,AOF文件的体积就会迅速膨胀。首先,AOF文件的大小受操作系统大小的限制,不能无限增长;第二,过大的AOF文件会影响指令的写入速度,延长阻塞时间。最后,AOF文件越大,Redis数据恢复越快。时间越长。为了解决AOF文件庞大的问题,Redis提供了rewrite的AOF重写功能来减小AOF文件的体积。虽然AOF重写的实现原理叫做AOF“重写”,但是新AOF文件的生成并不是在原AOF文件的基础上进行操作得到的,而是通过读取Redis当前的数据状态重新生成的。不难理解,后者的处理方式远比前者效率高。为了避免阻塞主线程,导致数据库性能下降,不像AOF日志被主进程回写,重写过程是由子进程执行bgrewriteaof完成的。这样处理最大的好处是:在子进程AOF改写过程中,主进程可以继续处理命令请求;子进程有主进程的数据副本,运行效率更高。这里有两个问题值得我们思考。1、AOF改写为什么要用子进程而不是多线程?如果使用线程,内存将在线程之间共享。在修改共享内存数据时,需要加锁来保证数据安全,这样会降低性能。如果使用子进程,操作系统会使用“copy-on-write”技术:fork子进程时,子进程会复制父进程的页表,即虚实映射关系,并且不会复制物理内存。子进程复制父进程的页表,也可以共享和访问父进程的内存数据,达到共享内存的效果。但是,这个共享内存只能是只读的。当父子进程中的任何一个修改共享内存时,都会发生“copy-on-write”,因此父子进程拥有独立的数据副本,不需要加锁来保证数据安全。这里我拿一张RedisPersistenceStrategy-RDB画的图来帮助大家理解copy-on-write。因此,有两种进程可能导致主进程阻塞:在fork子进程的过程中,由于复制父进程的页表等数据,阻塞时间与页表大小有关,页表越大,阻塞时间越长,但一般来说,这个过程很快;在子进程fork之后,如果父子进程中的任何一个修改了共享数据,就会发生“copy-on-write”,这期间会复制物理内存。内存越大,自然阻塞的时间就越长。对于第二个过程,这里有一个小细节要提一下。Copy-on-write,复制的粒度是一个内存页。如果只修改了一个256B的数据,父进程需要读取整个原来的内存页,然后映射到新的物理地址进行写入。一读一写会造成读写放大。如果内存页比较大(比如2MB的大页),读写放大会比较严重,影响Redis的性能。因此,在使用Redis的AOF功能时,需要注意不要将页表的大小设置的过大。2.子进程AOF改写期间,主进程还需要继续处理命令,新的命令可能会修改已有数据,导致当前数据库中的数据与改写后的AOF文件中的数据不一致.我应该怎么办?为了解决这个问题,Redis引入了另一个缓冲区概念(这也是本文涉及的第三个缓冲区的概念)——AOF重写缓冲区。也就是说,子进程在执行AOF重写(bgrewriteaof)时,主进程需要执行以下三个任务:处理客户端的命令请求;将写入命令附加到AOF缓冲区(aof_buf);将写入命令附加到AOF重写缓冲区。这样可以保证:已有的AOF函数会继续执行,即使AOF重写时出现宕机,也不会丢失数据;所有修改数据库的命令都会记录在AOF重写缓冲区中。当子进程完成AOF改写后,会向父进程发送完成信号。父进程收到完成信号后,会调用一个信号处理函数,完成以下工作:将AOF重写缓冲区中的内容全部写入新的AOF文件;重命名新的AOF文件并覆盖原来的AOF文件。请注意,这是一个原子操作,在重命名过程中不接受任何客户端指令。步骤1执行后,现有AOF文件、新AOF文件和数据库的状态将完全一致。执行完第2步后,程序完成新旧AOF文件的交替。信号处理函数执行完毕后,主进程可以照常继续接受命令请求。在整个AOF后台重写过程中,只有将AOF重写缓冲区数据写入新的AOF文件和重命名操作才会导致主进程阻塞。其他时候,AOF后台重写不会对主进程造成阻塞,AOFRewrites对性能的影响微乎其微。AOF后台rewrite触发条件看另外两个关于AOF的配置:auto-aof-rewrite-percentage100auto-aof-rewrite-min-size64mbAOFrewrite可以由用户通过调用bgrewriteaof手动触发。另外,当AOF功能开启后,服务端会维护以下三个变量:记录当前AOF文件大小的变量aof_current_size;变量aof_rewrite_base_size,记录上次AOF重写后AOF文件的大小;增长百分比变量aof_rewrite_perc。每次执行Redis中的定时函数serverCron时,都会检查是否满足以下条件,如果满足则触发自动AOF重写:没有bgsave命令正在进行中。没有正在进行的bgrewriteaof。当前AOF文件大小大于我们设置的auto-aof-rewrite-min-size。当前AOF文件大小与上次AOF重写后大小的比值大于等于指定的增长百分比auto-aof-rewrite-percentage。默认情况下,增长百分比是100%,也就是说,如果满足前面三个条件,并且当前AOF文件大小是上次AOF重写的两倍大,那么就会触发自动AOF重写。总结经过多次修改,终于!小编为大家梳理一下Redis的AOF持久化方式,最后我们简单总结一下。AOF是一种持久化的方式,将Redis的所有写日志同步到磁盘。通过执行AOF中记录的所有指令,可以恢复Redis的原始数据状态。对于指令的同步时机,Redis提供了三种AOF同步策略,分别是No、Everysec、Always。三种策略对Redis性能的负面影响由低到高,数据可靠性也由低到高。.为了解决AOF日志过大的问题,Redis提供了AOF重写机制,使用“copy-on-write”和“AOFrewritebuffer”来达到精简AOF文件的目的。