Linux 內核根據應用程序的要求分配內存,通常來說應用程序分配了內存但是並沒有實際全部使用,為了提高性能,這部分沒用的內存可以留作它用,這部分內存是屬於每個進程的,內核直接回收利用的話比較麻煩,所以內核采用一種過度分配內存(over-commit memory)的辦法來間接利用這部分“空閑”的內存,提高整體內存的使用效率。一般來說這樣做沒有問題,但當大多數應用程序都消耗完自己的內存的時候麻煩就來了,因為這些應用程序的內存需求加起來超出了物理內存(包括 swap)的容量,內核(OOM killer)必須殺掉一些進程才能騰出空間保障系統正常運行。用銀行的例子來講可能更容易懂一些,部分人取錢的時候銀行不怕,銀行有足夠的存款應付,當全國人民(或者絕大多數)都取錢而且每個人都想把自己錢取完的時候銀行的麻煩就來了,銀行實際上是沒有這麽多錢給大家取的。
比如某天一臺機器突然ssh遠程登錄不了,但能ping通,說明不是網絡的故障,原因是sshd進程被OOM killer殺掉了。重啟機器後查看系統日誌/var/log/messages會發現Out of Memory:Kill process 1865(sshd)類似的錯誤信息。又比如有時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內核有個機制叫OOM killer(Out-Of-Memory 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) + 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 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;
}
從上面的 oom_kill.c 代碼裏可以看到 oom_badness() 給每個進程打分,根據 points 的高低來決定殺哪個進程,這個 points 可以根據 adj 調節,root 權限的進程通常被認為很重要,不應該被輕易殺掉,所以打分的時候可以得到 3% 的優惠(分數越低越不容易被殺掉)。我們可以在用戶空間通過操作每個進程的 oom_adj 內核參數來決定哪些進程不這麽容易被 OOM killer 選中殺掉。比如,如果不想 MySQL 進程被輕易殺掉的話可以找到 MySQL 運行的進程號後,調整 /proc/PID/oom_score_adj 為 -15(註意 points 越小越不容易被殺)防止重要的系統進程觸發(OOM)機制而被殺死,內核會通過特定的算法給每個進程計算一個分數來決定殺哪個進程,每個進程的oom分數可以在/proc/PID/oom_score中找到。每個進程都有一個oom_score的屬性,oom killer會殺死oom_score較大的進程,當oom_score為0時禁止內核殺死該進程。設置/proc/PID/oom_adj可以改變oom_score,oom_adj的範圍為【-17,15】,其中15最大-16最小,-17為禁止使用OOM,至於為什麽用-17而不用其他數值(默認值為0),這個是由linux內核定義的,查看內核源碼可知:路徑為linux-xxxxx/include /uapi/linux/oom.h。
oom_score為2的n次方計算出來的,其中n就是進程的oom_adj值,oom_score的分數越高就越會被內核優先殺掉。當oom_adj=-17時,oom_score將變為0,所以可以設置參數/proc/PID/oom_adj為-17禁止內核殺死該進程。
上面的那個MySQL例子可以如下解決來降低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
當然了,保證某個進程不被內核殺掉可以這樣操作:
echo -17 > /proc/$PID/oom_adj
例如防止sshd被殺,可以這樣操作:
pgrep -f "/usr/sbin/sshd" | while read PID;do echo -17 > /proc/$PID/oom_adj;done
為了驗證OOM機制的效果,我們不妨做個測試。
首先看看我系統現有內存大小,沒錯96G多,物理上還要比查看的值大一些。
再看看目前進程最大的有哪些,top查看,我目前只跑了兩個java程序的進程,分別4.6G,再往後redis進程吃了21m,iscsi服務占了32m,gdm占了25m,其它的進程都是幾M而已。
現在我自己用C寫一個叫bigmem程序,我指定該程序分配內存85G,呵呵,效果明顯,然後執行後再用top查看,排在第一位的是我的bigmem,RES是物理內存,已經吃滿了85G。
繼續觀察,當bigmem穩定保持在85G一會後,內核會自動將其進程kill掉,增長的過程中沒有被殺,如果不希望被殺可以執行
pgrep -f "bigmem" | while read PID; do echo -17 > /proc/$PID/oom_adj;done
執行以上命令前後,明顯會對比出效果,就可以體會到內核OOM機制的實際作用了。
註意,由任意調整的進程衍生的任意進程將繼承該進程的 oom_score。例如:如果 sshd 進程不受 oom_killer 功能影響,所有由 SSH 會話產生的進程都將不受其影響。這可在出現 OOM 時影響 oom_killer 功能救援系統的能力。
當然還可以通過修改內核參數禁止在內存出現OOM時采取殺掉進程的這種機制,但此時會觸發kernel panic。當內存嚴重不足時,內核有兩種選擇:1.直接panic 2.殺掉部分進程,釋放一些內存。通過/proc/sys/vm/panic_on_oom可以控制,當panic_on_oom為1時,直接panic,當panic_on_oom為0時內核將通過oom killer殺掉部分進程。(默認是為0的)
# sysctl -w vm.panic_on_oom=1
vm.panic_on_oom = 1 //1表示關閉,默認為0表示開啟OOM killer
# sysctl –p
我們可以通過一些內核參數來調整 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的問題(不過不推薦這樣做):
# sysctl -w vm.overcommit_memory=2
# echo "vm.overcommit_memory=2" >> /etc/sysctl.conf,
vm.overcommit_memory 表示內核在分配內存時候做檢查的方式。這個變量可以取到0,1,2三個值。對取不同的值時的處理方式都定義在內核源碼 mm/mmap.c 的 __vm_enough_memory 函數中。
0:當用戶空間請求更多的的內存時,內核嘗試估算出剩余可用的內存。此時宏為 OVERCOMMIT_GUESS,內核計算:NR_FILE_PAGES 總量+SWAP總量+slab中可以釋放的內存總量,如果申請空間超過此數值,則將此數值與空閑內存總量減掉 totalreserve_pages(?) 的總量相加。如果申請空間依然超過此數值,則分配失敗。
1:當設這個參數值為1時,宏為 OVERCOMMIT_ALWAYS,內核允許超量使用內存直到用完為止,主要用於科學計算。
2:當設這個參數值為2時,此時宏為 OVERCOMMIT_NEVER,內核會使用一個決不過量使用內存的算法,即系統整個內存地址空間不能超過swap+50%的RAM值,50%參數的設定是在overcommit_ratio中設定,內核計算:內存總量×vm.overcommit_ratio/100+SWAP 的總量,如果申請空間超過此數值,則分配失敗。vm.overcommit_ratio 的默認值為50。
以上為粗略描述,在實際計算時,如果非root進程,則在計算時候會保留3%的空間,而root進程則沒有該限制。詳細過程可看源碼。
找出最有可能被 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 殺掉)的進程:
#!/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
註意:
1.Kernel-2.6.26之前版本的oomkiller算法不夠精確,RHEL6.x版本的2.6.32可以解決這個問題。
2.子進程會繼承父進程的oom_adj。
3.OOM不適合於解決內存泄漏(Memory leak)的問題。
4.有時free查看還有充足的內存,但還是會觸發OOM,是因為該進程可能占用了特殊的內存地址空間。
- Jul 15 Wed 2015 15:54
Linux內核的OOM機制
全站熱搜
留言列表
發表留言