大数据量存储引起的Redis相关问题

1.问题


目前公司有个项目需要用Redis来存储一些数据,但是当开发导入数据的时候才知道,这个数据量是相当庞大的,接着就遇到了一些问题。 首先,Redis Master所在的物理机的内存是32GB,24核心的IBM X3530 M4。考虑到安全问题,当时做配置的时候我给Redis配置了20GB的内存,本来想着足够用了,但是后来当keys达到179658571条的时候,20G内存已经写满了,接着Redis就写不进去数据了.

经同事提醒,想起Redis有个vm的配置,于是查阅资料,几个相关的参数如下:


是否使用虚拟内存,默认值为no

vm-enabled yes

虚拟内存文件路径,默认值为/tmp/redis.swap,不可多个Redis实例共享

vm-swap-file /tmp/redis.swap

将所有大于vm-max-memory的数据存入虚拟内存,无论vm-max-memory设置多小,所有索引数据都是内存存储的 (Redis的索引数据就是keys),也就是说,当vm-max-memory设置为0的时候,其实是所value都存在于磁盘。默认值为0。

vm-max-memory 0

于是在配置文件中加上几条配置,但是报错了,居然还是语法错误:


Stopping ...
Waiting for Redis to shutdown ...
Redis stopped.
Starting Redis server...

*** FATAL CONFIG FILE ERROR ***
Reading the configuration file, at line 69
>>>'vm-enabled yes'
Bad directive or wrong number of arguments

后来发现,在新的Redis版本中vm相关的参数已经去掉了,线上server版本为 2.6.17。 无奈查看log,发现报错信息: [15964] 08 Oct 15:25:33.044 # Can’t save in background: fork: Cannot allocate memory

于是,从github上下回2.6.17的源码,开始查找问题出现在哪里。最终找到了上面报错的函数位于rdb.c中:


int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;

    if (server.rdb_child_pid != -1) return REDIS_ERR;

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

    start = ustime();
    if ((childpid = fork()) == 0) {
        int retval;

        /* Child */
        if (server.ipfd > 0) close(server.ipfd);
        if (server.sofd > 0) close(server.sofd);
        retval = rdbSave(filename);
        if (retval == REDIS_OK) {
            size_t private_dirty = zmalloc_get_private_dirty();

            if (private_dirty) {
                redisLog(REDIS_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
        }
        exitFromChild((retval == REDIS_OK) ? 0 : 1);
    } else {
        /* Parent */
        server.stat_fork_time = ustime()-start;
        if (childpid == -1) {
            server.lastbgsave_status = REDIS_ERR;
            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

发现是由于内存不足,父进程无法fork子进程回写数据照成的,这也证实了为什么后来重启Redis丢了2000W数据

同时查看了以前版本Redis的同名函数,代码如下:


int rdbSaveBackground(char *filename) {
    pid_t childpid;
    long long start;

    if (server.bgsavechildpid != -1) return REDIS_ERR;
    if (server.vm_enabled) waitEmptyIOJobsQueue();
    server.dirty_before_bgsave = server.dirty;
    start = ustime();
    if ((childpid = fork()) == 0) {
        /* Child */
        if (server.vm_enabled) vmReopenSwapFile();
        if (server.ipfd > 0) close(server.ipfd);
        if (server.sofd > 0) close(server.sofd);
        if (rdbSave(filename) == REDIS_OK) {
            _exit(0);
        } else {
            _exit(1);
        }
    } else {
        /* Parent */
        server.stat_fork_time = ustime()-start;
        if (childpid == -1) {
            redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return REDIS_ERR;
        }
        redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
        server.bgsavechildpid = childpid;
        updateDictResizePolicy();
        return REDIS_OK;
    }
    return REDIS_OK; /* unreached */
}

很明显,新版本的该函数中已经去掉了 vm_enabled 参数。

2.总结


回到最本质问题,Redis只是对内存中的数据提出了一些持久化的解决方案,如果你要保存的数据特别大,比如上百G,在服务器内存一定的情况下,在大量key没有过期,无法淘汰掉的情况下,系统会考虑使用SWAP,(如果vm.overcommit_memory = 1 会直接放行),由于系统还有9G左右的剩余内存空间,所以并没有启用SWAP,于是Redis出现了一开始的问题。我想Redis去掉了vm特性,也是想把这部分的功能交给底层的系统来做吧。如果SWAP也不够用了呢,这时候估计不仅仅是Redis写不进去数据,就连系统也会出问题了吧。

我始终坚信:Cache就是Cache,我们不可能把所有数据放到内存中,那就尽量把热数据放到内存中吧,这不正是计算机为什么有了内存和磁盘吗,你说呢