暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Redis的持久化​解析之RDB

万照 2021-06-29
1664
最近数据库团队在做自建机房Redis的方案,如果我手头事情能可以往后放,就和团队的同学参与到云厂商Redis的方案讨论中去。我有个想法想去细致的梳理Redis的知识,根据这几年的Redis经验,从运维和源码两个方面去展示Redis数据库的功能。

1、为什么需要持久化?

Redis的数据是存放在服务器的内存中,试想Redis的进程异常退出,数据不会写入到磁盘就丢失掉,为了解决这个问题,Redis提供几种选项进行持久化,包括RDB快照,AOF文件,混合RDB和AOF。无论哪种方式,Redis主进程都调用linux库函数fork(),创建一个子进程来进行持久化操作,然后主进程就离开去处理其他请求,详见上图。


2、如何触发RDB持久化?

  • 自动触发

    配置文件"3600 1 300 100 60 10000":3600内至少有1个key变更,300秒内至少有100个key变更,60秒内至少有1000个key变更,满足任意条件动触发rdbSaveBackground。

请问如何关闭save参数功能?

可以设置 config set save ""关闭自动触发bgsave的功能。

  • 手动触发

           save命令:saveCommand(),主进程会阻塞其他请求来处理数据写入磁盘操作。

    bgsave命令:bgsaveCommand(),主进程会fork一个子进程来单独处理数据写入磁盘操作。


3、理解RDB持久化的流程

  • RDB触发和执行流程图

  • GDB调试执行bgsave命令的堆栈

redis-cli -p 6379 bgsave #手动执行bgsave

    Thread 2 hit Breakpoint 1, rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:1333
    1333 if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    #0 rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:1333
    #1 0x00000001000407b5 in bgsaveCommand (c=0x101809200) at Users/wanzhao/CLionProjects/redis/src/rdb.c:2505
    #2 0x0000000100007818 in call (c=0x101809200, flags=15) at Users/wanzhao/CLionProjects/redis/src/server.c:2479
    #3 0x00000001000086d0 in processCommand (c=0x101809200) at Users/wanzhao/CLionProjects/redis/src/server.c:2781
    • GDB调试执行写入命令触发rdb的堆栈

    redis-benchmark -t set -n 10000 -r 10000 -c 5 #执行写入命令

    21649:M 26 Jun 2021 10:46:00.838 * 100 changes in 300 seconds. Saving...

      Thread 2 hit Breakpoint 1, rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:1333
      1333 if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
      #0 rdbSaveBackground (filename=0x100404290 "dump.rdb", rsi=0x0) at Users/wanzhao/CLionProjects/redis/src/rdb.c:1333
      #1 0x0000000100004323 in serverCron (eventLoop=0x1004334c0, id=0, clientData=0x0) at Users/wanzhao/CLionProjects/redis/src/server.c:1296
      #2 0x000000010000e9e0 in processTimeEvents (eventLoop=0x1004334c0) at Users/wanzhao/CLionProjects/redis/src/ae.c:331
      #3 0x000000010000e58a in aeProcessEvents (eventLoop=0x1004334c0, flags=11) at Users/wanzhao/CLionProjects/redis/src/ae.c:469

      4、阅读源码来解析RDB的持久化和加载过程

      这里直接给出rdbSaveBackground的代码,因为手动和自动触发rdb持久化都调用该函数。

        //后台线程执行rdb的核心函数
        int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
        pid_t childpid;
        long long start;


            //如果有正在运行的aof或rdb,返回C_ERR=-1
        if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;


        server.dirty_before_bgsave = server.dirty;
        server.lastbgsave_try = time(NULL);
            //打开子进程的信息管道,进程之间是不共享内存空间的,通过管道来通信
        openChildInfoPipe();


        start = ustime();
        if ((childpid = fork()) == 0) {
                //进入子进程分支处理
        int retval;


        /* Child */
        closeClildUnusedResourceAfterFork();
        redisSetProcTitle("redis-rdb-bgsave");
        retval = rdbSave(filename,rsi);
        if (retval == C_OK) {
                    //计算copy on write是占用的内存大小,当父进程被写入新数据时,触发copy-on-write
        size_t private_dirty = zmalloc_get_private_dirty(-1);


        if (private_dirty) {
        serverLog(LL_NOTICE,
        "RDB: %zu MB of memory used by copy-on-write",
        private_dirty/(1024*1024));
        }


        server.child_info_data.cow_size = private_dirty;
        sendChildInfo(CHILD_INFO_TYPE_RDB);
        }
        exitFromChild((retval == C_OK) ? 0 : 1);
        } else {
        /* Parent */
                //父进程继续处理
                //计算fork所花费的时间
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) {
        closeChildInfoPipe();
        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 %d,%d",childpid,server.stat_fork_rate);
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
                //关闭resize哈希表,因为resize操作会拷贝很多内存
        updateDictResizePolicy();
        return C_OK;
        }
        return C_OK; /* unreached */
        }
          //将rdb文件加载到内存中
          int rdbLoad(char *filename, rdbSaveInfo *rsi) {
          FILE *fp;
          rio rdb;
          int retval;


          //打开文件只读方式
          if ((fp = fopen(filename,"r")) == NULL) return C_ERR;
          //初始化load rdb的实例状态
          startLoading(fp);
          //初始化一个文件流对象并赋值给fp指针
          rioInitWithFile(&rdb,fp);
          //从rio流中加载RDB文件,成功返回C_OK
          retval = rdbLoadRio(&rdb,rsi,0);
          fclose(fp);
          stopLoading();
          return retval;
          }


          5、做RDB持久化会遇到什么问题呢?

          • Background saving terminated by signal N

            1个Redis节点占用10gb内存,手动执行bgsave命令,ps -ef会看到redis-rdb-bgsave进程号19055,然后执行redis-benchmark -p 6379 -c 20 -t set -n 1000000 -d 2000000 -r 10000写入数据,会发现redis-rdb-bgsave进程因为服务器内存不足oom被操作系统kill:

            Redis日志:
            18486:M 27 Jun 2021 11:56:01.138 * Background saving started by pid 19055
            18486:M 27 Jun 2021 11:56:05.939 # Background saving terminated by signal 9
            /var/log/messages日志:
            Jun 27 11:56:04 iZ2zeetwpf5smee40rgm2zZ kernel: Out of memory: Kill process 18486 (redis-server) score 734 or sacrifice child
            Jun 27 11:56:04 iZ2zeetwpf5smee40rgm2zZ kernel: Killed process 19055 (redis-server), UID 0, total-vm:1741308kB, anon-rss:1387532kB, file-rss:396kB, shmem-rss:0kB
            • RDB: 137 MB of memory used by copy-on-write

              1个Redis节点占用10gb内存,执行bgsave命令时fork操作耗时300ms,这个耗时是阻塞的时间,对于请求量高的情况请求会排队。当父进程被写入数据时,kernel会对需要修改的内存页复制出一份副本给父进程来完成写操作,可以看到copy-on-write时占用内存137 MB。

              latest_fork_usec:296251
              rdb_last_cow_size:144547840
              • 为什么Redis启动时提示要关闭大页(huge pages)?

                # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis。

                HugePageSize默认是2M,如果开启大页,copy-on-write期间,当有页面要修改,那么就要拷贝N个2MB的大页面,如果父进程写入量很大,会增加内存复制数量,子进程会消耗更多内存甚至超过used_memory。

              • Redis实例的碎片率mem_fragmentation_ratio竟然会小于1?

                如果启用了swap分区,当服务器内存不足的时候,操作系统把Redis的内存数据交换到磁盘,此时发现used_memory内存竟然大于used_memory_rss,因为磁盘速度太慢了,会严重影响Redis的性能。

              • Can't save in background: fork: Cannot allocate memory


              6、什么场景使用RDB持久化比较好?

              实时持久化不适合使用RDB备份,因为我们通过前文知道执行bgsave需要fork子进程,消耗资源较大,频繁执行成本太高。但是RDB的备份压缩率高,恢复数据时load速度很快,可以用于以下场景比较合适:

              1、对于业务不经常修改的数据,读请求占比写请求要高;

              2、业务使用Redis做为缓存时,当内存达到max_memory数据淘汰后可以接受的场景


              参考文档:

              Redis5配置:https://raw.githubusercontent.com/redis/redis/5.0/redis.conf

              文章转载自万照,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

              评论