• redis持久化机制,深入分析redisAOF和RDB模式的利弊


    文章目录

    写在前面

    我们都知道,Redis是一个内存数据库,数据保存在内存中,访问速度是相当快的。

    但是内存中的数据,服务器每次重启之后就会丢失,redis是如何做到持久化的呢?redis持久化设计有哪些巧妙之处呢?

    目前,Redis 的持久化主要有两大机制,即 AOF(Append Only File)日志和 RDB 快照。

    日志文件-AOF

    AOF的格式

    redis的AOF是一种日志文件,不像mysql的redo log(重做日志),记录的是修改后的二进制数据, AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。

    我们以 Redis 收到“set testkey testvalue”命令后记录的日志为例,看看 AOF 日志的内容。其中,“*3”表示当前命令有三个部分,每部分都是由“$+数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“$3 set”表示这部分有 3 个字节,也就是“set”命令。
    在这里插入图片描述

    AOF的写入方式

    AOF 里记录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的,也就是说每一条命令都会生成一条AOF日志。

    那么假如说这条命令是语法有误的命令呢?也会记录吗?

    Redis 是先执行命令,把数据写入内存,然后才记录日志,如下图所示:
    在这里插入图片描述
    语法有问题的命令当然不会写入AOF日志,但是如果写入AOF时再额外检查语法的话,就会有一些额外的开销。

    为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。所以AOF采用先执行命令后写日志的方式——写后日志。

    而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。

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

    不过,redis的AOF写后日志是有风险的:
    ① 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。
    ② AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。

    那么有解决以上问题的方案吗?有。

    三种写回策略

    redis有三种写AOF的方式:

    • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
    • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

    但是这三种写回方式都是有弊端的:

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

    在这里插入图片描述

    AOF 中开启 always 刷盘策略也会存在数据丢失吗?

    可能会。Redis 是先操作内存,后写AOF磁盘日志。比如 Redis 内存执行完了,去刷盘的时候宕机了就会导致数据丢失。

    AOF配置为每秒刷盘,有可能阻塞Redis,影响性能吗?

    有可能的。

    AOF 配置为每秒刷盘,具体逻辑是这样的:

    1、Redis 主线程把命令写到 AOF page cache(调用 write 系统调用)
    2、Redis 后台线程每间隔 1 秒,把 AOF page cache 持久化到磁盘(调用 fsync 系统调用)

    如果 2 执行时,迟迟没有成功,那么 1 执行时就会阻塞住,原因是在操作同一个 fd 时,fsync 和 write互斥的,一方必须等待另一方完成。

    步骤 2 执行不成功的原因在于:机器的磁盘 IO 负载非常高(可能有别的程序在疯狂写磁盘,把磁盘带宽占满了),此时 1 在执行时,就会阻塞等待,从而影响到了主线程,进而影响整个 Redis 性能。

    具体可参见 Redis 源码 aof.c,搜索:Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
    在这里插入图片描述

    AOF日志太大的弊端

    随着接收的写命令越来越多,AOF 文件就会不可避免的越来越大。

    AOF文件太大必然会影响性能:
    ① 文件系统本身对文件大小有限制,无法保存过大的文件;
    ② 如果文件太大,之后再往里面追加命令记录的话,效率也会变低;
    ③ 如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。

    怎么处理AOF文件过大的问题呢?——AOF重写机制。

    AOF重写

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

    AOF重写结果其实就是 对旧日志文件中的多条命令,在重写后的新日志中变成了一条命令。
    AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。

    在这里插入图片描述
    如上图,redis在不断对数据进行操作时,其实多条命令操作一条数据之后的结果,可以将这多条命令合并,这样在AOF文件中只记录一个最终结果即可。这样可以大大的缩小AOF文件的大小。

    AOF重写对主线程的影响性

    AOF重写会影响到redis的主线程吗?

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

    AOF 重写过程中其他潜在的阻塞风险

    这里有两个风险。

    风险一:

    Redis 主线程 fork 创建 bgrewriteaof 子进程时,内核需要创建用于管理子进程的相关数据结构,这些数据结构在操作系统中通常叫作进程控制块(Process Control Block,简称为 PCB)。内核要把主线程的 PCB 内容拷贝给子进程。这个创建和拷贝过程由内核执行,是会阻塞主线程的。而且,在拷贝过程中,子进程要拷贝父进程的页表,这个过程的耗时和 Redis 实例的内存大小有关。如果 Redis 实例内存大,页表就会大,fork 执行时间就会长,这就会给主线程带来阻塞风险。

    a、fork子进程,fork这个瞬间一定是会阻塞主线程的(注意,fork时并不会一次性拷贝所有内存数据给子进程,老师文章写的是拷贝所有内存数据给子进程,我个人认为是有歧义的),fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据,这个过程中,父进程也可能会产生阻塞的风险,就是下面介绍的场景。
    
    b、fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,这样逐渐地,父子进程内存数据开始分离,父子进程逐渐拥有各自独立的内存空间。因为内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。
    
    • 1
    • 2
    • 3

    风险二:

    bgrewriteaof 子进程会和主线程共享内存。当主线程收到新写或修改的操作时,主线程会申请新的内存空间,用来保存新写或修改的数据,如果操作的是 bigkey,也就是数据量大的集合类型数据,那么,主线程会因为申请大空间而面临阻塞风险。因为操作系统在分配内存空间时,有查找和锁的开销,这就会导致阻塞。

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

    关于Huge page

    Huge page对提升TLB命中率比较友好,因为在相同的内存容量下,使用huge page可以减少页表项,TLB就可以缓存更多的页表项,能减少TLB miss的开销。

    但是,这个机制对于Redis这种喜欢用fork的系统来说,的确不太友好,尤其是在Redis的写入请求比较多的情况下。因为fork后,父进程修改数据采用写时复制,复制的粒度为一个内存页。如果只是修改一个256B的数据,父进程需要读原来的内存页,然后再映射到新的物理地址写入。一读一写会造成读写放大。如果内存页越大(例如2MB的大页),那么读写放大也就越严重,对Redis性能造成影响。

    Huge page在实际使用Redis时是建议关掉的。

    AOF重写的过程

    AOF重写的过程:
    每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。

    AOF重写的日志记录:
    1、因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。
    2、而第二处日志,就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
    在这里插入图片描述
    总结来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。

    AOF重写的时候,如果重写缓冲区满了,怎么处理?

    重写缓冲区会满吗?满了会放弃本次重写吗?

    AOF重写缓冲区不会满,是个链表,只要内存不超过设置的maxmemory。
    如果超过maxmemory,执行配置的淘汰策略。

    AOF 重写能共享使用 AOF 本身的日志吗

    显然是不能的。如果都用 AOF 日志的话,主线程要写,bgrewriteaof 子进程也要写,这两者会竞争文件系统的锁,这就会对 Redis 主线程的性能造成影响。

    内存快照-RDB

    所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。

    对 Redis 来说,RDB(Redis DataBase) 它实现类似照片记录效果的方式,把某一时刻的状态以文件的形式写到磁盘上,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。

    RDB 记录的是某一时刻的数据,所以,在做数据恢复时,我们可以直接把 RDB 文件读入内存,很快地完成恢复。

    给哪些数据做快照

    Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,需要把内存中的所有数据都记录到磁盘中。

    那么问题来了,生成快照的时候,需要redis主线程阻塞吗?
    给内存的全量数据做快照,把它们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。

    Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。

    • save:在主线程中执行,会导致阻塞;
    • bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。

    就像给人拍照一样,拍照的时候摄影师都会要求大家保持一个表情不动,那么redis生成RDB的时候需要保持数据静止吗?

    生成快照时需要数据静止吗

    生成快照并不是一瞬间生成的,肯定是所有的数据逐步生成的。
    那么,在生成快照时,需要数据全部静止吗(不允许写)?

    举个例子。我们在时刻 t 给内存做快照,假设内存数据量是 4GB,磁盘的写入带宽是 0.2GB/s,简单来说,至少需要 20s(4/0.2 = 20)才能做完。如果在时刻 t+5s 时,一个还没有被写入磁盘的内存数据 A,被修改成了 A’,那么就会破坏快照的完整性,因为 A’不是时刻 t 时的状态。

    但是,如果快照执行期间数据不能被修改,那无疑就会给业务服务造成巨大的影响。

    写时复制(Copy-On-Write)

    简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。

    此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本(键值对 C’)。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。

    在这里插入图片描述
    这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响。

    Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主线程同时可以修改数据。

    为啥RDB 要 fork 子进程而不是线程?

    1、先想一下RDB的目的是什么?就是把内存数据持久化到磁盘上,而且只持久化截止某一时刻的数据即可,不关心之后的数据怎么改(内存快照)

    2、性能:如果用子线程做的话,主线程写,其他线程读,然后子线程数据写磁盘,有资源竞争,需要加锁,加锁会降低Redis性能,而且在实现上很复杂,成本高。

    3、基于以上考虑,fork一个子进程来搞,最经济,成本也最低。因为fork子进程,操作系统把这些事都做好了,有内存快照数据,没有锁竞争,子进程怎么写磁盘也不会影响父进程,还有COW不影响主进程写数据,一举多得。

    如果上一次生成RDB快照还没执行完,又触发了持久化策略,这个时候是顺序执行等上一次持久化完成?还是并行处理?

    等上一次RDB执行完,才能触发执行下一次RDB。

    关于Copy On write问题:数据持久化fork子进程时,子进程不会一次copy所有数据,而是在修改时触发Copy On write。假设主线程中有1000条数据,fork创建子进程后,主线程有请求新增了100条,修改了200条,这些内存是如何在主进程和子进程分配的?

    1、首先要理解 Copy On Write 含义:即写时复制,谁写谁复制

    2、fork子进程,此时的子进程和父进程会指向相同的地址空间,当父进程有新的写请求进来,它想要修改数据,那么它就把需要修改的key的内存,拷贝一份出来,再修改这块新内存的数据,此时父进程内存地址就会指向这个新申请的内存空间

    3、在这期间,子进程不会修改任何数据,所以不会分配任何新的内存,它依旧指向父进程那些数据的内存地址空间,这个过程是操作系统层面做好的。

    那fork期间会阻塞父进程吗?为什么会阻塞?

    1、fork完成之前,会阻塞父进程,主要是父进程需要拷贝进程中的内存页表给子进程,每个进程都要有自己的内存页表,所以这个父子进程无法共享,必须要拷贝一份

    2、拷贝内存页表也需要花费时间,进程占用的内存越大,拷贝时间越久

    RDB写入的时候,通过主线程fork出bgsave子进程时,进行写入RDB文件,此时主线程也可以接受的写操作,那么主线程接收新的写操作,bgsave子进程还会再把这个数据,写入到RDB文件吗?

    不会。RDB的目的是,只要一份内存快照,即只要fork那一瞬间,父进程所拥有的数据,fork完成后子进程指向父进程的所有内存数据地址空间,所以就与父进程共享数据了,此时子进程把这些数据scan出来,持久化到磁盘就可以了,不需要关心父进程有没有写入新数据。

    子进程做RDB期间,父进程写入新数据,父进程做Copy On Write申请新的内存,那子进程完成RDB后,进程退出,内存回收是怎样的?

    子进程退出时,如果它指向的内存数据,没有被父进程修改过(对于这块数据,父进程没有做COW),那么这块内存数据,还是归父进程所有,子进程不会回收。

    如果在子进程RDB期间,父进程有新数据写入或修改,对一部分key的内存做了COW,那这些key的内存,父子进程各自独立,子进程退出时,就会回收它指向的这些内存空间。

    内存快照的缺点

    1、两次内存快照期间服务器宕机时,此时新的数据没来得及保存快照,会造成数据丢失。

    2、性能问题:
    ① 频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
    ② bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了(所以,在 Redis 中如果有一个 bgsave 在运行,就不会再启动第二个 bgsave 子进程)。

    AOF+RDB混合模式

    AOF+RDB混合模式就是,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

    这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。

    在这里插入图片描述

    开启混合持久化,在恢复时加载 RDB 文件和 AOF 的顺序是怎样的

    准确来说,混合持久化只是对 AOF rewrite 做的优化。

    因为 AOF 写入很长时间后,文件体积会变得非常大,为了优化文件大小,Redis提出了 AOF rewrite,即文件增长一定阈值(可配置),则自动重写这个 AOF 文件,以达到缩小文件体积的目的。

    后来到 Redis 4.0,又进一步做了优化,就是混合持久化。

    就是在 AOF rewrite 时,先写一个 RDB 进去,再把在这期间产生的写命令,追加到 AOF 文件中。

    在 AOF 文件中,前面是一个 RDB 格式的数据,后面是 AOF 格式的数据。

    这样一来,这个 AOF 文件体积就更小了。

    Redis【混合持久化】,具体持久化的数据格式是怎样的?

    一个 Redis 要想开启混合持久化,必须这样配置:1.开启AOF 2.配置AOF rewrite 3.配置AOF rewrite 开启混合持久化,缺一不可。

    重点:混合持久化是对 AOF rewrite 的进一步优化。

    然后,从最简单的说起,一个空Redis,开始写入数据,数据做持久化,会是这样:

    假设写入了 100 条命令,那 AOF 中记录的就是这 100 条命令
    假设现在触发了 AOF rewrite 的阈值,需要对 AOF 进行瘦身,因为开启了【混合持久化】,那 Redis 会扫描整个实例,把整个实例中的数据,生成一个 RDB,写到 AOF 中 (RDB 是二进制数据)
    在写 RDB 到 AOF 文件期间,Redis 依旧会收到写操作,假设这段时间收到了 10 条写命令,那这 10 条命令会等 RDB 写完成后,再依次追加到 AOF 中,此时 AOF 中的数据就是 RDB + 10 条写命令
    之后 Redis 继续收到写操作,假设收到 5 条写命令,那也依次追加到 AOF 中,此时 AOF 中就是一个 RDB + 10 + 5 条写命令
    继续写操作,继续追加 AOF,如果此时 AOF 又触发到了需要 rewrite 的阈值,那就继续走 2-3-4 步骤。

    如何区分rdb和aof的分界?

    再次触发aof重写时候,如何处理的?会不会生成新的文件?rdb+10+5已经存在的数据如何处理

    答:
    RDB 文件是有结束标志位的,读到标志位,就知道读完RDB了。
    只要触发 AOF rewrite,就会生成新的 AOF 文件,替换旧的

    参考资料

    蒋德钧老师对redis的讲解。
    Kaito老师对redis的分享。
    https://www.yuque.com/docs/share/9677b389-1af5-4d22-805b-ee2d651100d9#AOF

  • 相关阅读:
    make: /opt/rh/llvm-toolset-7/root/usr/bin/clang: Command not found
    中学数学建模书籍及相关的视频等(2022.08.09)
    IMS异常场景介绍
    OC 新建项目流程2022
    Vue常见问题
    SSE:后端向前端发送消息(springboot SseEmitter)
    STL-常用容器
    软考 系统架构设计师系列知识点之特定领域软件体系结构DSSA(5)
    emacs从缓冲中获取信息,并执行shell 命令
    使用 OpenCV 进行图像操作:腐蚀、膨胀等等
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/126758491