Redis 作为一种非常流行的内存数据库,通过将数据保存在内存中,Redis 得以拥有极高的读写性能。但是一旦进程退出,Redis 的数据就会全部丢失。

为了解决这个问题,Redis 提供了 RDB 和 AOF 两种持久化方案,将内存中的数据保存到磁盘中,避免数据丢失。本文将重点讨论AOF持久化方案,以及其存在的一些问题,并探讨在Redis 7.0 (已发布RC1) 中Multi Part AOF,下文简称为MP-AOF。

一  AOF

AOF( append only file )持久化以独立日志文件的方式记录每条写命令,并在 Redis 启动时回放 AOF 文件中的命令以达到恢复数据的目的。

由于AOF会以追加的方式记录每一条redis的写命令,因此随着Redis处理的写命令增多,AOF文件也会变得越来越大,命令回放的时间也会增多,为了解决这个问题,Redis引入了AOF REWRITE 机制(下文称之为AOFRW),AOFRW 会移除 AOF 中冗余的写命令,以等效的方式重写、生成一个新的 AOF 文件,来达到减少 AOF 文件大小的目的(通过开启子线程)。

AOF 文件比 RDB 文件大,恢复数据慢,启动效率低。

二  AOFRW( AOF REWRITE)机制

AOF 里面可能有写没用的命令,所以 AOP 会定期根据最新的内存生成最新的 AOF文件。当 AOFRW 被触发执行时,Redis 首先会 fork  一个子进程进行后台重写操作,该操作会将执行 fork 那一刻 Redis 的数据快照全部重写到一个名为 temp-rewriteaof-bg-pid.aof 的临时 AOF 文件中。 具体的 AOFRW  实现原理请参考图1。

图1 AOFRW实现原理

由于重写操作为子进程后台执行,主进程在 AOF 重写期间依然可以正常响应用户命令。因此,为了让子进程最终也能获取重写期间主进程产生的增量变化,主进程除了会将执行的写命令写入aof_buf,还会写一份到 aof_rewrite_buf 中进行缓存。在子进程重写的后期阶段,主进程会将aof_rewrite_buf 中累积的数据使用pipe发送给子进程,子进程会将这些数据追加到临时AOF文件中(详细原理可参考[1])。

当主进程承接了较大的写入流量时,aof_rewrite_buf 中可能会堆积非常多的数据,导致在重写期间子进程无法将 aof_rewrite_buf 中的数据全部消费完。此时,aof_rewrite_buf  剩余的数据将在重写结束时由主进程进行处理。

当子进程完成重写操作并退出后,主进程会在 backgroundRewriteDoneHandler  中处理后续的事情。首先,将重写期间 aof_rewrite_buf 中未消费完的数据追加到临时 AOF 文件中。其次,当一切准备就绪时,Redis 会使用 rename  操作将临时 AOF 文件原子的重命名为server.aof_filename,此时原来的 AOF 文件会被覆盖。至此,整个 AOFRW 流程结束。

三  AOFRW存在的问题

1  内存开销

由图1可以看到,在 AOFRW 期间,主进程会将 fork 之后的数据变化写进 aof_rewrite_buf 中,aof_rewrite_buf 和 aof_buf 中的内容绝大部分都是重复的,因此这将带来额外的内存冗余开销。

在 Redis INFO 中的 aof_rewrite_buffer_length 字段可以看到当前时刻 aof_rewrite_buf 占用的内存大小。如下面显示的,在高写入流量下 aof_rewrite_buffer_length 几乎和 aof_buffer_length占用了同样大的内存空间,几乎浪费了一倍的内存。

aof_pending_rewrite:0aof_buffer_length:35500aof_rewrite_buffer_length:34000aof_pending_bio_fsync:0

当 aof_rewrite_buf 占用的内存大小超过一定阈值时,我们将在 Redis 日志中看到如下信息。可以看到,aof_rewrite_buf 占用了 100MB 的内存空间且主进程和子进程之间传输了 2135MB 的数据(子进程在通过 pipe 读取这些数据时也会有内部读 buffer 的内存开销)。

对于内存型数据库Redis而言,这是一笔不小的开销。

3351:M 25 Jan 2022 09:55:39.655 * Background append only file rewriting started by pid 68173351:M 25 Jan 2022 09:57:51.864 * AOF rewrite child asks to stop sending diffs.6817:C 25 Jan 2022 09:57:51.864 * Parent agreed to stop sending diffs. Finalizing AOF...6817:C 25 Jan 2022 09:57:51.864 * Concatenating 2135.60 MB of AOF diff received from parent.3351:M 25 Jan 2022 09:57:56.545 * Background AOF buffer size: 100 MB

AOFRW 带来的内存开销有可能导致 Redis 内存突然达到 maxmemory 限制,从而影响正常命令的写入,甚至会触发操作系统限制被 OOM Killer 杀死,导致 Redis 不可服务。

2  CPU开销

CPU的开销主要有三个地方,分别解释如下:

  1. 在 AOFRW 期间,主进程需要花费 CPU 时间向 aof_rewrite_buf 写数据,并使用 eventloop事件循环向子进程发送 aof_rewrite_buf 中的数据:

/* Append data to the AOF rewrite buffer, allocating new blocks if needed. */void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {    // 此处省略其他细节...
    /* Install a file event to send data to the rewrite child if there is     * not one already. */    if (!server.aof_stop_sending_diff &&        aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0)    {        aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,            AE_WRITABLE, aofChildWriteDiffData, NULL);    } 
    // 此处省略其他细节...}
  1. 在子进程执行重写操作的后期,会循环读取 pipe 中主进程发送来的增量数据,然后追加写入到临时 AOF 文件:

int rewriteAppendOnlyFile(char *filename) {     // 此处省略其他细节...
    /* Read again a few times to get more data from the parent.     * We can't read forever (the server may receive data from clients     * faster than it is able to send data to the child), so we try to read     * some more data in a loop as soon as there is a good chance more data     * will come. If it looks like we are wasting time, we abort (this     * happens after 20 ms without new data). */    int nodata = 0;    mstime_t start = mstime();    while(mstime()-start < 1000 && nodata < 20) {        if (aeWait(server.aof_pipe_read_data_from_parent, AE_READABLE, 1) <= 0)        {            nodata++;            continue;        }        nodata = 0; /* Start counting from zero, we stop on N *contiguous*                       timeouts. */        aofReadDiffFromParent();    }
    // 此处省略其他细节...}
  1. 在子进程完成重写操作后,主进程会在 backgroundRewriteDoneHandler  中进行收尾工作。其中一个任务就是将在重写期间 aof_rewrite_buf 中没有消费完成的数据写入临时 AOF 文件。如果 aof_rewrite_buf 中遗留的数据很多,这里也将消耗CPU时间。

​​​​​​​
void backgroundRewriteDoneHandler(int exitcode, int bysignal) {    // 此处省略其他细节...
    /* Flush the differences accumulated by the parent to the rewritten AOF. */    if (aofRewriteBufferWrite(newfd) == -1) {        serverLog(LL_WARNING,                "Error trying to flush the parent diff to the rewritten AOF: %s", strerror(errno));        close(newfd);        goto cleanup;     }
     // 此处省略其他细节...}

AOFRW带来的 CPU 开销可能会造成 Redis 在执行命令时出现RT上的抖动,甚至造成客户端超时的问题。

3  磁盘IO开销

如前文所述,在 AOFRW 期间,主进程除了会将执行过的写命令写到 aof_buf 之外,还会写一份到 aof_rewrite_buf 中。aof_buf 中的数据最终会被写入到当前使用的旧  AOF  文件中,产生磁盘IO。同时,aof_rewrite_buf  中的数据也会被写入重写生成的新 AOF 文件中,产生磁盘 IO。因此,同一份数据会产生两次磁盘IO。

4  代码复杂度

Redis 使用下面所示的六个 pipe 进行主进程和子进程之间的数据传输和控制交互,这使得整个 AOFRW 逻辑变得更为复杂和难以理解。

​​​​​​​

 /* AOF pipes used to communicate between parent and child during rewrite. */ int aof_pipe_write_data_to_child; int aof_pipe_read_data_from_parent; int aof_pipe_write_ack_to_parent; int aof_pipe_read_ack_from_child; int aof_pipe_write_ack_to_child; int aof_pipe_read_ack_from_parent;

四  MP-AOF实现

1  方案概述

顾名思义,MP-AOF 就是将原来的单个 AOF 文件拆分成多个 AOF 文件。在 MP-AOF 中,我们将 AOF 分为三种类型,分别为:

  • BASE:表示基础 AOF,它一般由子进程通过重写产生,该文件最多只有一个。

  • INCR:表示增量 AOF,它一般会在 AOFRW 开始执行时被创建,该文件可能存在多个。

  • HISTORY:表示历史 AOF,它由 BASE 和 INCR AOF 变化而来,每次 AOFRW 成功完成时,本次 AOFRW 之前对应的 BASE 和 INCR AOF 都将变为 HISTORY ,HISTORY类型的AOF 会被Redis自动删除。

为了管理这些 AOF 文件,我们引入了一个 manifest(清单)文件来跟踪、管理这些 AOF。同时,为了便于 AOF 备份和拷贝,我们将所有的 AOF 文件和 manifest 文件放入一个单独的文件目录中,目录名由 appenddirname 配置(Redis 7.0新增配置项)决定。

图2 MP-AOF Rewrite原理

图2展示的是在 MP-AOF 中执行一次 AOFRW 的大致流程。在开始时我们依然会 fork 一个子进程进行重写操作,在主进程中,我们会同时打开一个新的 INCR 类型的 AOF 文件,在子进程重写操作期间,所有的数据变化都会被写入到这个新打开的 INCR AOF 中。子进程的重写操作完全是独立的,重写期间不会与主进程进行任何的数据和控制交互,最终重写操作会产生一个 BASE AOF。新生成的 BASE AOF 和新打开的 INCR AOF 就代表了当前时刻 Redis 的全部数据。AOFRW 结束时,主进程会负责更新 manifest 文件,将新生成的 BASE AOF 和 INCR AOF 信息加入进去,并将之前的 BASE AOF 和 INCR  AOF 标记为 HISTORY(这些 HISTORY AOF 会被Redis 异步删除)。一旦 manifest 文件更新完毕,就标志整个 AOFRW  流程结束。

由图2可以看到,我们在 AOFRW 期间不再需要 aof_rewrite_buf ,因此去掉了对应的内存消耗。同时,主进程和子进程之间也不再有数据传输和控制交互,因此对应的 CPU 开销也全部去掉。对应的,前文提及的六个 pipe 及其对应的代码也全部删除,使得 AOFRW 逻辑更加简单清晰。

2  关键实现

Manifest

1)在内存中的表示

MP-AOF 强依赖 manifest 文件,manifest 在内存中表示为如下结构体,其中:

  • aofInfo:表示一个 AOF 文件信息,当前仅包括文件名、文件序号和文件类型

  • base_aof_info:表示 BASE AOF 信息,当不存在 BASE AOF 时,该字段为NULL

  • incr_aof_list:用于存放所有I NCR AOF 文件的信息,所有的 INCR AOF 都会按照文件打开顺序排放

  • history_aof_list:用于存放 HISTORY AOF 信息,history_aof_list 中的元素都是从 base_aof_info 和 incr_aof_list 中 move 过来的

​​​​​​​
typedef struct {    sds           file_name;  /* file name */    long long     file_seq;   /* file sequence */    aof_file_type file_type;  /* file type */} aofInfo;
typedef struct {    aofInfo     *base_aof_info;       /* BASE file information. NULL if there is no BASE file. */    list        *incr_aof_list;       /* INCR AOFs list. We may have multiple INCR AOF when rewrite fails. */    list        *history_aof_list;    /* HISTORY AOF list. When the AOFRW success, The aofInfo contained in                                         `base_aof_info` and `incr_aof_list` will be moved to this list. We                                         will delete these AOF files when AOFRW finish. */    long long   curr_base_file_seq;   /* The sequence number used by the current BASE file. */    long long   curr_incr_file_seq;   /* The sequence number used by the current INCR file. */    int         dirty;                /* 1 Indicates that the aofManifest in the memory is inconsistent with                                         disk, we need to persist it immediately. */} aofManifest;

为了便于原子性修改和回滚操作,我们在 redisServer 结构中使用指针的方式引用 aofManifest 。

​​​​​​​

struct redisServer {    // 此处省略其他细节...
    aofManifest *aof_manifest;       /* Used to track AOFs. */
    // 此处省略其他细节...}

2)在磁盘上的表示

Manifest 本质就是一个包含多行记录的文本文件,每一行记录对应一个 AOF 文件信息,这些信息通过 key/value 对的方式展示,便于 Redis 处理、易于阅读和修改。下面是一个可能的 manifest 文件内容:

​​​​​​​

file appendonly.aof.1.base.rdb seq 1 type bfile appendonly.aof.1.incr.aof seq 1 type ifile appendonly.aof.2.incr.aof seq 2 type i

Manifest 格式本身需要具有一定的扩展性,以便将来添加或支持其他的功能。比如可以方便的支持新增 key/value 和注解(类似 AOF 中的注解),这样可以保证较好的 forward compatibility。

​​​​​​​

file appendonly.aof.1.base.rdb seq 1 type b newkey newvaluefile appendonly.aof.1.incr.aof type i seq 1 # this is annotationsseq 2 type i file appendonly.aof.2.incr.aof

文件命名规则

在 MP-AOF 之前,AOF 的文件名为 appendfilename 参数的设置值(默认为appendonly.aof)。

在 MP-AOF 中,我们使用 basename.suffix 的方式命名多个 AOF 文件。其中,appendfilename 配置内容将作为 basename 部分,suffix 则由三个部分组成,格式为 seq.type.format ,其中:

  • seq为文件的序号,由1开始单调递增,BASE 和INCR 拥有独立的文件序号

  • type为AOF的类型,表示这个 AOF 文件是 BASE 还是 INCR

  • format, 用来表示这个 AOF 内部的编码方式,由于 Redis 支持 RDB preamble 机制,因此BASE AOF可能是RDB格式编码也可能是AOF格式编码:

#define BASE_FILE_SUFFIX           ".base"#define INCR_FILE_SUFFIX           ".incr"#define RDB_FORMAT_SUFFIX          ".rdb"#define AOF_FORMAT_SUFFIX          ".aof"#define MANIFEST_NAME_SUFFIX       ".manifest"

因此,当使用 appendfilename 默认配置时,BASE、INCR 和 manifest 文件的可能命名如下:​​​​​​​

appendonly.aof.1.base.rdb // 开启RDB preambleappendonly.aof.1.base.aof // 关闭RDB preambleappendonly.aof.1.incr.aofappendonly.aof.2.incr.aof

兼容老版本升级

由于 MP-AOF 强依赖 manifest 文件,Redis 启动时会严格按照 manifest 的指示加载对应的AOF 文件。但是在从老版本 Redis(指Redis 7.0之前的版本)升级到 Redis 7.0时,由于此时并无manifest 文件,因此如何让Redis正确识别这是一个升级过程并正确、安全的加载旧AOF是一个必须支持的能力。

识别能力是这一重要过程的首要环节,在真正加载 AOF 文件之前,我们会检查 Redis 工作目录下是否存在名为 server.aof_filename 的 AOF 文件。如果存在,那说明我们可能在从一个老版本Redis 执行升级,接下来,我们会继续判断,当满足下面三种情况之一时我们会认为这是一个升级启动:

  1. 如果appenddirname目录不存在

  2. 或者appenddirname目录存在,但是目录中没有对应的manifest清单文件

  3. 如果appenddirname目录存在且目录中存在manifest清单文件,且清单文件中只有BASE AOF相关信息,且这个BASE AOF的名字和server.aof_filename相同,且appenddirname目录中不存在名为server.aof_filename的文件​​​​​​​

/* Load the AOF files according the aofManifest pointed by am. */int loadAppendOnlyFiles(aofManifest *am) {    // 此处省略其他细节...
    /* If the 'server.aof_filename' file exists in dir, we may be starting     * from an old redis version. We will use enter upgrade mode in three situations.     *     * 1. If the 'server.aof_dirname' directory not exist     * 2. If the 'server.aof_dirname' directory exists but the manifest file is missing     * 3. If the 'server.aof_dirname' directory exists and the manifest file it contains     *    has only one base AOF record, and the file name of this base AOF is 'server.aof_filename',     *    and the 'server.aof_filename' file not exist in 'server.aof_dirname' directory     * */    if (fileExist(server.aof_filename)) {        if (!dirExists(server.aof_dirname) ||            (am->base_aof_info == NULL && listLength(am->incr_aof_list) == 0) ||            (am->base_aof_info != NULL && listLength(am->incr_aof_list) == 0 &&             !strcmp(am->base_aof_info->file_name, server.aof_filename) && !aofFileExist(server.aof_filename)))        {            aofUpgradePrepare(am);        }    }
    // 此处省略其他细节...  }

一旦被识别为这是一个升级启动,我们会使用aofUpgradePrepare 函数进行升级前的准备工作。

升级准备工作主要分为三个部分:

  1. 使用server.aof_filename作为文件名来构造一个BASE AOF信息

  2. 将该BASE AOF信息持久化到manifest文件

  3. 使用rename 将旧AOF文件移动到appenddirname目录中​​​​​​​

void aofUpgradePrepare(aofManifest *am) {    // 此处省略其他细节...
    /* 1. Manually construct a BASE type aofInfo and add it to aofManifest. */    if (am->base_aof_info) aofInfoFree(am->base_aof_info);    aofInfo *ai = aofInfoCreate();    ai->file_name = sdsnew(server.aof_filename);    ai->file_seq = 1;    ai->file_type = AOF_FILE_TYPE_BASE;    am->base_aof_info = ai;    am->curr_base_file_seq = 1;    am->dirty = 1;
    /* 2. Persist the manifest file to AOF directory. */    if (persistAofManifest(am) != C_OK) {        exit(1);    }
    /* 3. Move the old AOF file to AOF directory. */    sds aof_filepath = makePath(server.aof_dirname, server.aof_filename);    if (rename(server.aof_filename, aof_filepath) == -1) {        sdsfree(aof_filepath);        exit(1);;    }
    // 此处省略其他细节...}

升级准备操作是Crash Safety的,以上三步中任何一步发生Crash我们都能在下一次的启动中正确的识别并重试整个升级操作。

多文件加载及进度计算

Redis在加载AOF时会记录加载的进度,并通过Redis INFO的loading_loaded_perc字段展示出来。在MP-AOF中,loadAppendOnlyFiles 函数会根据传入的aofManifest进行AOF文件加载。在进行加载之前,我们需要提前计算所有待加载的AOF文件的总大小,并传给startLoading 函数,然后在loadSingleAppendOnlyFile 中不断的上报加载进度。

接下来,loadAppendOnlyFiles 会根据aofManifest依次加载BASE AOF和INCR AOF。当前加载完所有的AOF文件,会使用stopLoading 结束加载状态。

​​​​​​​

int loadAppendOnlyFiles(aofManifest *am) {    // 此处省略其他细节...
    /* Here we calculate the total size of all BASE and INCR files in     * advance, it will be set to `server.loading_total_bytes`. */    total_size = getBaseAndIncrAppendOnlyFilesSize(am);    startLoading(total_size, RDBFLAGS_AOF_PREAMBLE, 0);
    /* Load BASE AOF if needed. */    if (am->base_aof_info) {        aof_name = (char*)am->base_aof_info->file_name;        updateLoadingFileName(aof_name);        loadSingleAppendOnlyFile(aof_name);    }
    /* Load INCR AOFs if needed. */    if (listLength(am->incr_aof_list)) {        listNode *ln;        listIter li;
        listRewind(am->incr_aof_list, &li);        while ((ln = listNext(&li)) != NULL) {            aofInfo *ai = (aofInfo*)ln->value;            aof_name = (char*)ai->file_name;            updateLoadingFileName(aof_name);            loadSingleAppendOnlyFile(aof_name);        }    }
    server.aof_current_size = total_size;    server.aof_rewrite_base_size = server.aof_current_size;    server.aof_fsync_offset = server.aof_current_size;
    stopLoading();
    // 此处省略其他细节...}

AOFRW Crash Safety

当子进程完成重写操作,子进程会创建一个名为 temp-rewriteaof-bg-pid.aof 的临时 AOF 文件,此时这个文件对Redis而言还是不可见的,因为它还没有被加入到manifest文件中。要想使得它能被Redis识别并在Redis启动时正确加载,我们还需要将它按照前文提到的命名规则进行rename 操作,并将其信息加入到manifest文件中。

AOF文件rename 和manifest文件修改虽然是两个独立操作,但我们必须保证这两个操作的原子性,这样才能让Redis在启动时能正确的加载对应的AOF。MP-AOF使用两个设计来解决这个问题:

  1. BASE AOF的名字中包含文件序号,保证每次创建的BASE AOF不会和之前的BASE AOF冲突;

  2. 先执行AOF的rename 操作,再修改manifest文件;

为了便于说明,我们假设在AOFRW开始之前,manifest文件内容如下:

​​​​​​​

file appendonly.aof.1.base.rdb seq 1 type bfile appendonly.aof.1.incr.aof seq 1 type i

则在AOFRW开始执行后manifest文件内容如下:

​​​​​​​

file appendonly.aof.1.base.rdb seq 1 type bfile appendonly.aof.1.incr.aof seq 1 type ifile appendonly.aof.2.incr.aof seq 2 type i

子进程重写结束后,在主进程中,我们会将temp-rewriteaof-bg-pid.aof重命名为appendonly.aof.2.base.rdb,并将其加入manifest中,同时会将之前的BASE和INCR AOF标记为HISTORY。此时manifest文件内容如下:​​​​​​​

file appendonly.aof.2.base.rdb seq 2 type bfile appendonly.aof.1.base.rdb seq 1 type hfile appendonly.aof.1.incr.aof seq 1 type hfile appendonly.aof.2.incr.aof seq 2 type i

此时,本次AOFRW的结果对Redis可见,HISTORY AOF会被Redis异步清理。

backgroundRewriteDoneHandler 函数通过七个步骤实现了上述逻辑:

  1. 在修改内存中的server.aof_manifest前,先dup一份临时的manifest结构,接下来的修改都将针对这个临时的manifest进行。这样做的好处是,一旦后面的步骤出现失败,我们可以简单的销毁临时manifest从而回滚整个操作,避免污染server.aof_manifest全局数据结构;

  2. 从临时manifest中获取新的BASE AOF文件名(记为new_base_filename),并将之前(如果有)的BASE AOF标记为HISTORY;

  3. 将子进程产生的temp-rewriteaof-bg-pid.aof临时文件重命名为new_base_filename;

  4. 将临时manifest结构中上一次的INCR  AOF全部标记为HISTORY类型;

  5. 将临时manifest对应的信息持久化到磁盘(persistAofManifest内部会保证manifest本身修改的原子性);

  6. 如果上述步骤都成功了,我们可以放心的将内存中的server.aof_manifest指针指向临时的manifest结构(并释放之前的manifest结构),至此整个修改对Redis可见;

  7. 清理HISTORY类型的AOF,该步骤允许失败,因为它不会导致数据一致性问题。​​​​​​​

void backgroundRewriteDoneHandler(int exitcode, int bysignal) {    snprintf(tmpfile, 256, "temp-rewriteaof-bg-%d.aof",        (int)server.child_pid);
    /* 1. Dup a temporary aof_manifest for subsequent modifications. */    temp_am = aofManifestDup(server.aof_manifest);
    /* 2. Get a new BASE file name and mark the previous (if we have)     * as the HISTORY type. */    new_base_filename = getNewBaseFileNameAndMarkPreAsHistory(temp_am);
    /* 3. Rename the temporary aof file to 'new_base_filename'. */    if (rename(tmpfile, new_base_filename) == -1) {        aofManifestFree(temp_am);        goto cleanup;    }
    /* 4. Change the AOF file type in 'incr_aof_list' from AOF_FILE_TYPE_INCR     * to AOF_FILE_TYPE_HIST, and move them to the 'history_aof_list'. */    markRewrittenIncrAofAsHistory(temp_am);
    /* 5. Persist our modifications. */    if (persistAofManifest(temp_am) == C_ERR) {        bg_unlink(new_base_filename);        aofManifestFree(temp_am);        goto cleanup;    }
    /* 6. We can safely let `server.aof_manifest` point to 'temp_am' and free the previous one. */    aofManifestFreeAndUpdate(temp_am);
    /* 7. We don't care about the return value of `aofDelHistoryFiles`, because the history     * deletion failure will not cause any problems. */    aofDelHistoryFiles();}

支持AOF truncate 

在进程出现Crash时AOF文件很可能出现写入不完整的问题,如一条事务里只写了MULTI,但是还没写EXEC时Redis就Crash。默认情况下,Redis无法加载这种不完整的AOF,但是Redis支持AOF truncate功能(通过aof-load-truncated配置打开)。其原理是使用server.aof_current_size跟踪AOF最后一个正确的文件偏移,然后使用ftruncate 函数将该偏移之后的文件内容全部删除,这样虽然可能会丢失部分数据,但可以保证AOF的完整性。

在MP-AOF中,server.aof_current_size已经不再表示单个AOF文件的大小而是所有AOF文件的总大小。因为只有最后一个INCR AOF才有可能出现不完整写入的问题,因此我们引入了一个单独的字段server.aof_last_incr_size用于跟踪最后一个INCR AOF文件的大小。当最后一个INCR AOF出现不完整写入时,我们只需要将server.aof_last_incr_size之后的文件内容删除即可。

​​​​​​​
 if (ftruncate(server.aof_fd, server.aof_last_incr_size) == -1) {      //此处省略其他细节... }

AOFRW限流

Redis在AOF大小超过一定阈值时支持自动执行AOFRW,当出现磁盘故障或者触发了代码bug导致AOFRW失败时,Redis将不停的重复执行AOFRW直到成功为止。在MP-AOF出现之前,这看似没有什么大问题(顶多就是消耗一些CPU时间和fork开销)。但是在MP-AOF中,因为每次AOFRW都会打开一个INCR AOF,并且只有在AOFRW成功时才会将上一个INCR和BASE转为HISTORY并删除。因此,连续的AOFRW失败势必会导致多个INCR AOF并存的问题。极端情况下,如果AOFRW重试频率很高我们将会看到成百上千个INCR AOF文件。

为此,我们引入了AOFRW限流机制。即当AOFRW已经连续失败三次时,下一次的AOFRW会被强行延迟1分钟执行,如果下一次AOFRW依然失败,则会延迟2分钟,依次类推延迟4、8、16...,当前最大延迟时间为1小时。

在AOFRW限流期间,我们依然可以使用bgrewriteaof命令立即执行一次AOFRW。

​​​​​​​

if (server.aof_state == AOF_ON &&    !hasActiveChildProcess() &&    server.aof_rewrite_perc &&    server.aof_current_size > server.aof_rewrite_min_size &&    !aofRewriteLimited()){    long long base = server.aof_rewrite_base_size ?        server.aof_rewrite_base_size : 1;    long long growth = (server.aof_current_size*100/base) - 100;    if (growth >= server.aof_rewrite_perc) {        rewriteAppendOnlyFileBackground();    }}

AOFRW限流机制的引入,还可以有效的避免AOFRW高频重试带来的CPU和fork开销。Redis中很多的RT抖动都和fork有关系。

五  总结

MP-AOF的引入,成功的解决了之前AOFRW存在的内存和CPU开销对Redis实例甚至业务访问带来的不利影响。同时,在解决这些问题的过程中,我们也遇到了很多未曾预料的挑战,这些挑战主要来自于Redis庞大的使用群体、多样化的使用场景,因此我们必须考虑用户在各种场景下使用MP-AOF可能遇到的问题。如兼容性、易用性以及对Redis代码尽可能的减少侵入性等。这都是Redis社区功能演进的重中之重。

同时,MP-AOF的引入也为Redis的数据持久化带来了更多的想象空间。如在开启aof-use-rdb-preamble时,BASE AOF本质是一个RDB文件,因此我们在进行全量备份的时候无需在单独执行一次BGSAVE操作。直接备份BASE AOF即可。MP-AOF支持关闭自动清理HISTORY AOF的能力,因此那些历史的AOF有机会得以保留,并且目前Redis已经支持在AOF中加入timestamp annotation,因此基于这些我们甚至可以实现一个简单的PITR能力( point-in-time recovery)。

MP-AOF的设计原型来自于Tair for redis企业版[2]的binlog实现,这是一套在阿里云Tair服务上久经验证的核心功能,在这个核心功能上阿里云Tair成功构建了全球多活、PITR等企业级能力,使用户的更多业务场景需求得到满足。今天我们将这个核心能力贡献给Redis社区,希望社区用户也能享受这些企业级特性,并通过这些企业级特性更好的优化,创造自己的业务代码。有关MP-AOF的更多细节,请移步参考相关PR(#9788),那里有更多的原始设计和完整代码。

[1]http://mysql.taobao.org/monthly/2018/12/06/

[2]https://help.aliyun.com/document_detail/145956.html

05 Redis 持久化的设计和实现相关推荐

  1. Redis设计与实现 -- 浅谈Redis持久化

    在讲解Redis持久化相关的话题之前,我们需要了解的是Redis为什么这么快?也就是Redis的IO模型 – 多路复用. 我们一句话概括为什么Redis这么快: Redis是单线程的,使用多路复用的I ...

  2. Redis持久化实践及数据恢复

    2019独角兽企业重金招聘Python工程师标准>>> 参考资料: Redis Persistence http://redis.io/topics/persistence Goog ...

  3. Redis持久化实践及灾难恢复模拟

    Redis持久化实践及灾难恢复模拟 源地址:http://heylinux.com/archives/1932.html 另一篇:Redis主从自动failover http://ylw6006.bl ...

  4. Redis持久化-数据丢失及解决

    转载自 http://www.cnblogs.com/hs8888/p/5520495.html Redis的数据回写机制 Redis的数据回写机制分同步和异步两种, 同步回写即SAVE命令,主进程直 ...

  5. 小伙用 12 张图讲明白了 Redis 持久化!

    00 前言 很多小伙伴都用 Redis 做缓存,那如果 Redis 服务器宕机,内存中数据全部丢失,应该如何做数据恢复呢?有人说很简单呀,直接从 MySQL 数据库再读回来就得了. 这种方式存在两个问 ...

  6. 跟着狂神学Redis(NoSql+环境配置+五大数据类型+三种特殊类型+Hyperloglog+Bitmap+事务+Jedis+SpringBoot整合+Redis持久化+...)

    跟着狂神学Redis 狂神聊Redis 学习方式:不是为了面试和工作学习!仅仅是为了兴趣!兴趣才是最好的老师! 基本的理论先学习,然后将知识融汇贯通! 狂神的Redis课程安排: nosql 讲解 阿 ...

  7. 05 Redis的RDB日志

    05 Redis的RDB日志 前言 一.Redis 做内存数据快照的数据 二.Redis 生成 RDB 文件的命令save 和 bgsave 三.Redis 生成RDB文件时的写时复制技术 四.Red ...

  8. 深入学习Redis持久化

    一.Redis高可用概述 在介绍Redis高可用之前,先说明一下在Redis的语境中高可用的含义. 我们知道,在web服务器中,高可用是指服务器可以正常访问的时间,衡量的标准是在多长时间内可以提供正常 ...

  9. quartz持久化是指_面试必问:Redis 持久化是如何做的?RDB 和 AOF 对比分析

    从这篇文章开始,我们来介绍Redis高可用相关的机制.Redis要想实现高可用,主要有以下方面来保证: 数据持久化 主从复制 自动故障恢复 集群化 这篇文章我们先介绍Redis的高可用保障的基础:数据 ...

最新文章

  1. 2022-2028年中国公路客运行业市场研究及前瞻分析报告
  2. let const var 比较说明
  3. 深度学习目标检测(object detection)系列(一) R-CNN
  4. Android毛玻璃处理代码(Blur)
  5. 用 Apache 发布 ASP.NET 网站
  6. C#语法之Linq查询基础一
  7. 互联网大鳄的成长模式
  8. [LintCode] Minimum Size Subarray Sum 最小子数组和的大小
  9. 看图工具—IrfanView
  10. Linux下Wireshark的Lua: Error during loading 和 couldn't run /usr/bin/dumpcap in child process 的解决方案
  11. iview使用原生html,iview在vue-cli3如何按需加载的方法
  12. jquery网页日历显示控件calendar3.1使用详解
  13. python3 centos7-linux 安装
  14. vim中编辑了代码 但是提示can not write的解决办法和代码对齐办法
  15. Cacti监控Varnish
  16. PLC编程语言入门,常用指令集汇总分享
  17. ubuntu freeswitch安装
  18. 第1节 中华人民共和国网络安全法
  19. 学习笔记 Tianmao 篇 OkHttp 网络的使用的简单封装 获取Json用GSON来解析
  20. 程序员 做头发 奇遇记

热门文章

  1. macosx输入法将英文设成默认
  2. redis 复制集群搭建
  3. 隐私政策--Walkermi
  4. HBU训练营【动态规划DP】——兔子跳楼梯 (20分)
  5. 2021高考成绩查询镇远一中,离太阳由近到远的八大行星排序及记忆方法
  6. snap安装nextcloud关键点
  7. javascript按钮的三级联动
  8. 【nginx】version `OPENSSL_1.0.2‘ not found
  9. 人生苦短,该是及时行乐?或是该苦尽甘来?
  10. Nim和anti-Nim