Linux 的 OOM 終結者

Java譯站發表於2015-07-21

現在是早晨6點鐘。已經醒來的我正在總結到底是什麼事情使得我的起床鬧鈴提前了這麼多。故事剛開始的時候,手機鈴聲恰好停止。又困又煩躁的我看了下手機,看看是不是我自己瘋了把鬧鐘調得這麼早,居然是早晨5點。然而不是,而是我們的監控系統顯示,Plumbr服務出故障了。

Linux 的 OOM 終結者

作為這個領域的經驗豐富的老鳥,我開啟了咖啡機,這是正確解決問題的第一步。一杯咖啡在手之後,現在我可以開始處理故障了。首先要懷疑的是應用程式本身,因為它在崩潰之前一點異常也沒有。應用程式日誌中沒有錯誤,沒有警告,也沒有任何可疑的資訊。

我們部署的監控系統發現程式已經掛掉了並重啟了服務。由於現在咖啡因已經流淌在我的血液中了,我開始變得信心十足。果然在30分鐘後,我在/var/log/kern.log日誌中發現了下面的資訊:

Jun  4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice child
Jun  4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, filers:0kB

很明顯我們被Linux核心給坑了。你知道的,Linux裡面有許多邪惡的怪物(也叫作守護程式)。這些守護程式是由幾個核心作業所看管的,其中的一個猶為惡毒。所有的現代Linux核心中都會有一個記憶體不足終結者(Out of memory Killer, OOM Killer)的內建機制,在記憶體過低的情況下,它會殺掉你的程式。當探測到這一情況時,這個終結者會被啟用,然後挑選出一個程式去終結掉。選擇目標程式使用的是一套啟發式演算法,它會計算所有程式的分數,然後選出那個分數最低的程式。

理解“Out of memory killer”

預設情況下,Linux核心會允許程式請求的記憶體超出實際可用記憶體的大小。這在現實世界中是有意義的,因為大多數程式其實並不會用到所有分配給它的記憶體(注:同一時間內不會全用到)。和這個問題最類似的就是運營商了。他們承諾賣給使用者的都是100Mb的頻寬,這實際上遠遠超出了他們的網路容量。他們賭的就是使用者實際上並不會同時用完分配給他們的下載上限。一個10Gb的連線可以很輕鬆地承載100個以上的使用者,這裡的100是通過簡單的數學運算得出的(10G/100M)。

這個做法的一個很明顯的副作用就是,萬一有一個程式正走上了一條耗盡記憶體的不歸路怎麼辦。這會導致低可用記憶體的情況,也就是沒有記憶體頁能夠再分配給程式了。你可能也碰到過這種情況,沒有root帳戶你是殺不掉這種頑固的程式的。為了解決這一情況,終結者被啟用了,並找出了要終結的程式。

關於”Out of memory killer”引數的調整,可以參考下這篇文章

是誰觸發了Out of memory killer?

雖然現在已經知道發生了什麼,但還是搞不清楚到底是誰觸發了這個終結者,然後在早晨5點鐘把我吵醒。進一步的分析後找到了答案:

  • /proc/sys/vm/overcommit_memory中的配置允許記憶體的超量使用——該值設定為1,這意味著每個malloc()請求都會成功。
  • 應用程式執行在一臺EC2 m1.small的例項上。EC2的例項預設是禁用了交換分割槽的。

這兩個因素正好又趕上了我們服務的突然的流量高峰,最終導致應用程式為了支援這些額外的使用者而不斷請求更多的記憶體。記憶體超量使用的配置允許這個貪心的程式不停地申請記憶體,最後會觸發這個記憶體不足的終結者,它就是來履行它的使命的。去殺掉了我們的程式,然後在大半夜把我給叫醒。

示例

當我把這個情況描述給工程師的時候,有一位工程師覺得很有意思,因此寫了個小的測試用例來重現了這個問題。你可以在Linux下編譯並執行下面這個程式碼片段(我是在最新的穩定版Ubuntu上執行的)。

package eu.plumbr.demo;
public class OOM {

  public static void main(String[] args){
    java.util.List l = new java.util.ArrayList();
    for (int i = 10000; i < 100000; i++) {
      try {
        l.add(new int[100_000_000]);
      } catch (Throwable t) {
        t.printStackTrace();
      }
    }
  }
}

然後你就會發現同樣的一個 Out of memory: Kill process (java) score or sacrifice child資訊。

注意的是,你可能得調整下交換分割槽以及堆的大小,在我這個測試用例中,我通過-Xm2g設定了2G大小的堆,同時交換記憶體使用的是如下的配置:

swapoff -a 
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile

解決方案?

這種情況有好幾種解決方案。在我們這個例子中,我們只是把系統遷移到了一臺記憶體更大的機器上(褲子都脫了就讓我看這個?)我也考慮過啟用交換分割槽,不過諮詢了工程師之後我想起來JVM上的GC程式在交換分割槽下的表現並不是很理想,因此這個選項就作罷了。

還有別的一些方法比如OOM killer的調優,或者將負載水平分佈到數個小的例項上,又或者減少應用程式的記憶體佔用量。

相關文章