Linux內核OOM機制的詳細分析

    Linux內核根據應用程序的要求分配內存,一般來講應用程序分配了內存可是並無實際所有使用,爲了提升性能,這部分沒用的內存能夠留做它用,這部份內存是屬於每一個進程的,內核直接回收利用的話比較麻煩,因此內核採用一種過分分配內存(over-commit memory)的辦法來間接利用這部分「空閒」的內存,提升總體內存的使用效率。通常來講這樣作沒有問題,但當大多數應用程序都消耗完本身的內存的時候麻煩就來了,由於這些應用程序的內存需求加起來超出了物理內存(包括swap)的容量,內核(OOM killer)必須殺掉一些進程才能騰出空間保障系統正常運行。用銀行的例子來說可能更容易懂一些,部分人取錢的時候銀行不怕,銀行有足夠的存款應付,當全國人民(或者絕大多數)都取錢並且每一個人都想把本身錢取完的時候銀行的麻煩就來了,銀行其實是沒有這麼多錢給你們取的。html

       好比某天一臺機器忽然ssh遠程登陸不了,但能ping通,說明不是網絡的故障,緣由是sshd進程被OOM killer殺掉了。重啓機器後查看系統日誌/var/log/messages會發現Out of Memory:Killprocess 1865(sshd)相似的錯誤信息。又好比有時VPS的MySQL老是平白無故掛掉,或者VPS 常常死機,登錄到終端發現都是常見的 Out of memory 問題。這一般是由於某時刻應用程序大量請求內存致使系統內存不足形成的,這時會觸發 Linux 內核裏的 Out of Memory (OOM) killer,OOM killer 會殺掉某個進程以騰出內存留給系統用,不致於讓系統馬上崩潰。若是檢查相關的日誌文件(/var/log/messages)就會看到下面相似的Out of memory:Kill process 信息:java

  ...node

  Out of memory: Kill process 9682(mysqld) score 9 or sacrifice childmysql

  Killed process 9682, UID 27,(mysqld) total-vm:47388kB, anon-rss:3744kB, file-rss:80kBlinux

  httpd invoked oom-killer:gfp_mask=0x201da, order=0, oom_adj=0, oom_score_adj=0redis

  httpd cpuset=/ mems_allowed=0算法

  Pid: 8911, comm: httpd Not tainted2.6.32-279.1.1.el6.i686 #1sql

  ...api

  21556 total pagecache pagesbash

  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: 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() 給每一個進程打分,根據 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,是由於該進程可能佔用了特殊的內存地址空間。
相關文章
相關標籤/搜索