伺服器維運

OOM killer 是什麼?程序被殺怎麼排查與預防

OOM killer 是 Linux 核心在記憶體徹底耗盡、怎麼回收都騰不出空間時的最後手段:給每個程序打分,向分數最高的那個發 SIGKILL,讓系統活下來。程序莫名消失,先用 dmesg 查有沒有 Killed process;確認是 OOM 後,短期用 oom_score_adj 保護關鍵程序,長期要把各服務的記憶體上限加起來算清楚,再配適量 swap。

作者 頂聯產品團隊發布於 6 分鐘閱讀

OOM killer(Out Of Memory killer)是 Linux 核心的記憶體耗盡處理機制:實體記憶體和 swap 都用完、核心回收快取和換頁之後仍然分配不出記憶體時,它給每個程序算一個分數,殺掉分數最高的那個,釋放記憶體讓系統繼續執行。被選中的程序收到的是 SIGKILL,不能捕獲也不能拒絕,表現就是“程序突然沒了”,程式自己的日誌裡沒有任何退出資訊。所以服務莫名其妙地消失時,第一件事是看核心日誌裡有沒有 Out of memory 和 Killed process;有,再弄清楚是誰把記憶體吃光的、為什麼偏偏殺了它。

OOM killer 什麼時候會觸發

Linux 預設允許程式申請超過實際可用量的記憶體(overcommit),申請時只登記虛擬位址,真正寫入時才分配物理頁。所以 OOM 不是發生在 malloc 那一刻,而是發生在某個程序觸碰新記憶體、核心拿不出空閒頁的時候:核心先回收頁快取、把匿名頁換到 swap、整理記憶體碎片,這些都失敗後才呼叫 OOM killer。按觸發範圍分四種:

觸發場景 條件 在哪個範圍裡挑程序 日誌特徵
整機記憶體耗盡 實體記憶體和 swap 都用完,回收不出可用頁 全系統 oom-kill: 行帶 global_oom,constraint 為 CONSTRAINT_NONE
cgroup 記憶體上限 容器或 systemd 服務的 memory.max(cgroup v1 為 memory.limit_in_bytes)到頂 只在該 cgroup 內 Memory cgroup out of memory,constraint 為 CONSTRAINT_MEMCG
NUMA 節點或 cpuset 限制 程序被限定只能用部分記憶體節點,這些節點耗盡 受限的程序 constraint 為 CONSTRAINT_CPUSET 或 CONSTRAINT_MEMORY_POLICY
手動觸發 透過 SysRq 的 f 鍵 全系統 與整機 OOM 類似,invoked oom-killer 行裡 order=-1

下面兩種情況看起來像 OOM,其實不是 OOM killer 乾的,排查時要分開:

  • Java 報 java.lang.OutOfMemoryError:這是 JVM 的堆或元空間到了 -Xmx 等引數設定的上限,由 JVM 自己丟擲,程序不一定退出,核心日誌裡也沒有記錄。
  • 程式報 Cannot allocate memory:申請記憶體當場失敗,常見於 vm.overcommit_memory=2(嚴格模式)或程序觸及 ulimit 限制,核心沒有殺任何程序。

三個相關的核心引數,先看當前值:

sysctl vm.overcommit_memory vm.panic_on_oom vm.oom_kill_allocating_task
引數 預設 含義
vm.overcommit_memory 0 0 按啟發式允許超額申請;1 總是允許;2 嚴格限制,承諾總量不超過 CommitLimit(swap + 記憶體 × overcommit_ratio%,ratio 預設 50)
vm.panic_on_oom 0 0 殺程序;1 整機 OOM 時核心崩潰,受 cpuset、記憶體策略或 cgroup 限制的 OOM 仍只殺程序;2 任何 OOM 都崩潰
vm.oom_kill_allocating_task 0 0 按分數挑;非 0 直接殺掉正在申請記憶體的那個程序,省去掃描

OOM killer 怎麼挑程序:打分規則與算例

核心給每個程序算的“壞分”(badness)由兩部分組成:

  1. 記憶體佔用:常駐記憶體頁(匿名頁、檔案對映頁、共享記憶體頁)+ 已被換到 swap 的頁 + 頁表佔用的頁,單位是頁。
  2. 人為調整:oom_score_adj × 可用總頁數 ÷ 1000。oom_score_adj 取值 -1000 到 1000,相當於按總記憶體的千分之幾加分或減分。

整機 OOM 時“可用總頁數”是實體記憶體加 swap。幾類程序不參與評選:oom_score_adj 為 -1000 的、核心執行緒和 init(PID 1)。如果上一個被選中的程序還在退出、沒釋放完記憶體,核心會先等它,而不是馬上再殺一個。

用一個算例看 oom_score_adj 的分量。假設一台伺服器有 16 GiB 記憶體和 2 GiB swap,總共 18 GiB,按 4 KiB 一頁是 4,718,592 頁:

程序 記憶體佔用 oom_score_adj 調整分 總分
mysqld 9 GiB = 2,359,296 頁 0 0 2,359,296
報表任務(java) 3 GiB = 786,432 頁 500 500 × 4,718,592 ÷ 1000 = 2,359,296 3,145,728
php-fpm 單個子程序 60 MiB = 15,360 頁 0 0 15,360

報表任務只佔 3 GiB,卻因為 +500 的調整排在 mysqld 前面先被殺掉。反過來,如果什麼都不調,被殺的就是佔用最大的 mysqld。由此有兩個結論:

  • 分數只看佔用,不看誰在洩漏。 一個小程序慢慢洩漏把記憶體擠滿,最後被殺的往往是最大、最無辜的資料庫。日誌第一行 invoked oom-killer 前面的程序名,是觸發那一刻正在申請記憶體的程序,也未必是罪魁禍首。
  • 每個程序的當前分數能直接看。 /proc/<pid>/oom_score 是核心按總記憶體摺算後的分數,數字越大越先被殺,只適合在同一台機器上橫向比較。
# 列出 oom_score 最高的 10 個程序:分數、oom_score_adj、程序名
for p in /proc/[0-9]*; do
  printf '%s %s %s\n' "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/oom_score_adj 2>/dev/null)" "$(cat $p/comm 2>/dev/null)"
done | sort -rn | head -10

程序被殺了,怎麼確認是不是 OOM

# 本次啟動以來的核心日誌,-T 顯示可讀時間
dmesg -T | grep -E 'invoked oom-killer|Out of memory|Killed process|oom-kill:'
# 機器已經重啟過:查上一次啟動的核心日誌(需要 journald 持久化儲存)
journalctl -k -b -1 | grep -iE 'oom|killed process'
# 傳統日誌檔案,RHEL 系在 messages,Debian/Ubuntu 在 kern.log
grep -iE 'out of memory|killed process' /var/log/messages /var/log/kern.log 2>/dev/null

一次整機 OOM 在日誌裡大致是這樣(範例,已刪去時間戳和中間的記憶體明細):

php-fpm invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
Mem-Info:
active_anon:3612340 inactive_anon:210422 ... free:33711 ...
Tasks state (memory values in pages):
[  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[   1123]    27  1123  2852610  2401230 20512768        0             0 mysqld
[   2210]    33  2210    98213    15520   417792        0             0 php-fpm
oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/mysqld.service,task=mysqld,pid=1123,uid=27
Out of memory: Killed process 1123 (mysqld) total-vm:11410440kB, anon-rss:9604920kB, file-rss:0kB, shmem-rss:0kB, UID:27 pgtables:20032kB oom_score_adj:0

逐行讀:

日誌位置 看什麼
invoked oom-killer 行 觸發時正在申請記憶體的程序;order=0 是申請單個頁,order 較大而 free 並不低時,問題多在記憶體碎片而不是總量
Mem-Info active_anon、inactive_anon 是程序的匿名記憶體,單位是頁;free 很低、anon 佔了絕大部分,說明是程序把記憶體吃光,而不是快取
Tasks state 表 當時每個程序的佔用,rss 單位是頁(4 KiB):2,401,230 頁約 9.2 GiB。把這張表按 rss 排序,能看出除了被殺的程序還有誰在佔記憶體
oom-kill: 行 global_oom 表示整機 OOM,oom_memcg=... 表示某個 cgroup 的上限到了;task_memcg 能看出程序屬於哪個服務或容器
Killed process 行 被殺的程序和它當時的匿名記憶體 anon-rss;老核心這裡寫作 Out of memory: Kill process 1123 (mysqld) score 912 or sacrifice child

核心日誌裡找不到記錄,程序卻確實被殺了,按下表繼續查:

現象 可能的來源 怎麼確認
整組程序或整個使用者會話被殺,dmesg 無記錄 systemd-oomd 等使用者態 OOM 守護程式,它在核心 OOM 之前按記憶體壓力殺掉整個 cgroup journalctl -u systemd-oomd
容器退出碼 137 137 = 128 + 9,即收到 SIGKILL docker inspect -f '{{.State.OOMKilled}}' 容器名;Kubernetes 看 kubectl describe pod 裡的 Reason: OOMKilled
systemd 服務失敗 服務所在 cgroup 觸發了 OOM systemctl status 服務名,較新版本會顯示 Failed with result 'oom-kill'
退出碼 137,以上都沒有 有人或某個監控指令碼執行了 kill -9 查審計日誌、定時任務和看門狗指令碼

用 oom_score_adj 保護關鍵程序

oom_score_adj 寫在 /proc/<pid>/oom_score_adj 裡,直接改是臨時的,程序重啟後失效:

cat /proc/$(pidof -s sshd)/oom_score_adj     # 檢視當前值
echo -500 > /proc/1123/oom_score_adj          # 臨時調整
choom -p 1123 -n -500                         # util-linux 自帶的工具,效果相同

降低分值(往負數方向調)需要 root 或 CAP_SYS_RESOURCE 許可權,普通使用者只能往高調。要持久生效,寫進 systemd 服務的覆蓋配置。下例的服務名以實際為準(RHEL 系的 MySQL 服務叫 mysqld,Debian/Ubuntu 叫 mysql):

mkdir -p /etc/systemd/system/mysqld.service.d
printf '[Service]\nOOMScoreAdjust=-600\n' > /etc/systemd/system/mysqld.service.d/oom.conf
systemctl daemon-reload && systemctl restart mysqld
cat /proc/$(pidof -s mysqld)/oom_score_adj    # 確認已生效

調多少,按程序的角色定(下表數值是經驗取值,不是標準):

程序型別 建議取值 理由
sshd、監控與維運 agent -900 到 -1000 佔記憶體很少,保住它們才能登入進去處理問題
資料庫等主業務 -500 到 -800 降低被選中的機率,但不要 -1000
報表、批處理、可重跑的任務 300 到 1000 讓它們先被犧牲

兩個坑要避開:

  • 不要給最大的記憶體使用者設 -1000。 它被完全豁免後,核心只能去殺其他程序;如果洩漏的正是它,其他程序殺光了記憶體還是不夠,最後會因為找不到可殺的程序而整機崩潰,日誌是 Out of memory and no killable processes。
  • 子程序會繼承 oom_score_adj。 給 sshd 設了 -1000,從 SSH 登入後啟動的程式也可能帶著 -1000,跑飛了反而殺不掉。設定後登入進去執行 cat /proc/self/oom_score_adj 檢查,不是 0 的話,在登入指令碼里執行 echo 0 > /proc/self/oom_score_adj 調回來,往高調不需要特權。

根本解決:先算清記憶體帳,再配 swap

oom_score_adj 只決定“殺誰”,不決定“殺不殺”。根治靠兩件事:讓各服務的記憶體上限加起來小於實體記憶體,以及給突發情況留緩衝。

先把每個服務能用到的記憶體上限列出來。以一台 16 GB 記憶體的網站伺服器為例(配置值和單程序佔用都是假設):

元件 配置 估算上限
系統與常駐服務 sshd、systemd、監控 agent 約 1 GB
MySQL 緩衝池 innodb_buffer_pool_size = 8G 8 GB,實際佔用還會再多一些
MySQL 連線 300 個連線 × 峰值每連線約 4 MB 約 1.2 GB
PHP-FPM pm.max_children = 60 × 每程序約 50 MB 約 3 GB
Redis maxmemory 2gb 2 GB,後台持久化 fork 期間寫入多時還會額外增加
合計 — 約 15.2 GB,只剩不到 1 GB 給頁快取和突發

這個配置平時跑得好好的,高峰期 PHP 程序開滿、Redis 又正好在做持久化,就會觸發 OOM。把緩衝池調到 6G、max_children 調到 40 後,合計約 12.2 GB,留出約 3.8 GB 給檔案快取和突發。單個 PHP-FPM 程序的佔用可以這樣粗估(程序名以實際為準,有的發行版叫 php-fpm8.1 之類),它用的是 RSS,會把共享記憶體重複計入,所以算出來偏保守:

ps -C php-fpm -o rss= | awk '{s+=$1; n++} END {printf "%d 個程序,平均 %.0f MB\n", n, s/n/1024}'

再配 swap。swap 的作用是把長期不用的記憶體頁換出去,並在記憶體突然吃緊時爭取時間,讓告警先於 OOM 到達;它不能替記憶體洩漏續命,配得過大,機器會在大量換頁中變得極慢,比直接 OOM 更難處理。業務伺服器配幾 GB 作為緩衝是常見做法。沒有 swap 分割槽時用 swap 檔案:

fallocate -l 4G /swapfile        # ext4、XFS 可用;不支援時改用 dd if=/dev/zero of=/swapfile bs=1M count=4096
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap defaults 0 0' >> /etc/fstab
swapon --show

最後,給可能失控的服務設上限,把 OOM 限制在它自己的 cgroup 裡:systemctl set-property 服務名 MemoryHigh=6G MemoryMax=8G。MemoryHigh 是軟上限,超過後核心會加緊回收、拖慢這個服務;MemoryMax 是硬上限,超過就只在該服務內部觸發 OOM,不連累其他程序。要提前發現記憶體壓力,核心 4.20 起可以讀 /proc/pressure/memory,some 和 full 的 avg10 持續上升說明程序已經在等記憶體了;部分發行版需在核心啟動引數中加 psi=1 才會開啟。

在管理系統裡怎麼落地

OOM 發生時,被殺的有時恰好是 sshd,或者系統在大量換頁中卡住,SSH 怎麼也連不上。這時要走不經過作業系統的 BMC 通道:Toplink DCIM 的 IPMI 遠端管理提供瀏覽器裡的 VNC 控制檯,核心列印到控制檯的 OOM 資訊在螢幕上直接可見,也能從本地終端登入排查;控制檯也卡死時,可以在同一頁面遠端重啟,每次會話都會錄影,事後能回放當時做了什麼。出租給客戶的伺服器,客戶可以在客戶自助端自己開啟控制檯、重啟;如果是某個服務一開機就把記憶體吃光、反覆觸發 OOM,還可以進入救援模式修改配置,不動硬碟資料,救援這類高危操作需要輸入 OTP 動態口令確認。

常見問題

OOM killer 可以關掉嗎?

不能真正關掉,只能換一種失敗方式:vm.panic_on_oom=1 讓記憶體耗盡時整機核心崩潰,適合有叢集自動切換的場景;vm.overcommit_memory=2 讓程式在申請記憶體時直接收到失敗,而不是事後被殺。對單台業務伺服器,這兩種結果通常都比殺掉一個程序更糟。

被 OOM 殺掉的程序會自動重啟嗎?

核心不負責重啟。由 systemd 管理、設定了 Restart=on-failure 或 Restart=always 的服務會被重新拉起,容器取決於重啟策略,手工啟動的程序就停在那裡。自動重啟只能止血,根因不解決會反覆被殺。

發生 OOM 之後要重啟伺服器嗎?

一般不用。被殺的程序已經把記憶體還回來了,系統通常會恢復正常,重新拉起被殺的服務、查清原因即可。只有機器仍在大量換頁、卡到登入不進去,或者關鍵服務反覆被殺起不來時才考慮重啟;重啟前先把 dmesg 輸出儲存下來,沒有持久化的核心日誌會隨重啟丟失。

想看看在你的機房裡怎麼用?

從一次產品示範開始,梳理你的業務鏈路。

查看價格
服務熱線 400-112-2951

讓我們聊聊你的 IDC 業務

掃碼新增企業微信,預約產品示範或申請試用。

Toplink 企業微信二維碼

長按儲存或使用企業微信 / 微信掃描

也可致電
400-112-2951
Telegram
@TopLink88