Linux內(nèi)核根據(jù)應(yīng)用程序的要求分配內(nèi)存,通常來(lái)說(shuō)應(yīng)用程序分配了內(nèi)存但是并沒(méi)有實(shí)際全部使用,為了提高性能捂人,這部分沒(méi)用的內(nèi)存可以留作它用,這部分內(nèi)存是屬于每個(gè)進(jìn)程的矢沿,內(nèi)核直接回收利用的話比較麻煩滥搭,所以內(nèi)核采用一種過(guò)度分配內(nèi)存(over-commit memory)的辦法來(lái)間接利用這部分“空閑”的內(nèi)存,提高整體內(nèi)存的使用效率捣鲸。一般來(lái)說(shuō)這樣做沒(méi)有問(wèn)題论熙,但當(dāng)大多數(shù)應(yīng)用程序都消耗完自己的內(nèi)存的時(shí)候麻煩就來(lái)了,因?yàn)檫@些應(yīng)用程序的內(nèi)存需求加起來(lái)超出了物理內(nèi)存(包括swap)的容量摄狱,內(nèi)核(OOM killer)必須殺掉一些進(jìn)程才能騰出空間保障系統(tǒng)正常運(yùn)行脓诡。用銀行的例子來(lái)講可能更容易懂一些无午,部分人取錢的時(shí)候銀行不怕,銀行有足夠的存款應(yīng)付祝谚,當(dāng)全國(guó)人民(或者絕大多數(shù))都取錢而且每個(gè)人都想把自己錢取完的時(shí)候銀行的麻煩就來(lái)了宪迟,銀行實(shí)際上是沒(méi)有這么多錢給大家取的。
比如某天一臺(tái)機(jī)器突然ssh遠(yuǎn)程登錄不了交惯,但能ping通次泽,說(shuō)明不是網(wǎng)絡(luò)的故障,原因是sshd進(jìn)程被OOM killer殺掉了席爽。重啟機(jī)器后查看系統(tǒng)日志/var/log/messages會(huì)發(fā)現(xiàn)Out of Memory:Killprocess 1865(sshd)類似的錯(cuò)誤信息意荤。又比如有時(shí)VPS的MySQL總是無(wú)緣無(wú)故掛掉,或者VPS 經(jīng)常死機(jī)只锻,登陸到終端發(fā)現(xiàn)都是常見(jiàn)的 Out of memory 問(wèn)題玖像。這通常是因?yàn)槟硶r(shí)刻應(yīng)用程序大量請(qǐng)求內(nèi)存導(dǎo)致系統(tǒng)內(nèi)存不足造成的,這時(shí)會(huì)觸發(fā) Linux 內(nèi)核里的 Out of Memory (OOM) killer齐饮,OOM killer 會(huì)殺掉某個(gè)進(jìn)程以騰出內(nèi)存留給系統(tǒng)用捐寥,不致于讓系統(tǒng)立刻崩潰。如果檢查相關(guān)的日志文件(/var/log/messages)就會(huì)看到下面類似的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 tainted2.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)核有個(gè)機(jī)制叫OOM killer(Out-Of-Memory killer)祖驱,該機(jī)制會(huì)監(jiān)控那些占用內(nèi)存過(guò)大握恳,尤其是瞬間很快消耗大量?jī)?nèi)存的進(jìn)程,為了防止內(nèi)存耗盡內(nèi)核會(huì)把該進(jìn)程殺掉捺僻。
內(nèi)核檢測(cè)到系統(tǒng)內(nèi)存不足乡洼、挑選并殺掉某個(gè)進(jìn)程的過(guò)程可以參考內(nèi)核源代碼 linux/mm/oom_kill.c,當(dāng)系統(tǒng)內(nèi)存不足的時(shí)候匕坯,out_of_memory()被觸發(fā)就珠,然后調(diào)用 select_bad_process() 選擇一個(gè)“bad”進(jìn)程殺掉,判斷和選擇一個(gè)“bad”進(jìn)程的過(guò)程由 oom_badness()決定醒颖,最 bad 的那個(gè)進(jìn)程就是那個(gè)最占用內(nèi)存的進(jìn)程妻怎。
/**
oom_badness -heuristic function to determine which candidate task to kill
@p: taskstruct of which task we should calculate
@totalpages:total present RAM allowed for page allocation
The heuristicfor determining which task to kill is made to be as simple and
predictableas possible. The goal is to return thehighest value for the
task consumingthe most memory to avoid subsequent oom failures.
*/
unsigned long oom_badness(struct task_struct *p,struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned longtotalpages)
{
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 thebadness score is the proportion of RAM that each
* task's rss, pagetable and swap space use.
*/
points = get_mm_rss(p->mm) + atomic_long_read(&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 -= (points * 3) / 100;
/*Normalize to oom_score_adj units */
adj *= totalpages / 1000;
points += adj;
/*
* Never return 0 for an eligible taskregardless of the root bonus and
* oom_score_adj (oom_score_adj can't beOOM_SCORE_ADJ_MIN here).
*/
returnpoints > 0 ? points : 1;
}
從上面的 oom_kill.c 代碼里可以看到 oom_badness() 給每個(gè)進(jìn)程打分,根據(jù) points 的高低來(lái)決定殺哪個(gè)進(jìn)程泞歉,這個(gè) points 可以根據(jù) adj 調(diào)節(jié)逼侦,root 權(quán)限的進(jìn)程通常被認(rèn)為很重要,不應(yīng)該被輕易殺掉腰耙,所以打分的時(shí)候可以得到 3% 的優(yōu)惠(分?jǐn)?shù)越低越不容易被殺掉)榛丢。我們可以在用戶空間通過(guò)操作每個(gè)進(jìn)程的 oom_adj 內(nèi)核參數(shù)來(lái)決定哪些進(jìn)程不這么容易被 OOM killer 選中殺掉。比如挺庞,如果不想 MySQL 進(jìn)程被輕易殺掉的話可以找到 MySQL 運(yùn)行的進(jìn)程號(hào)后晰赞,調(diào)整 /proc/PID/oom_score_adj 為 -15(注意 points越小越不容易被殺)防止重要的系統(tǒng)進(jìn)程觸發(fā)(OOM)機(jī)制而被殺死,內(nèi)核會(huì)通過(guò)特定的算法給每個(gè)進(jìn)程計(jì)算一個(gè)分?jǐn)?shù)來(lái)決定殺哪個(gè)進(jìn)程,每個(gè)進(jìn)程的oom分?jǐn)?shù)可以在/proc/PID/oom_score中找到掖鱼。每個(gè)進(jìn)程都有一個(gè)oom_score的屬性然走,oom killer會(huì)殺死oom_score較大的進(jìn)程,當(dāng)oom_score為0時(shí)禁止內(nèi)核殺死該進(jìn)程戏挡。設(shè)置/proc/PID/oom_adj可以改變oom_score芍瑞,oom_adj的范圍為【-17,15】褐墅,其中15最大-16最小拆檬,-17為禁止使用OOM,至于為什么用-17而不用其他數(shù)值(默認(rèn)值為0)妥凳,這個(gè)是由linux內(nèi)核定義的竟贯,查看內(nèi)核源碼可知:路徑為linux-xxxxx/include /uapi/linux/oom.h。
oom_score為2的n次方計(jì)算出來(lái)的逝钥,其中n就是進(jìn)程的oom_adj值屑那,oom_score的分?jǐn)?shù)越高就越會(huì)被內(nèi)核優(yōu)先殺掉。當(dāng)oom_adj=-17時(shí)晌缘,oom_score將變?yōu)?齐莲,所以可以設(shè)置參數(shù)/proc/PID/oom_adj為-17禁止內(nèi)核殺死該進(jìn)程痢站。
上面的那個(gè)MySQL例子可以如下解決來(lái)降低mysql的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
當(dāng)然了,保證某個(gè)進(jìn)程不被內(nèi)核殺掉可以這樣操作:
echo -17 > /proc/$PID/oom_adj
例如防止sshd被殺阵难,可以這樣操作:
pgrep -f "/usr/sbin/sshd" | while read PID;do echo -17 > /proc/$PID/oom_adj;done
為了驗(yàn)證OOM機(jī)制的效果岳枷,我們不妨做個(gè)測(cè)試。
首先看看我系統(tǒng)現(xiàn)有內(nèi)存大小呜叫,沒(méi)錯(cuò)96G多空繁,物理上還要比查看的值大一些。
再看看目前進(jìn)程最大的有哪些朱庆,top查看盛泡,我目前只跑了兩個(gè)java程序的進(jìn)程,分別4.6G娱颊,再往后redis進(jìn)程吃了21m傲诵,iscsi服務(wù)占了32m,gdm占了25m箱硕,其它的進(jìn)程都是幾M而已拴竹。
現(xiàn)在我自己用C寫一個(gè)叫bigmem程序,我指定該程序分配內(nèi)存85G剧罩,呵呵栓拜,效果明顯,然后執(zhí)行后再用top查看,排在第一位的是我的bigmem幕与,RES是物理內(nèi)存挑势,已經(jīng)吃滿了85G。
繼續(xù)觀察纽门,當(dāng)bigmem穩(wěn)定保持在85G一會(huì)后薛耻,內(nèi)核會(huì)自動(dòng)將其進(jìn)程kill掉,增長(zhǎng)的過(guò)程中沒(méi)有被殺赏陵,如果不希望被殺可以執(zhí)行
pgrep -f "bigmem" | while read PID; do echo -17 > /proc/$PID/oom_adj;done
執(zhí)行以上命令前后饼齿,明顯會(huì)對(duì)比出效果,就可以體會(huì)到內(nèi)核OOM機(jī)制的實(shí)際作用了蝙搔。
注意缕溉,由任意調(diào)整的進(jìn)程衍生的任意進(jìn)程將繼承該進(jìn)程的 oom_score。例如:如果 sshd 進(jìn)程不受 oom_killer 功能影響吃型,所有由 SSH 會(huì)話產(chǎn)生的進(jìn)程都將不受其影響证鸥。這可在出現(xiàn) OOM 時(shí)影響 oom_killer 功能救援系統(tǒng)的能力。
當(dāng)然還可以通過(guò)修改內(nèi)核參數(shù)禁止在內(nèi)存出現(xiàn)OOM時(shí)采取殺掉進(jìn)程的這種機(jī)制勤晚,但此時(shí)會(huì)觸發(fā)kernel panic枉层。當(dāng)內(nèi)存嚴(yán)重不足時(shí),內(nèi)核有兩種選擇:1.直接panic 2.殺掉部分進(jìn)程赐写,釋放一些內(nèi)存鸟蜡。通過(guò)/proc/sys/vm/panic_on_oom可以控制,當(dāng)panic_on_oom為1時(shí)挺邀,直接panic揉忘,當(dāng)panic_on_oom為0時(shí)內(nèi)核將通過(guò)oom killer殺掉部分進(jìn)程。(默認(rèn)是為0的)
# sysctl -w vm.panic_on_oom=1
vm.panic_on_oom = 1 //1表示關(guān)閉端铛,默認(rèn)為0表示開(kāi)啟OOM killer
# sysctl –p
我們可以通過(guò)一些內(nèi)核參數(shù)來(lái)調(diào)整 OOM killer 的行為泣矛,避免系統(tǒng)在那里不停的殺進(jìn)程。比如我們可以在觸發(fā) OOM 后立刻觸發(fā) kernel panic禾蚕,kernel panic 10秒后自動(dòng)重啟系統(tǒng):
# 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
當(dāng)然您朽,如果需要的話可以完全不允許過(guò)度分配內(nèi)存,此時(shí)也就不會(huì)出現(xiàn)OOM的問(wèn)題(不過(guò)不推薦這樣做):
# sysctl -w vm.overcommit_memory=2
# echo "vm.overcommit_memory=2" >> /etc/sysctl.conf换淆,
vm.overcommit_memory 表示內(nèi)核在分配內(nèi)存時(shí)候做檢查的方式哗总。這個(gè)變量可以取到0,1,2三個(gè)值。對(duì)取不同的值時(shí)的處理方式都定義在內(nèi)核源碼 mm/mmap.c 的 __vm_enough_memory 函數(shù)中产舞。
0:當(dāng)用戶空間請(qǐng)求更多的的內(nèi)存時(shí)魂奥,內(nèi)核嘗試估算出剩余可用的內(nèi)存。此時(shí)宏為 OVERCOMMIT_GUESS易猫,內(nèi)核計(jì)算:NR_FILE_PAGES 總量+SWAP總量+slab中可以釋放的內(nèi)存總量耻煤,如果申請(qǐng)空間超過(guò)此數(shù)值,則將此數(shù)值與空閑內(nèi)存總量減掉 totalreserve_pages(?) 的總量相加。如果申請(qǐng)空間依然超過(guò)此數(shù)值哈蝇,則分配失敗棺妓。
1:當(dāng)設(shè)這個(gè)參數(shù)值為1時(shí),宏為 OVERCOMMIT_ALWAYS炮赦,內(nèi)核允許超量使用內(nèi)存直到用完為止怜跑,主要用于科學(xué)計(jì)算。
2:當(dāng)設(shè)這個(gè)參數(shù)值為2時(shí)吠勘,此時(shí)宏為 OVERCOMMIT_NEVER性芬,內(nèi)核會(huì)使用一個(gè)決不過(guò)量使用內(nèi)存的算法,即系統(tǒng)整個(gè)內(nèi)存地址空間不能超過(guò)swap+50%的RAM值剧防,50%參數(shù)的設(shè)定是在overcommit_ratio中設(shè)定植锉,內(nèi)核計(jì)算:內(nèi)存總量×vm.overcommit_ratio/100+SWAP 的總量,如果申請(qǐng)空間超過(guò)此數(shù)值峭拘,則分配失敗俊庇。vm.overcommit_ratio 的默認(rèn)值為50。
以上為粗略描述鸡挠,在實(shí)際計(jì)算時(shí)辉饱,如果非root進(jìn)程,則在計(jì)算時(shí)候會(huì)保留3%的空間拣展,而root進(jìn)程則沒(méi)有該限制彭沼。詳細(xì)過(guò)程可看源碼。
找出最有可能被 OOM Killer 殺掉的進(jìn)程:
我們知道了在用戶空間可以通過(guò)操作每個(gè)進(jìn)程的 oom_adj 內(nèi)核參數(shù)來(lái)調(diào)整進(jìn)程的分?jǐn)?shù)瞎惫,這個(gè)分?jǐn)?shù)也可以通過(guò) oom_score 這個(gè)內(nèi)核參數(shù)看到溜腐,比如查看進(jìn)程號(hào)為981的 omm_score译株,這個(gè)分?jǐn)?shù)被上面提到的 omm_score_adj 參數(shù)調(diào)整后(-15)瓜喇,就變成了3:
cat /proc/981/oom_score
18
echo -15 > /proc/981/oom_score_adj
cat /proc/981/oom_score
3
下面這個(gè) bash 腳本可用來(lái)打印當(dāng)前系統(tǒng)上 oom_score 分?jǐn)?shù)最高(最容易被 OOM Killer 殺掉)的進(jìn)程:
!/bin/bash
for proc in (cat (basename (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
注意:
1.Kernel-2.6.26之前版本的oomkiller算法不夠精確,RHEL6.x版本的2.6.32可以解決這個(gè)問(wèn)題歉糜。
2.子進(jìn)程會(huì)繼承父進(jìn)程的oom_adj乘寒。
3.OOM不適合于解決內(nèi)存泄漏(Memory leak)的問(wèn)題。
4.有時(shí)free查看還有充足的內(nèi)存匪补,但還是會(huì)觸發(fā)OOM伞辛,是因?yàn)樵撨M(jìn)程可能占用了特殊的內(nèi)存地址空間。