一、背景说明

在测试过程中遇到一个Java进程由于申请内存过大导致被Linux OOM killer杀掉的问题,所以来分析一下Linux中的CommitLimit与OMM killer机制。

二、Linux内存分配机制

  Linux系统允许程序申请比系统可用内存更多的内存空间,这个特性叫做 overcommit 特性,这样做可能是为了系统的优化,因为不是所有的程序申请了内存就会立刻使用,当真正的使用时,系统可能已经回收了一些内存。为了避免内存的浪费,在分配页面时,Linux 采用的是按需分配物理页面的方式。譬如说,某个进程调用malloc()申请了一块小内存,这时内核会分配一个虚拟页面,但这个页面不会映射到实际的物理页面。
OS内存分配
从图中可以看到,当程序首次访问这个虚拟页面时,会触发一个缺页异常 (page fault)。这时内核会分配一个物理页面,让虚拟页面映射到这个物理页面,同时更新进程的页表 (page table)。

2.1 Linux的Memory Overcommit

这种按需分配物理页面的方式,可以大大节省物理内存的使用,但有时会导致 Memory Overcommit。所谓 Memory Overcommit,也就是说,所有进程使用的虚拟内存超过了系统的物理内存和交换空间的总和。默认情况下,Linux 是允许 Memory Overcommit 的。并且在大多数情况下,Memory Overcommit 也是安全的,因为很多进程只是申请了很多内存,但实际使用到的内存并不多。

但万一很多进程都使用了申请来的大部分内存,就可能导致物理内存和交换空间不够用了,这时内核的 OOM Killer 就会出马,它会选择杀掉一个或多个进程,这样就能腾出一些内存给其它进程使用。

Linux设计了一个OOM killer机制(out of memory killer)来处理这种危机:挑选一个进程出来杀死,以腾出部分内存,如果还不够就继续杀…也可通过设置内核参数 vm.panic_on_oom 使得发生OOM时自动重启系统。

Linux 2.6之后允许通过内核参数 vm.overcommit_memory 禁止memory overcommit。
内核参数 vm.overcommit_memory 接受三种取值:

取值 说明
0 Heuristic overcommit handling. 这是缺省值,它允许overcommit,但过于明目张胆的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存。
1 Always overcommit. 允许overcommit,对内存申请来者不拒
2 Don’t overcommit. 禁止overcommit。

如何才算 overcommit?

Linux 设定了一个阈值,叫做 CommitLimit,如果所有进程申请的总内存超过了 CommitLimit,那就算是 overcommit 了。在/proc/meminfo中可以看到 CommitLimit 的大小:

$ grep -i commit /proc/meminfo
CommitLimit:    32905144 kB
Committed_AS:   30462700 kB
  • CommitLimit : overcommit的阈值
  • Committed_AS : 表示所有进程已经申请的内存总大小

注意 Committed_AS是已经申请的,不是已经分配的,如果 Committed_AS 超过 CommitLimit 就表示发生了 overcommit,超出越多表示 overcommit 越严重。

CommitLimit计算方式

CommitLimit = [swap size] + [RAM size] * vm.overcommit_ratio / 100
  • [swap size] : 交换区空间大小
  • [RAM size] : 内存空间大小
  • vm.overcommit_ratio : vm.overcommit_ratio 是内核参数,缺省值是50,表示物理内存的50%
$ free
              total        used        free      shared  buff/cache   available
Mem:       32780152    16704624     2104672     1442992    13970856    14124620
Swap:      16515068      127488    16387580

CommitLimit = 16515068 + 32780152 * 50 / 100 = 32905144

2.2 Linux OOM Killer

当物理内存严重不足时,Linux内核调用OOM Killer来检查所有正在运行的进程并杀死其中一个或多个进程,以释放系统内存并保持系统运行。

OOM Killer如何选择进程?

Linux内核给每个正在运行的进程评分oom_score,该评分显示了在可用内存不足的情况下终止该进程的可能性。

oom_score = 10 x [percent of memory used by process]

oom_score最大得分为100% x 10 = 1000 。此外,如果进程以root用户身份运行,则与普通用户进程使用相同的内存相比,该进程的oom_score略低。

Linux 的每个进程都有一个oom_score (位于/proc/$pid/oom_score),这个值越大,就越有可能被 OOM Killer 选中。
这个值是系统综合进程的内存消耗量、CPU时间(utime + stime)、存活时间(uptime - start time)和oom_adj计算出的,消耗内存越多分越高,存活时间越长分越低。

如何避免进程被OOM Killer杀死?

oom_score_adj

OOM killer检查oom_score_adj以调整其最终计算出的分数。该文件位于/proc/$pid/oom_score_adj中,所以我们可以通过在这个文件中给一个大的负数,以降低该进程被选中并终止的可能性。oom_score_adj取值范围在-1000到1000之间。如果你给了-1000,进程即使使用了100%的内存也不会被OOM Killer杀死。

修改pid为42的进程的oom_score_adj的方法:

sudo echo -200 > /proc/42/oom_score_adj

需要以root用户或sudo的身份执行此操作,因为Linux不允许普通用户降低OOM分数。您可以在没有任何特殊权限的情况下以普通用户身份提高OOM分数。例如:

echo 100> /proc/42/oom_score_adj

oom_adj

另外还有一个较细粒度的分数,称为oom_adj,该文件位于/proc/$pid/oom_adj,范围从-16到15。与oom_score_adj类似。 实际上,设置oom_score_adj时,内核会自动将其按比例缩小并计算oom_adj。
当oom_adj的魔术值为-17,指示给定的进程永远不能被OOM杀手杀死。

显示所有正在运行的进程的OOM分数

该脚本以OOM分数的降序显示所有正在运行的进程的OOM分数和OOM调整后的分数:

#!/bin/bash
# Displays running processes in descending order of OOM score
printf 'PID\tOOM Score\tOOM Adj\tCommand\n'
while read -r pid comm; do [ -f /proc/$pid/oom_score ] && [ $(cat /proc/$pid/oom_score) != 0 ] && printf '%d\t%d\t\t%d\t%s\n' "$pid" "$(cat /proc/$pid/oom_score)" "$(cat /proc/$pid/oom_score_adj)" "$comm"; done < <(ps -e -o pid= -o comm=) | sort -k 2nr

检查进程是否已被OOM杀死

 $ egrep -i 'killed process' /var/log/messages
Nov 19 16:24:31 localhost kernel: Killed process 26980 (java) total-vm:14316628kB, anon-rss:6138236kB, file-rss:0kB, shmem-rss:0kB

参考文章

理解LINUX的MEMORY OVERCOMMIT
Linux 的 OOM Killer 机制分析
内存管理
Surviving the Linux OOM Killer