爪哇国和他的伙伴们 · 2022年 9月 26日 0

Redis持久化—AOF

Redis持久化—AOF

AOF日志是如何实现的?

说到⽇志,我们⽐较熟悉的是数据库的写前⽇志(Writ e Ahead Log, WAL),也就是说,在实际写数据前, 先把修改的数据记到⽇志⽂件中,以便故障时进⾏恢复。不过,AOF⽇志正好相反,它是写后⽇志,“写后”的意思是Redis是先执⾏命令,把数据写⼊内存,然后才记录⽇志,如下图所⽰:

那AOF为什么要先执⾏命令再记⽇志呢?要回答这个问题,我们要先知道AOF⾥记录了什么内容。

传统数据库的⽇志,例如redo log(重做⽇志),记录的是修改后的数据,⽽AOF⾥记录的是Redis收到的每⼀条命令,这些命令是以⽂本形式保存的。

我们以Redis收到“set testkey testvalue”命令后记录的⽇志为例,看看AOF⽇志的内容。其中,“*3”表 ⽰当前命令有三个部分,每部分都是由“+数字”开头,后⾯紧跟着具体的命令、键或值。这⾥,“数 字”表⽰这部分中的命令、键或值⼀共有多少字节。例如,“3 set”表⽰这部分有3个字节,也就 是“set”命令。

但是,为了避免额外的检查开销,Redis在向AOF⾥⾯记录⽇志的时候,并不会先去对这些命令进⾏语法检查。所以,如果先记⽇志再执⾏命令的话,⽇志中就有可能记录了错误的命令,Redis在使⽤⽇志恢复数据 时,就可能会出错。

⽽写后⽇志这种⽅式,就是先让系统执⾏命令,只有命令能执⾏成功,才会被记录到⽇志中,否则,系统就会直接向客⼾端报错。所以,Redis使⽤写后⽇志这⼀⽅式的⼀⼤好处是,可以避免出现记录错误命令的情况。

除此之外,AOF还有⼀个好处:它是在命令执⾏后才记录⽇志,所以不会阻塞当前的写操作。

不过,AOF也有两个潜在的⻛险

⾸先,如果刚执⾏完⼀个命令,还没有来得及记⽇志就宕机了,那么这个命令和相应的数据就有丢失的⻛ 险。如果此时Redis是⽤作缓存,还可以从后端数据库重新读⼊数据进⾏恢复,但是,如果Redis是直接⽤作数据库的话,此时,因为命令没有记⼊⽇志,所以就⽆法⽤⽇志进⾏恢复了。

其次,AOF虽然避免了对当前命令的阻塞,但可能会给下⼀个操作带来阻塞⻛险。这是因为,AOF⽇志也是在主线程中执⾏的,【着重去记住】如果在把⽇志⽂件写⼊磁盘时,磁盘写压⼒⼤,就会导致写盘很慢,进⽽导致后续的操 作也⽆法执⾏了。

其实仔细分析,就会发现,问题的出现主要和落盘的时机有关。AOF机制给我们提供了三个选择,也就是AOF配置项appendfsync的三个可选值。

三种写回策略:

Always:

​ 同步写回:每个写命令执⾏完,⽴⻢同步地将⽇志写回磁盘。

Everysec:

​ 每秒写回:每个写命令执⾏完,只是先把⽇志写到AOF⽂件的内存缓冲区,每隔⼀秒把缓冲区中的内容写⼊磁盘。

No:

​ 操作系统控制的写回:每个写命令执⾏完,只是先把⽇志写到AOF⽂件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

针对避免主线程阻塞和减少数据丢失问题,这三种写回策略都⽆法做到两全其美。我们来分析下其中的原因。

  • “同步写回”可以做到基本不丢数据,但是它在每⼀个写命令后都有⼀个慢速的落盘操作,不可避免地会影响主线程性能;
  • “从不写回”虽然“操作系统控制的写回”在写完缓冲区后,就可以继续执⾏后续的命令,但是落盘的时机已经不在 Redis⼿中了,只要AOF记录没有写回磁盘,⼀旦宕机对应的数据就丢失了;
  • “每秒写回”采⽤⼀秒写回⼀次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发⽣宕机,上⼀秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。

到这⾥,就可以根据系统对⾼性能和⾼可靠性的要求,来选择使⽤哪种写回策略了。总结⼀下就是:想要获得⾼性能,就选择No策略;如果想要得到⾼可靠性保证,就选择Always策略;如果允许数据有⼀点丢 失,⼜希望性能别受太⼤影响的话,那么就选择Everysec策略。

但是,按照系统的性能需求选定了写回策略,并不是“⾼枕⽆忧”了。毕竟,AOF是以⽂件的形式在记录接 收到的所有写命令。随着接收的写命令越来越多,AOF⽂件会越来越⼤。这也就意味着,我们⼀定要⼩⼼ AOF⽂件过⼤带来的性能问题。

“性能问题”,主要在于以下三个⽅⾯:

⼀是,⽂件系统本⾝对⽂件⼤⼩有限制,⽆法保存过⼤的⽂件;

⼆是,如果⽂件太⼤,之后再往⾥⾯追加命令记录的话,效率也会变低;

三是,如果发⽣宕机,AOF中 记录的命令要⼀个个被重新执⾏,⽤于故障恢复,如果⽇志⽂件太⼤,整个恢复过程就会⾮常缓慢,这就会 影响到Redis的正常使⽤。

⽇志⽂件太⼤了怎么办?

简单来说,AOF重写机制就是在重写时,Redis根据数据库的现状创建⼀个新的AOF⽂件,也就是说,读取数据库中的所有键值对,然后对每⼀个键值对⽤⼀条命令记录它的写⼊。⽐如说,当读取了键值 对“testkey”: “testvalue”之后,重写机制会记录set testkey testvalue这条命令。这样,当需要恢复 时,可以重新执⾏该命令,实现“testkey”: “testvalue”的写⼊。

为什么重写机制可以把⽇志⽂件变⼩呢? 实际上,重写机制具有“多变⼀”功能。所谓的“多变⼀”,也就 是说,旧⽇志⽂件中的多条命令,在重写后的新⽇志中变成了⼀条命令。

我们知道,AOF⽂件是以追加的⽅式,逐⼀记录接收到的写命令的。当⼀个键值对被多条写命令反复修改 时,AOF⽂件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它⽣成 对应的写⼊命令。这样⼀来,⼀个键值对在重写⽇志中只⽤⼀条命令就⾏了,⽽且,在⽇志恢复时,只⽤执 ⾏这条命令,就可以直接完成这个键值对的写⼊了。

当我们对⼀个列表先后做了6次修改操作后,列表的最后状态是[“D”, “C”, “N”],此时,只⽤LPUSH u:list “N”, “C”, “D”这⼀条命令就能实现该数据的恢复,这就节省了五条命令的空间。对于被修改过成 百上千次的键值对来说,重写能节省的空间当然就更⼤了。

不过,虽然AOF重写后,⽇志⽂件会缩⼩,但是,要把整个数据库的最新数据的操作⽇志都写回磁盘,仍然是⼀个⾮常耗时的过程。这时,我们就要继续关注另⼀个问题了:重写会不会阻塞主线程?

AOF重写会阻塞吗?

和AOF⽇志由主线程写回不同,重写过程是由后台线程bgrewriteaof来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。

重写的过程总结为“⼀个拷⻉,两处⽇志”。

“⼀个拷⻉”就是指,每次执⾏重写时,主线程fork出后台的bgrewriteaof⼦进程。此时,fork会把主线程 的内存拷⻉⼀份给bgrewriteaof⼦进程,这⾥⾯就包含了数据库的最新数据。然后,bgrewriteaof⼦进程就 可以在不影响主线程的情况下,逐⼀把拷⻉的数据写成操作,记⼊重写⽇志。

两处⽇志”⼜是什么呢?

因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第⼀处⽇志就是指正在使⽤的AOF⽇志,Redis会把这个操作写到它的缓冲区。这样⼀来,即使宕机了,这个AOF⽇志的操作仍然是⻬全的,可 以⽤于恢复。【aof日志缓冲区刷盘很快,丢数据最低可以控制到1s,业务能容忍】

⽽第⼆处⽇志,就是指新的AOF重写⽇志。这个操作也会被写到重写⽇志的缓冲区。这样,重写⽇志也不会丢失最新的操作。等到拷⻉数据的所有操作记录重写完成后,重写⽇志记录的这些最新操作也会写⼊新的 AOF⽂件,以保证数据库最新状态的记录。此时,我们就可以⽤新的AOF⽂件替代旧⽂件了。【第二处那个重写aof日志缓存应该没这个刷盘的,只做回放】

若原有的内存数据是A, 那重写AOF时,fork后父子进程都会指向A地址,当新的写入进来时,父进程会拷贝数据(新的key直接申请内存就写了),而子进程访问A逐渐写入到重写AOF中,写完通知父进程,父进程会把写到缓冲的新key,再追加到重写AOF中。

这里只有AOF缓冲区,AOF重写缓冲区(这是两个不同缓冲区),写入新数据会写入两个缓冲区。之所以写入AOF缓冲区(后续写入AOF文件)是为了防止宕机而冗余的数据,而AOF重写缓冲区是在重写日志文件生成后,再追加到重写日志文件中,最后再将重写日志文件命名为AOF日志文件。

1、如果写入内存成功了,在写入aof日志的时候失败了,宕机数据会不会丢失,有什么处理机制吗

redis-check-aof –fix xx.aof

2、什么时候会触发AOF 重写呢?

①: 手动发送“bgrewriteaof”指令,通过子进程生成更小体积的aof,然后替换掉旧的、大体量的aof文件

②:配置自动触发

​ 1)auto-aof-rewrite-percentage 50

​ 2)auto-aof-rewrite-min-size 64mb

这两个配置项的意思是,在aof文件体量超过64mb,且比上次重写后的体量增加了50%时自动触发重写。

AOF 日志重写的时候,是由 bgrewriteaof 子进程来完成的,不用主线程参与,我们今天说的非阻塞也是指子进程的执行不阻塞主线程。但是这个重写过程会有没有其他潜在的阻塞风险

更深入的理解:【能答·出来这些 直接要18k】

a. fork子进程,fork这个瞬间一定是会阻塞主线程的(注意,fork时并不会一次性拷贝所有内存数据给子进程),fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据。

b、 fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离。

AOF重写不复用AOF本身的日志,一个原因是父子进程写同一个文件必然会产生竞争问题,控制竞争就意味着会影响父进程的性能。二是如果AOF重写过程中失败了,那么原本的AOF文件相当于被污染了,无法做恢复使用。所以Redis AOF重写一个新文件,重写失败的话,直接删除这个文件就好了,不会对原先的AOF文件产生影响。等重写完成之后,直接替换旧文件即可。

写时复制机制(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

AOF两大好处:
1、避免出现记录错误命令的情况;
2、它是在命令执行后才记录日志,所以不会阻塞当前的写操作。;
AOF两大风险:
1、还没有来得及记日志就宕机,命令和相应数据就丢失了;
2、日志写入磁盘,影响后续AOF落盘操作;

AOF的工作原理:
1、Redis 执行 fork() ,现在同时拥有父进程和子进程。
2、子进程开始将新 AOF 文件的内容写入到临时文件。
3、对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
4、当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将AOF重写缓存中的内容全部写入到新的AOF文件中。
5、对新的AOF文件进行改名,原子的覆盖原有的AOF文件;完成新旧两个AOF文件的替换。