Linux的OOM終結者

jopen 10年前發布 | 23K 次閱讀 Linux

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

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

我們部署的監控系統發現進程已經掛掉了并重啟了服務。由于現在咖啡因已經流淌在我的血液中了,我開始變得信心十足。果然在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的調優,或者將負載水平分布到數個小的實例上,又或者減少應用程序的內存占用量。

原創文章轉載請注明出處:Linux的OOM終結者

英文原文鏈接

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!