通常是因为某时刻应用程序大量请求内存导致系统内存不足造成的,这通常会触发 Linux 内核里的 Out of Memory (OOM) killer,OOM killer 会杀掉某个进程(用户态进程,不是内核线程)以腾出内存留给系统用,不致于让系统立刻崩溃。

overcommit

Linux 内核根据应用程序的要求分配内存,通常来说应用程序分配了内存但是并没有实际全部使用,为了提高性能,这部分没用的内存可以留作它用,这部分内存是属于每个进程的,内核直接回收利用的话比较麻烦,所以内核采用一种过度分配内存(over-commit memory)的办法来间接利用这部分 “空闲” 的内存,提高整体内存的使用效率。一般来说这样做没有问题,但当大多数应用程序都消耗完自己的内存的时候麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。

/proc/sys/vm/overcommit_memory 取值为[0-2],默认值为0:  
  
0: 启发式过度使用处理,显而易见的过度使用地址空间被拒绝。它在允许的情况下确保 严重分配失败、过度使用以减少交换使用。  
1: 始终过度使用内存,表示kernel永远不会检查是否有足够的内存可用,总是返回true.  
2: 禁止过度使用,表示kernel拒绝 >= 可用的swap+物理内存 * overcommit_ratio(默认为50)的内存分配请求.  
在大多数情况下,这意味着访问页面时不会终止进程,但会在适当时收到内存分配错误。

#16 GB Swap, 16 GB RAM, overcommit_memory=2 内存请求上限及计算方法
# free
              total        used        free      shared  buff/cache   available
Mem:       16311328     6048244      573316       42992     9689768     8963032
Swap:      16601084     3580760    13020324

# 计算方法
cat /proc/sys/vm/overcommit_ratio #默认50
Mem * overcommit_ratio (50%) + swap= 8155664 + 16601084 = 24756748 kB

16G+16G*50%/100=24G (overcommit_memory = 2)
若修改vm.overcommit_raito为100,则请求内存16G+16G*100%/100=32G

# grep -i commit /proc/meminfo
CommitLimit:    24756748 kB
Committed_AS:   14178044 kB

cat << EOF >> /etc/sysctl.conf
vm.overcommit_memory=1 #redis
vm.overcommit_ratio=50 # 默认
EOF
sysctl -p

oom killer

查看oom killer 日志,最常见的就是MySQL 无缘无故挂掉,Out of memory: Kill process信息:

grep -i "kill" /var/log/messages #CentOS
#grep -i "kill" /var/log/kern.log #Ubuntu
...
Out of memory: Kill process 9682 (mysqld) score 9 or sacrifice child
Killed process 9682, UID 27, (mysqld) total-vm:47388kB, anon-rss:3744kB, file-rss:80kB
httpd invoked oom-killer: gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0
httpd cpuset=/ mems_allowed=0
Pid: 8911, comm: httpd Not tainted 2.6.32-279.1.1.el6.i686 #1
...

内核检测到系统内存不足、挑选并杀掉某个进程的过程可以参考内核源代码 linux/mm/oom_kill.c,该函数会计算每个进程的点数(0~1000)。点数越高,这个进程越有可能被杀死。每个进程的点数跟oom_score_adj有关,而且oom_score_adj可以被设置(-1000最低,1000最高)。

out_of_memory() 被触发,然后调用 select_bad_process() 选择一个 “bad” 进程杀掉,挑选的过程由 oom_badness() 决定,挑选的算法和想法都很简单很朴实:最 bad 的那个进程就是那个最占用内存的进程。

/**
 * oom_badness - heuristic function to determine which candidate task to kill
 * @p: task struct of which task we should calculate
 * @totalpages: total present RAM allowed for page allocation
 *
 * The heuristic for determining which task to kill is made to be as simple and
 * predictable as possible.  The goal is to return the highest value for the
 * task consuming the most memory to avoid subsequent oom failures.
 */
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
              const nodemask_t *nodemask, unsigned long totalpages)
{
    long points;
    long adj;

    if (oom_unkillable_task(p, memcg, nodemask))
        return 0;

    p = find_lock_task_mm(p);
    if (!p)
        return 0;

    adj = (long)p->signal->oom_score_adj;
    if (adj == OOM_SCORE_ADJ_MIN) {
        task_unlock(p);
        return 0;
    }

    /*
     * The baseline for the badness score is the proportion of RAM that each
     * task's rss, pagetable and swap space use.
     */
    points = get_mm_rss(p->mm) + p->mm->nr_ptes +
         get_mm_counter(p->mm, MM_SWAPENTS);
    task_unlock(p);

    /*
     * Root processes get 3% bonus, just like the __vm_enough_memory()
     * implementation used by LSMs.
     */
    if (has_capability_noaudit(p, CAP_SYS_ADMIN))
        adj -= 30;

    /* Normalize to oom_score_adj units */
    adj *= totalpages / 1000;
    points += adj;

    /*
     * Never return 0 for an eligible task regardless of the root bonus and
     * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
     */
    return points > 0 ? points : 1;
}

上面代码里的注释写的很明白,理解了这个算法我们就理解了为啥 MySQL 躺着也能中枪了,因为它的体积总是最大(一般来说它在系统上占用内存最多),所以如果 Out of Memeory (OOM) 的话总是不幸第一个被 kill 掉。解决这个问题最简单的办法就是增加内存,或者想办法优化 MySQL 使其占用更少的内存,除了优化 MySQL 外还可以优化系统,让系统尽可能使用少的内存以便应用程序(如 MySQL) 能使用更多的内存,还有一个临时的办法就是调整内核参数,让 MySQL 进程不容易被 OOM killer 发现。

找出最有可能被 OOM Killer 杀掉的进程

# cat /data/shell/oomscore.sh 
#!/bin/bash
for proc in $(find /proc -maxdepth 1 -regex '/proc/[0-9]+'); do
    printf "%2d %5d %s\n" \
        "$(cat $proc/oom_score)" \
        "$(basename $proc)" \
        "$(cat $proc/cmdline | tr '\0' ' ' | head -c 50)"
done 2>/dev/null | sort -nr | head -n 10

调整oom_adj

/proc/<pid>/oom_adj ​值范围是[-17, 15],oom_score 值越高越容易被oom kill掉。设为 -17则该进程禁用 oom_killer。

比如查看进程号为187418的 omm_score,这个分数被上面提到的 omm_score_adj 参数调整后(-15),就变成了3:

pidof mysqld
187418

# cat /proc/187418/oom_score
18

# echo -15 > /proc/187418/oom_score_adj
# cat /proc/981/oom_score
3

配置oom killer

我们可以通过一些内核参数来调整 OOM killer 的行为,避免系统在那里不停的杀进程。比如我们可以在触发 OOM 后立刻触发 kernel panic,kernel panic 10秒后自动重启系统。

修改panic_on_oom值为1,表示请求内存不足时10秒后重启系统
cat << EOF >> /etc/sysctl.conf
vm.panic_on_oom=1
kernel.panic=10 # 表示10s后重启
EOF
sysctl -p

内核参数

/proc/sys/vm/panic_on_oom 取值为[0-2],默认值为0:

0: OOM时系统执行OOM Killer
1: OOM时系统会panic(恐慌)
2: OOM时系统一定会触发panic(恐慌)

/proc/sys/vm/oom_kill_allocating_task 取值为[0-1],默认值为0:

0: 内核将检查每个进程的分数,分数最高的进程将被kill掉
1: 那么内核将kill掉当前申请内存的进程

References

最后修改:2024 年 12 月 13 日
如果觉得我的文章对你有用,请随意赞赏