萬盛學電腦網

 萬盛學電腦網 >> 服務器教程 >> Linux系統的OOM Killer處理機制

Linux系統的OOM Killer處理機制

   最近有位 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 後立刻觸發 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_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 %sn"

  "$(cat $proc/oom_score)"

  "$(basename $proc)"

  "$(cat $proc/cmdline | tr '' ' ' | 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

copyright © 萬盛學電腦網 all rights reserved