最近有位 VPS 客戶抱怨 MySQL 無緣無故掛掉,還有位客戶抱怨 VPS 經(jīng)常死機,登陸到終端看了一下,都是常見的 Out of memory 問題。這通常是因為某時刻應用程序大量請求內(nèi)存導致系統(tǒng)內(nèi)存不足造成的,這通常會觸發(fā) Linux 內(nèi)核里的 Out of Memory (OOM) killer,OOM killer 會殺掉某個進程以騰出內(nèi)存留給系統(tǒng)用,不致于讓系統(tǒng)立刻崩潰。如果檢查相關(guān)的日志文件(/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=http://www.3lian.com/ 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 內(nèi)核根據(jù)應用程序的要求分配內(nèi)存,通常來說應用程序分配了內(nèi)存但是并沒有實際全部使用,為了提高性能,這部分沒用的內(nèi)存可以留作它用,這部分內(nèi)存是屬于每個進程的,內(nèi)核直接回收利用的話比較麻煩,所以內(nèi)核采用一種過度分配內(nèi)存(over-commit memory)的辦法來間接利用這部分 “空閑” 的內(nèi)存,提高整體內(nèi)存的使用效率。一般來說這樣做沒有問題,但當大多數(shù)應用程序都消耗完自己的內(nèi)存的時候麻煩就來了,因為這些應用程序的內(nèi)存需求加起來超出了物理內(nèi)存(包括 swap)的容量,內(nèi)核(OOM killer)必須殺掉一些進程才能騰出空間保障系統(tǒng)正常運行。用銀行的例子來講可能更容易懂一些,部分人取錢的時候銀行不怕,銀行有足夠的存款應付,當全國人民(或者絕大多數(shù))都取錢而且每個人都想把自己錢取完的時候銀行的麻煩就來了,銀行實際上是沒有這么多錢給大家取的。
內(nèi)核檢測到系統(tǒng)內(nèi)存不足、挑選并殺掉某個進程的過程可以參考內(nèi)核源代碼 linux/mm/oom_kill.c,當系統(tǒng)內(nèi)存不足的時候,out_of_memory() 被觸發(fā),然后調(diào)用 select_bad_process() 選擇一個 “bad” 進程殺掉,如何判斷和選擇一個 “bad” 進程呢,總不能隨機選吧?挑選的過程由 oom_badness() 決定,挑選的算法和想法都很簡單很樸實:最 bad 的那個進程就是那個最占用內(nèi)存的進程。
/**
* 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 躺著也能中槍了,因為它的體積總是最大(一般來說它在系統(tǒng)上占用內(nèi)存最多),所以如果 Out of Memeory (OOM) 的話總是不幸第一個被 kill 掉。解決這個問題最簡單的辦法就是增加內(nèi)存,或者想辦法優(yōu)化 MySQL 使其占用更少的內(nèi)存,除了優(yōu)化 MySQL 外還可以優(yōu)化系統(tǒng)(優(yōu)化 Debian 5,優(yōu)化 CentOS 5.x),讓系統(tǒng)盡可能使用少的內(nèi)存以便應用程序(如 MySQL) 能使用更多的內(nèi)存,還有一個臨時的辦法就是調(diào)整內(nèi)核參數(shù),讓 MySQL 進程不容易被 OOM killer 發(fā)現(xiàn)。