redis源代码分析7–内存(下)

上一节提到的used_memory变量保存了redis当前所使用的内存。其值常用来跟server.vm_max_memory、server.maxmemory进行比较。vm_max_memory表示redis vm启动swap的内存阈值,在超过该值后应启动vm的swap操作;maxmemory表示redis允许分配的最大内存,在超过该值后应进行内存的释放。这些比较主要在rdbLoad、loadAppendOnlyFile、serverCron、processCommand、vmThreadedIOCompletedJob等函数中。值得注意的是,尽管redis会尽量将内存使用量降低到server.maxmemory(甚至server.vm_max_memory)之下,但并不对此保证。

接下来的一节我们分析下rdbLoad、loadAppendOnlyFile、serverCron、processCommand、vmThreadedIOCompletedJob的内存检查策略。

在rdbLoad、loadAppendOnlyFile(分别代表以快照、aof方式进行数据的持久化后db的加载)中会检查vm_max_memory。超过vm_max_memory后,会先调用vmSwapOneObjectBlocking swap值到vm中,若一直到vmSwapOneObjectBlocking返回出错时,内存使用量还是超过vm_max_memory,则置swap_all_values为1,这样后面加载的数据都直接使用vmSwapObjectBlocking被swap到vm中,至于vmSwapOneObjectBlocking、vmSwapObjectBlocking怎么实现的(二者都是阻塞方式),我们在vm章节中再做详细分析。当然,在这两个函数中,对vm_max_memory的比较有所放宽,也就是只有比vm_max_memory多32M字节时,才进行swap的操作。从这里也可以看出,加载db时并不和server.maxmemory进行比较。此时,若超过最大内存限制,redis此时不管,也许加载时直接down掉(超过可用内存),或者等到加载完后运行到后面介绍的释放过程再进行释放。当然,检查vm_max_memory,并调用vmSwapOneObjectBlocking等函数是否起作用,还要看是否开启vm机制(server.vm_enabled)。

static int rdbLoad(char *filename) {
    ---
    int swap_all_values = 0;
    ---
    while(1) {
        robj *key, *val;
        int force_swapout;
        ---
        /* Handle swapping while loading big datasets when VM is on */

        /* If we detecter we are hopeless about fitting something in memory
         * we just swap every new key on disk. Directly...
         * Note that's important to check for this condition before resorting
         * to random sampling, otherwise we may try to swap already
         * swapped keys. */
        if (swap_all_values) {
            dictEntry *de = dictFind(d,key);

            /* de may be NULL since the key already expired */
            if (de) {
                key = dictGetEntryKey(de);
                val = dictGetEntryVal(de);

                if (val->refcount != 1) continue;

                /* Unshare the key if needed */
                if (key->refcount != 1) {
                    robj *newkey = dupStringObject(key);
                    decrRefCount(key);
                    key = dictGetEntryKey(de) = newkey;
                }

                if (vmSwapObjectBlocking(key,val) == REDIS_OK)
                    dictGetEntryVal(de) = NULL;
            }
            continue;
        }

        /* Flush data on disk once 32 MB of additional RAM are used... */
        force_swapout = 0;
        if ((zmalloc_used_memory() - server.vm_max_memory) > 1024*1024*32)
            force_swapout = 1;

        /* If we have still some hope of having some value fitting memory
         * then we try random sampling. */
        if (!swap_all_values && server.vm_enabled && force_swapout) {
            while (zmalloc_used_memory() > server.vm_max_memory) {
                if (vmSwapOneObjectBlocking() == REDIS_ERR) break;
            }
            if (zmalloc_used_memory() > server.vm_max_memory)
                swap_all_values = 1; /* We are already using too much mem */
        }
    }
    ---
}

int loadAppendOnlyFile(char *filename) {
        ---
       while(1) {

       force_swapout = 0;
        if ((zmalloc_used_memory() - server.vm_max_memory) > 1024*1024*32)
            force_swapout = 1;

        if (server.vm_enabled && force_swapout) {
            while (zmalloc_used_memory() > server.vm_max_memory) {
                if (vmSwapOneObjectBlocking() == REDIS_ERR) break;
            }
        }
     }
     ---
}

接着看看serverCron中的处理策略。

serverCron是redis中的定时循环函数(100ms循环一次),serverCron是先使用tryFreeOneObjectFromFreelist释放些内存,只有在释放内存还是不够时才启用vm的swap操作,并根据是否启用多线程swap调用vmSwapOneObjectBlocking(阻塞方式) 或者vmSwapOneObjectThreaded(多线程)。另外,如果是多线程方式,则只swap一个object(并不是立即swap),因为在此方式下,会将swap的操作作为一个job,插入到工作线程中,而当该工作线程完成后,会自动调用vmThreadedIOCompletedJob,而在vmThreadedIOCompletedJob中,也有内存大小检查的操作(内存大小超过阈值时,也是调用vmSwapOneObjectThreaded)。可以看到,如果serverCron中的这段代码中的retval == REDIS_ERR的话,则一段时间内无法保证使用的内存在指定的范围之内。(swap的策略在后面VM章节中介绍。)

static int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
     ---
     /* Swap a few keys on disk if we are over the memory limit and VM
     * is enbled. Try to free objects from the free list first. */
    if (vmCanSwapOut()) {
        while (server.vm_enabled && zmalloc_used_memory() >
                server.vm_max_memory)
        {
            int retval;

            if (tryFreeOneObjectFromFreelist() == REDIS_OK) continue;
            retval = (server.vm_max_threads == 0) ?
                        vmSwapOneObjectBlocking() :
                        vmSwapOneObjectThreaded();
            if (retval == REDIS_ERR && !(loops % 300) &&
                zmalloc_used_memory() >
                (server.vm_max_memory+server.vm_max_memory/10))
            {
                redisLog(REDIS_WARNING,"WARNING: vm-max-memory limit exceeded by more than 10%% but unable to swap more objects out!");
            }
            /* Note that when using threade I/O we free just one object,
             * because anyway when the I/O thread in charge to swap this
             * object out will finish, the handler of completed jobs
             * will try to swap more objects if we are still out of memory. */
            if (retval == REDIS_ERR || server.vm_max_threads > 0) break;
        }
    }
    ---
}

而在处理客户端命令的核心函数processCommand中,在超过内存阈值maxmemory时,会先调用freeMemoryIfNeeded释放一些内存;在释放内存后若还是超过了设置的内存大小,则在客户端命令设置了REDIS_CMD_DENYOOM参数时返回内存出错信息, 否则还是会正常处理。

static int processCommand(redisClient *c) {
    ---
    if (server.maxmemory) freeMemoryIfNeeded();
    if (server.maxmemory && (cmd->flags & REDIS_CMD_DENYOOM) &&
        zmalloc_used_memory() > server.maxmemory)
    {
        addReplySds(c,sdsnew("-ERR command not allowed when used memory > 'maxmemory'\r\n"));
        resetClient(c);
        return 1;
    }
    ----
}

我们先看一下 REDIS_CMD_DENYOOM参数。该参数其实并不是由客户端设置的,而是在redis解析客户端请求的命令后,取得该命令所对应的结构体,而该结构体早已设置好了该参数。全局命令表cmdTable中保存了所有命令的相关命令字机器处理函数和相关参数。可以看到,对于set等可能会增加内存使用的命令,一律设置了REDIS_CMD_DENYOOM参数,而get等则没有。

static struct redisCommand cmdTable[] = {
    {"get",getCommand,2,REDIS_CMD_INLINE,NULL,1,1,1},
    {"set",setCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM,NULL,0,0,0},
    {"setnx",setnxCommand,3,REDIS_CMD_BULK|REDIS_CMD_DENYOOM,NULL,0,0,0},
    {"setex",setexCommand,4,REDIS_CMD_BULK|REDIS_CMD_DENYOOM,NULL,0,0,0},
    ---
}

再来看看 freeMemoryIfNeeded是怎么释放内存的。释放时先调用tryFreeOneObjectFromFreelist释放内存,在内存仍不够时,会试图释放带 expire标记的key。对于每个db中的expire dict,每次会随机选择3个key,并删除会最先expire的key(此时就很可能丢失带expire标记的数据了)。

static void freeMemoryIfNeeded(void) {
    while (server.maxmemory && zmalloc_used_memory() > server.maxmemory) {
        int j, k, freed = 0;

        if (tryFreeOneObjectFromFreelist() == REDIS_OK) continue;
        for (j = 0; j < server.dbnum; j++) {
            int minttl = -1;
            robj *minkey = NULL;
            struct dictEntry *de;

            if (dictSize(server.db[j].expires)) {
                freed = 1;
                /* From a sample of three keys drop the one nearest to
                 * the natural expire */
                for (k = 0; k < 3; k++) {
                    time_t t;

                    de = dictGetRandomKey(server.db[j].expires);
                    t = (time_t) dictGetEntryVal(de);
                    if (minttl == -1 || t < minttl) {
                        minkey = dictGetEntryKey(de);
                        minttl = t;
                    }
                }
                deleteKey(server.db+j,minkey);
                server.stat_expiredkeys++;
            }
        }
        if (!freed) return; /* nothing to free... */
    }
}

最后我们来看看tryFreeOneObjectFromFreelist函数。redis会将系统中的无效list node(即该node已解除对其内部value的引用)放到server.objfreelist链表中,平时如果需要list node,可直接从该list中获得一个,但此刻因为内存不够,该释放它们了。

static int tryFreeOneObjectFromFreelist(void) {
    robj *o;

    if (server.vm_enabled) pthread_mutex_lock(&server.obj_freelist_mutex);
    if (listLength(server.objfreelist)) {
        listNode *head = listFirst(server.objfreelist);
        o = listNodeValue(head);
        listDelNode(server.objfreelist,head);
        if (server.vm_enabled) pthread_mutex_unlock(&server.obj_freelist_mutex);
        zfree(o);
        return REDIS_OK;
    } else {
        if (server.vm_enabled) pthread_mutex_unlock(&server.obj_freelist_mutex);
        return REDIS_ERR;
    }
}

前面的过程搞清后,就可以回答一个问题了。
redis不开启VM时,内存超过maxmemory设置后,是怎么处理的?

不开启VM,redis并不保证内存使用一定低于maxmemory,只是会尽可能释放。

先看client,对于有些会增加内存使用的命令,比如set,此时会返回出错信息。

释放策略是:因为redis会保存先前已不再使用的object,也就是一个object链表,平时这个链表的作用使得redis可以直接从上面取得一个object,不需要使用zmalloc分配。

当内存超过阈值时,这个链表就会首先被释放了。

若还是超过内存阈值,redis对于每个db,会随机选择3个带expire标记的key, 并释放最先expire的key及其val。

但如果此后还是超过内存阈值(把所有带expire标记的都释放后),我想redis是没办法了。

尽管如此,redis使用的内存>设置的maxmemory,只会出现在一开始加载的数据就超过maxmemroy。这样的话,client调用set等命令会一直返回出错信息。

此条目发表在 redis 分类目录。将固定链接加入收藏夹。

redis源代码分析7–内存(下)》有 2 条评论

  1. lisafang 说:

    代码贴的真是乱七八糟啊

    • peter 说:

      不好意思啊!昨天朋友的服务器到期了,都忘记备份了,现在从搜索引擎的快照中拷数据了! 马上完善。。。。。。

发表评论

电子邮件地址不会被公开。 必填项已被标记为 *

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>