简介:本文将详解Redis中现有AOF机制的一些有余以及Redis 7.0中引入的Multi Part AOF的设计和实现细节。

Redis 作为一种十分风行的内存数据库,通过将数据保留在内存中,Redis 得以领有极高的读写性能。然而一旦过程退出,Redis 的数据就会全副失落。

为了解决这个问题,Redis 提供了 RDB 和 AOF 两种长久化计划,将内存中的数据保留到磁盘中,防止数据失落。本文将重点探讨AOF长久化计划,以及其存在的一些问题,并探讨在Redis 7.0 (已公布RC1) 中Multi Part AOF(下文简称为MP-AOF,本个性由阿里云数据库Tair团队奉献)设计和实现细节。

AOF

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

因为AOF会以追加的形式记录每一条redis的写命令,因而随着Redis解决的写命令增多,AOF文件也会变得越来越大,命令回放的工夫也会增多,为了解决这个问题,Redis引入了AOF rewrite机制(下文称之为AOFRW)。AOFRW会移除AOF中冗余的写命令,以等效的形式重写、生成一个新的AOF文件,来达到缩小AOF文件大小的目标。

AOFRW

图1展现的是AOFRW的实现原理。当AOFRW被触发执行时,Redis首先会fork一个子过程进行后盾重写操作,该操作会将执行fork那一刻Redis的数据快照全副重写到一个名为temp-rewriteaof-bg-pid.aof的长期AOF文件中。

因为重写操作为子过程后盾执行,主过程在AOF重写期间仍然能够失常响应用户命令。因而,为了让子过程最终也能获取重写期间主过程产生的增量变动,主过程除了会将执行的写命令写入aof_buf,还会写一份到aof_rewrite_buf中进行缓存。在子过程重写的前期阶段,主过程会将aof_rewrite_buf中累积的数据应用pipe发送给子过程,子过程会将这些数据追加到长期AOF文件中(具体原理可参考这里)。

当主过程承接了较大的写入流量时,aof_rewrite_buf中可能会沉积十分多的数据,导致在重写期间子过程无奈将aof_rewrite_buf中的数据全副生产完。此时,aof_rewrite_buf残余的数据将在重写完结时由主过程进行解决。

当子过程实现重写操作并退出后,主过程会在backgroundRewriteDoneHandler 中解决后续的事件。首先,将重写期间aof_rewrite_buf中未生产完的数据追加到长期AOF文件中。其次,当所有准备就绪时,Redis会应用rename 操作将长期AOF文件原子的重命名为server.aof_filename,此时原来的AOF文件会被笼罩。至此,整个AOFRW流程完结。


图1 AOFRW实现原理

AOFRW存在的问题

内存开销

由图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不可服务。

CPU开销

CPU的开销次要有三个中央,别离解释如下:

在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);    }       // 此处省略其余细节...}

在子过程执行重写操作的前期,会循环读取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();    }    // 此处省略其余细节...}

在子过程实现重写操作后,主过程会在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上的抖动,甚至造成客户端超时的问题。

磁盘IO开销

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

代码复杂度

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实现

计划概述

顾名思义,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逻辑更加简略清晰。

要害实现

Manifest

在内存中的示意

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

  • aofInfo:示意一个AOF文件信息,以后仅包含文件名、文件序号和文件类型
  • base_aof_info:示意BASE AOF信息,当不存在BASE AOF时,该字段为NULL
  • incr_aof_list:用于寄存所有INCR 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. */    // 此处省略其余细节...}

在磁盘上的示意

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执行降级,接下来,咱们会持续判断,当满足上面三种状况之一时咱们会认为这是一个降级启动:

  • 如果appenddirname目录不存在
  • 或者appenddirname目录存在,然而目录中没有对应的manifest清单文件
  • 如果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 函数进行降级前的筹备工作。

降级筹备工作次要分为三个局部:

  • 应用server.aof_filename作为文件名来结构一个BASE AOF信息
  • 将该BASE AOF信息长久化到manifest文件
  • 应用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应用两个设计来解决这个问题:

  • BASE AOF的名字中蕴含文件序号,保障每次创立的BASE AOF不会和之前的BASE AOF抵触
  • 先执行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 函数通过七个步骤实现了上述逻辑:

  • 在批改内存中的server.aof_manifest前,先dup一份长期的manifest构造,接下来的批改都将针对这个长期的manifest进行。这样做的益处是,一旦前面的步骤呈现失败,咱们能够简略的销毁长期manifest从而回滚整个操作,防止净化server.aof_manifest全局数据结构
  • 从长期manifest中获取新的BASE AOF文件名(记为new_base_filename),并将之前(如果有)的BASE AOF标记为HISTORY
  • 将子过程产生的temp-rewriteaof-bg-pid.aof临时文件重命名为new_base_filename
  • 将长期manifest构造中上一次的INCR AOF全副标记为HISTORY类型
  • 将长期manifest对应的信息长久化到磁盘(persistAofManifest外部会保障manifest自身批改的原子性)
  • 如果上述步骤都胜利了,咱们能够释怀的将内存中的server.aof_manifest指针指向长期的manifest构造(并开释之前的manifest构造),至此整个批改对Redis可见
  • 清理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企业版的binlog实现,这是一套在阿里云Tair服务上久经验证的外围性能,在这个外围性能上阿里云Tair胜利构建了寰球多活、PITR等企业级能力,使用户的更多业务场景需要失去满足。明天咱们将这个外围能力奉献给Redis社区,心愿社区用户也能享受这些企业级个性,并通过这些企业级个性更好的优化,发明本人的业务代码。无关MP-AOF的更多细节,请移步参考相干PR(#9788),那里有更多的原始设计和残缺代码。

原文链接
本文为阿里云原创内容,未经容许不得转载。