5

Redis 专题:持久化方式之 RDB

 3 years ago
source link: https://mp.weixin.qq.com/s/sI9ZTvK9WJioQiTMEEDKCg
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

公众号搜索“码路印记”,点关注不迷路!

通过本文你将了解到以下内容: jYnM3az.png!mobile

写在前面

Redis是一个内存数据库,也就是说所有的数据将保存在内存中,这与传统的MySQL、Oracle、SqlServer等关系型数据库直接把数据保存到硬盘相比,Redis的读写效率非常高。但是保存在内存中也有一个很大的缺陷,一旦断电或者宕机,内存数据库中的内容将会全部丢失。为了弥补这一缺陷,Redis提供了把内存数据持久化到硬盘文件,以及通过备份文件来恢复数据的功能。

Redis支持两种方式的持久化:RDB快照文件和AOF。本文先介绍RDB快照方式的工作原理、优缺点等方面进行阐述,写完AOF方式后再对比两者的优缺点,结合前辈总结给出生产实践,希望能够对你理解Redis的持久化机制带来帮助。

RDB快照用官方的话来说:RDB持久化方案是按照指定时间间隔对你的数据集生成的时间点快照。它是Redis数据库中数据的内存快照,它是一个二进制文件(默认名称为:dump.rdb,可修改),存储了文件生成时Redis数据库中所有的数据内容。可用于Redis的数据备份、转移与恢复。

配置参数

RDB快照的触发方式及运行行为受配置参数的影响,打开配置文件 redis.conf 查看“SNAPSHOTTING”章节,了解RDB快照的参数及作用。对于各个参数的含义进行了翻译,英语好的同学可以直接看英文。

  • save

################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behavior will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

save参数是Redis触发自动备份的触发策略, seconds 为统计时间(单位:秒), changes 为在统计时间内发生写入的次数。 save m n 的意思是:m秒内有n条写入就触发一次快照,即备份一次。save参数可以配置多组,满足在不同条件的备份要求。如果需要关闭RDB的自动备份策略,可以使用 save "" 。以下为几种配置的说明:

save 900 1:表示900秒(15分钟)内至少有1个key的值发生变化,则执行
save 300 10:表示300秒(5分钟)内至少有1个key的值发生变化,则执行
save 60 10000:表示60秒(1分钟)内至少有10000个key的值发生变化,则执行
save "":该配置将会关闭RDB方式的持久化
  • dbfilename:快照文件的名称,默认为dump.rdb。

  • dir:快照文件保存目录,默认与当前配置文件在同一目录。

  • stop-writes-on-bgsave-error:默认值为yes。当启用了RDB且最后一次后台保存数据失败,Redis是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了。

# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes
  • rdbcompression:默认值yes。备份数据时是否使用LZF算法压缩字符串对象。默认是开启的,这样可以节约存储空间,但是在生成备份文件时消耗部分CPU。

# Compress string objects using LZF when dump .rdb databases?
# By default compression is enabled as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes
  • rdbchecksum:保存和加载rdb文件时是否使用CRC64校验,默认开启。启用此参数可以使rdb文件更加安全,提高稳定性,但是会有一定的性能(大约10%)损失。如果rdb文件创建时未使用校验和,那么校验和将被设置为0,以此告知Redis跳过校验。

# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes

持久化流程

在Redis内完成RDB持久化的方法有rdbSave和rdbSaveBackground两个函数方法(源码文件rdb.c中),先简单说下两者差别:

  • rdbSave:是同步执行的,方法调用后就会立刻启动持久化流程。由于Redis是单线程模型,持久化过程中会阻塞,Redis无法对外提供服务;

  • rdbSaveBackground:是后台(异步)执行的,该方法会fork出子进程,真正的持久化过程是在子进程中执行的,主进程会继续提供服务;

RDB持久化的触发必然离不开以上两个方法,触发的方式分为手动和自动。手动触发容易理解,是指我们通过Redis客户端人为的对Redis服务端发起持久化备份指令,然后Redis服务端开始执行持久化流程,这里的指令有save和bgsave。自动触发是Redis根据自身运行要求,在满足预设条件时自动触发的持久化流程,自动触发的场景有如下几个(摘自这篇文章):

  • serverCron中 save m n 配置规则自动触发;

  • 从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发bgsave;

  • 执行 debug reload 命令重新加载redis时;

  • 默认情况下(未开启AOF)执行shutdown命令时,自动执行bgsave;

结合源码及参考文章,我整理了RDB持久化流程来帮助大家有个整体的了解,然后再从一些细节进行说明。从下图可以知道,自动触发流程是一个完整的链路,涵盖了rdbSaveBackground、rdbSave等,接下来我以serverCron为例分析一下整个流程。 emYr6v6.png!mobile

serverCron

serverCron是Redis内的一个周期性函数,每隔100毫秒执行一次,它的其中一项工作就是:根据配置文件中save规则来判断当前需要进行自动持久化流程,如果满足条件则尝试开始持久化。简单了解一下这部分的运行原理。

第一次遇到这个函数,通过代码看下这个函数的代码注释。我们可以发现它会完成过期key处理、软件监控、更新一些统计数据、触发RDB持久化或AOF重写、客户端超时处理等,本节我们只关注RDB持久化部分。

/* This is our timer interrupt, called server.hz times per second.
 * Here is where we do a number of things that need to be done asynchronously.
 * For instance:
 *
 * - Active expired keys collection (it is also performed in a lazy way on
 *   lookup).
 * - Software watchdog.
 * - Update some statistic.
 * - Incremental rehashing of the DBs hash tables.
 * - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
 * - Clients timeout of different kinds.
 * - Replication reconnection.
 * - Many more...
 *
 * Everything directly called here will be called server.hz times per second,
 * so in order to throttle execution of things we want to do less frequently
 * a macro is used: run_with_period(milliseconds) { .... }
 */
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {}

在redisServer中有几个与RDB持久化有关的字段(如下代码)。 saveparams 为配置文件中的save配置,lastsave为上次持久化的时间戳,dirty为上次持久化后发生修改的次数。

struct redisServer {
    /* 省略其他字段 */ 
    /* RDB persistence */
    long long dirty;                /* Changes to DB from the last save,上次持久化后修改key的次数 */
    struct saveparam *saveparams;   /* Save points array for RDB,对应配置文件多个save参数 */
    int saveparamslen;              /* Number of saving points,save参数的数量 */
    time_t lastsave;                /* Unix time of last successful save 上次持久化时间*/
    /* 省略其他字段 */
}

/* 对应redis.conf中的save参数 */
struct saveparam {
    time_t seconds;                    /* 统计时间范围 */   
    int changes;                    /* 数据修改次数 */
};

这部分的源码比较简单,感兴趣的同学可以下载查看,为了节省篇幅我就不贴代码了。如果没有后台的RDB持久化或AOF重写进程,serverCron会根据以上配置及状态判断是否需要执行持久化操作,判断依据就是看lastsave、dirty是否满足saveparams数组中的其中一个条件。如果有一个条件匹配,则调用rdbSaveBackground方法,执行异步持久化流程。

rdbSaveBackground

rdbSaveBackground是RDB持久化的辅助性方法,根据调用方(父进程或者子进程)不同,有两种不同的执行逻辑。如果调用方是父进程,则fork出子进程,保存子进程信息后直接返回。如果调用方是子进程则调用rdbSave执行RDB持久化逻辑,持久化完成后退出子进程。

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;

    if (hasActiveChildProcess()) return C_ERR;

    server.dirty_before_bgsave = server.dirty;
    server.lastbgsave_try = time(NULL);

    // fork子进程
    if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
        int retval;

        /* Child */
        redisSetProcTitle("redis-rdb-bgsave");
        redisSetCpuAffinity(server.bgsave_cpulist);
        // 执行rdb持久化
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
            sendChildCOWInfo(CHILD_TYPE_RDB, 1, "RDB");
        }
        // 持久化完成后,退出子进程
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* Parent 父进程:记录fork子进程的时间等信息*/
        if (childpid == -1) {
            server.lastbgsave_status = C_ERR;
            serverLog(LL_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return C_ERR;
        }
        serverLog(LL_NOTICE,"Background saving started by pid %ld",(long) childpid);
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        return C_OK;
    }
    return C_OK; /* unreached */
}

rdbSave是真正执行持久化的方法,它在执行时存在大量的I/O、计算操作,耗时、CPU占用较大,在Redis的单线程模型中持久化过程会持续占用线程资源,进而导致Redis无法提供其他服务。为了解决这一问题Redis在rdbSaveBackground中fork出子进程,由子进程完成持久化工作,避免了占用父进程过多的资源。

需要注意的是,如果父进程内存占用过大,fork过程会比较耗时,在这个过程中父进程无法对外提供服务;另外,需要综合考虑计算机内存使用量,fork子进程后会占用双倍的内存资源,需要确保内存够用。通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork以操作的耗时。

rdbSave

Redis的rdbSave函数是真正进行RDB持久化的函数,流程、细节贼多,整体流程可以总结为:创建并打开临时文件、Redis内存数据写入临时文件、临时文件写入磁盘、临时文件重命名为正式RDB文件、更新持久化状态信息(dirty、lastsave)。其中“Redis内存数据写入临时文件”最为核心和复杂,写入过程直接体现了RDB文件的文件格式,本着一图胜千言的理念,我按照源码流程绘制了下图。 jam2i2I.png!mobile 补充说明一下,上图右下角“遍历当前数据库的键值对并写入”这个环节会根据不同类型的Redis数据类型及底层数据结构采用不同的格式写入到RDB文件中,不再展开了。我觉得大家对整个过程有个直观的理解就好,这对于我们理解Redis内部的运作机制大有裨益。

数据恢复

数据恢复是自动执行的,我们将备份文件 (例如:dump.rdb) 移动到Redis备份文件目录并启动服务即可,Redis就会自动加载文件数据至内存。Redis 服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

这里备份文件名称及目录需要与redis.conf中的配置信息保持一致。

RDB优缺点

了解了RDB的工作原理后,对于RDB的优缺点就比较容易总结了。先来看下RDB的优点:

  • RDB是一个紧凑压缩的二进制文件,代表Redis在某一个时间点上的数据快照,非常适合用于备份、全量复制等场景。

  • RDB对灾难恢复、数据迁移非常友好,RDB文件可以转移至任何需要的地方并重新加载。

  • RDB是Redis数据的内存快照,数据恢复速度较快,相比于AOF的命令重放有着更高的性能。

事物总是有两面性的,RDB优点明显,同样也存在缺点:

  • RDB方式无法做到实时或秒级持久化。因为持久化过程是通过fork子进程后由子进程完成的,子进程的内存只是在fork操作那一时刻父进程的数据快照。而fork操作是一个耗时操作,无法做到实时性。

  • RDB持久化过程中的fork操作,会导致内存占用加倍,而且父进程数据越多,fork过程越长。

  • RDB文件有文件格式要求,不同版本的Redis会对文件格式进行调整,存在老版本无法兼容新版本的问题。

参考文献

  • Redis官方文档:《Redis Persistence》:https://redis.io/topics/persistence

  • 《Redis RDB Persistence Details》:https://programming.vip/docs/redis-rdb-persistence-details.html

  • 一文看懂Redis的持久化原理:https://programming.vip/docs/redis-rdb-persistence-details.html

  • Redis详解(六)------ RDB 持久化:https://www.cnblogs.com/ysocean/p/9114268.html#_label2

BZZRz2.png!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK