谈谈Overcommit_memory

不要问我为何眼中常含着泪水,因为我是代码狗。。

1) 缘起

春节前几天本是开始吃着火锅唱着歌的时候。运维大侠说要扩容 Redis 从库,扩容本来就是家常便饭,x 了狗的是同步一直无法成功。看日志发现在做 bgsave 的时候一直失败。 日志如下:

[41738] 04 Feb 11:16:39.859 * Full resync requested by slave.
[41738] 04 Feb 11:16:39.859 * Starting BGSAVE for SYNC
[41738] 04 Feb 11:16:39.860 # Can't save in background: fork: Cannot allocate memory
[41738] 04 Feb 11:16:39.860 * Replication failed, can't BGSAVE

从日志来看原因也比较清楚,就是 bgsave 在做 fork 的时候内存分配失败,应该就是可用内存不足吧。

使用 info memory 看了一下实例使用内存, 差不多用了 8G:

used_memory:8045067888
used_memory_human:7.49G
used_memory_rss:8216522752
used_memory_peak:50615269608
used_memory_peak_human:47.14G

然后用 free -m 看到系统空闲页加上 pagecache 也有 21G,这个空闲内存远大于实例使用的 8G 呀,为什么会 fork 失败呢? 你妈炸了。

使用 top 发现虚拟内存使用了 48.7G, 常驻内存使用是 7.6G。

img

2) 那么问题来了

Q1. Redis 统计的虚拟内存为什么占用这么多?

从现象来看是峰值分配了这么多(见 peak_memory)。 但在内存释放的时候,物理内存释放而虚拟内存无法收缩。这个跟内存分配有关, 当前 Redis 默认是用的是 jemalloc。

在 github 上面提了一个 issue 说这个问题, Redis 作者也大概是这个意思。至于为什么虚拟内存无法收缩的原因,有待进一步研究。

Q2. fork 是根据虚拟内存来检查内存是否够用?

显然是的。因为如果是根据物理内存,fork 是可以成功的。那其实剩余的系统内存是足够的,而我需要的内存并没有这么大,有什么办法可以让进程继续 fork 么? 答案就是把 vm.overcommit_memory 设置为 1。

3) 解决方案

vm.overcommit_memory 用来控制在 fork 进程时用什么姿势来检查内存是否够用。 Redis 在实例启动的时候给出了提示信息。

WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. 
To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf 
and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.

vm.overcommit_memory 取值是 0, 1, 2, 默认是 0。 具体数值的意义可参考下面的文档,后面会结合代码来说明。

https://www.kernel.org/doc/Documentation/vm/overcommit-accounting

4) Overcommit memory

我们具体从内核代码(Linux-2.6.32)来看这个参数如何在 fork 进程的时候进行内存校验。

#define OVERCOMMIT_GUESS        0
#define OVERCOMMIT_ALWAYS       1
#define OVERCOMMIT_NEVER        2

我们在程序执行 fork() 的时候,会通过系统调用中断切换到内核态,再调用 sys_fork。

下面是 fork 进程时,内核的系统调用链:

sys_fork -> copy_mm -> dup_mm -> dup_mmap -> 
security_vm_enough_memory -> cap_vm_enough_memory -> __vm_enough_memory

最后调用 __vm_enough_memory 进行内存检查,我们重点来看这个函数。

int __vm_enough_memory(struct mm_struct *mm, long pages, int cap_sys_admin) {
    ...
    /*    
     * Sometimes we want to use more memory than we have
     */
    // OVERCOMMIT_ALWAYS = 1, 什么都不检查直接返回
    if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
        return 0;
    if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
        unsigned long n;

        // 剩余容量 = page cache使用的页 + 空闲swap + 可回收 slab + 系统空闲页
        free = global_page_state(NR_FILE_PAGES);
        free += nr_swap_pages;
        free += global_page_state(NR_SLAB_RECLAIMABLE);

        // root 如果是非 root 用户需要保留 3%
        if (!cap_sys_admin)
            free -= free / 32;

        // 如果空闲的页足够直接返回
        if (free > pages)
            return 0;

        // 计算系统空闲页比较耗时,所有上面 3 种空闲已经足够就不计算。
        n = nr_free_pages();

        // 去掉一些系统保留页
        if (n <= totalreserve_pages)
            goto error;
        else
            n -= totalreserve_pages;

        // root 如果是非 root 用户需要保留 3%
        if (!cap_sys_admin)
            n -= n / 32;
        free += n;

        if (free > pages)
            return 0;

        goto error;
    }

    // 上面两个分支分别是值为 1 和 0 的情况,下面则是值为 2 的判断条件
    // 因为我们计算的是 normal page, 所以计算允许使用的内存需要扣掉 huge page
    // sysctl_overcommit_ratio 系统控制的比例参数
    allowed = (totalram_pages - hugetlb_total_pages())
            * sysctl_overcommit_ratio / 100;

    //非root 用户保留 3%
    if (!cap_sys_admin)
        allowed -= allowed / 32;
    // 加上 swap 空闲页
    allowed += total_swap_pages;

    // 防止单进程占用过多内存,需要保留 3% 给其他进程
    if (mm)
        allowed -= mm->total_vm / 32;

    if (percpu_counter_read_positive(&vm_committed_as) < allowed)
        return 0;
error:
    vm_unacct_memory(pages);

    return -ENOMEM;
}

从代码层面来看:

  1. overcommit_memory = 1, 不检查,有锅自己背。
  2. overcommit_memory = 0,检查当前进程需要的虚拟内存 < (当前剩余的物理 + swap分区)
  3. overcommit_memory = 2, 检查整个系统已经分配的内存 < (物理内存*允许比例 + swap分区)

5) END

调整系统参数还是需要谨慎再谨慎,在没有详细查看官方文档以及全面了解参数对系统的影响的时候,切勿手贱随意调整。。。