理解和配置 Linux 下的 OOM Killer

追尋北極發表於2017-11-23

最近有位 VPS 客戶抱怨 MySQL 無緣無故掛掉,還有位客戶抱怨 VPS 經常當機,登陸到終端看了一下,都是常見的 Out of memory 問題。這通常是因為某時刻應用程式大量請求記憶體導致系統記憶體不足造成的,這通常會觸發 Linux 核心裡的 Out of Memory (OOM) killer,OOM killer 會殺掉某個程式以騰出記憶體留給系統用,不致於讓系統立刻崩潰。如果檢查相關的日誌檔案(/var/log/messages)就會看到下面類似的 Out of memory: Kill process 資訊:

...
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
...
21556 total pagecache pages
21049 pages in swap cache
Swap cache stats: add 12819103, delete 12798054, find 3188096/4634617
Free swap  = 0kB
Total swap = 524280kB
131071 pages RAM
0 pages HighMem
3673 pages reserved
67960 pages shared
124940 pages non-shared

Linux 核心根據應用程式的要求分配記憶體,通常來說應用程式分配了記憶體但是並沒有實際全部使用,為了提高效能,這部分沒用的記憶體可以留作它用,這部分記憶體是屬於每個程式的,核心直接回收利用的話比較麻煩,所以核心採用一種過度分配記憶體(over-commit memory)的辦法來間接利用這部分 “空閒” 的記憶體,提高整體記憶體的使用效率。一般來說這樣做沒有問題,但當大多數應用程式都消耗完自己的記憶體的時候麻煩就來了,因為這些應用程式的記憶體需求加起來超出了實體記憶體(包括 swap)的容量,核心(OOM killer)必須殺掉一些程式才能騰出空間保障系統正常執行。用銀行的例子來講可能更容易懂一些,部分人取錢的時候銀行不怕,銀行有足夠的存款應付,當全國人民(或者絕大多數)都取錢而且每個人都想把自己錢取完的時候銀行的麻煩就來了,銀行實際上是沒有這麼多錢給大家取的。

核心檢測到系統記憶體不足、挑選並殺掉某個程式的過程可以參考核心原始碼 linux/mm/oom_kill.c,當系統記憶體不足的時候,out_of_memory() 被觸發,然後呼叫 select_bad_process() 選擇一個 “bad” 程式殺掉,如何判斷和選擇一個 “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 外還可以優化系統(優化 Debian 5優化 CentOS 5.x),讓系統儘可能使用少的記憶體以便應用程式(如 MySQL) 能使用更多的記憶體,還有一個臨時的辦法就是調整核心引數,讓 MySQL 程式不容易被 OOM killer 發現。

配置 OOM killer

我們可以通過一些核心引數來調整 OOM killer 的行為,避免系統在那裡不停的殺程式。比如我們可以在觸發 OOM 後立刻觸發 kernel panic,kernel panic 10秒後自動重啟系統。

# sysctl -w vm.panic_on_oom=1
vm.panic_on_oom = 1

# sysctl -w kernel.panic=10
kernel.panic = 10

# echo "vm.panic_on_oom=1" >> /etc/sysctl.conf
# echo "kernel.panic=10" >> /etc/sysctl.conf

從上面的 oom_kill.c 程式碼裡可以看到 oom_badness() 給每個程式打分,根據 points 的高低來決定殺哪個程式,這個 points 可以根據 adj 調節,root 許可權的程式通常被認為很重要,不應該被輕易殺掉,所以打分的時候可以得到 3% 的優惠(adj -= 30; 分數越低越不容易被殺掉)。我們可以在使用者空間通過操作每個程式的 oom_adj 核心引數來決定哪些程式不這麼容易被 OOM killer 選中殺掉。比如,如果不想 MySQL 程式被輕易殺掉的話可以找到 MySQL 執行的程式號後,調整 oom_score_adj 為 -15(注意 points 越小越不容易被殺):

# ps aux | grep mysqld
mysql    2196  1.6  2.1 623800 44876 ?        Ssl  09:42   0:00 /usr/sbin/mysqld

# cat /proc/2196/oom_score_adj
0
# echo -15 > /proc/2196/oom_score_adj

當然,如果需要的話可以完全關閉 OOM killer(不推薦用在生產環境):

# sysctl -w vm.overcommit_memory=2

# echo "vm.overcommit_memory=2" >> /etc/sysctl.conf

找出最有可能被 OOM Killer 殺掉的程式

我們知道了在使用者空間可以通過操作每個程式的 oom_adj 核心引數來調整程式的分數,這個分數也可以通過 oom_score 這個核心引數看到,比如檢視程式號為981的 omm_score,這個分數被上面提到的 omm_score_adj 引數調整後(-15),就變成了3:

# cat /proc/981/oom_score
18

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

下面這個 bash 指令碼可用來列印當前系統上 oom_score 分數最高(最容易被 OOM Killer 殺掉)的程式:

# vi 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

# chmod +x oomscore.sh
# ./oomscore.sh
18   981 /usr/sbin/mysqld
 4 31359 -bash
 4 31056 -bash
 1 31358 sshd: root@pts/6
 1 31244 sshd: vpsee [priv]
 1 31159 -bash
 1 31158 sudo -i
 1 31055 sshd: root@pts/3
 1 30912 sshd: vpsee [priv]
 1 29547 /usr/sbin/sshd -D

相關文章