0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VPSの常駐プロセスがOOM Killerに殺されたので、cgroups v2で檻に入れた

0
Last updated at Posted at 2026-03-22

VPSの常駐プロセスがOOM Killerに殺されたので、cgroups v2で檻に入れた

深夜2時、n8nのワークフローが全部止まった。

VPSにSSHしようとしたら応答がない。コンソールから入ってdmesgを叩いた瞬間、嫌な文字列が並んでいた。

Out of memory: Killed process 1847 (myapp) total-vm:2048000kB
ext4_journal_aborted
EXT4-fs error: remounting filesystem read-only

常駐プロセスが殺されていた。しかもファイルシステムがread-onlyになっている。load average48。4コアのVPSで48。もう何も受け付けない状態だった。

何が起きたのか

構成はこうだ。VPS(4コア/6GB)で、Wine経由の常駐アプリを2つ動かしている。24時間止められないやつ。

同じVPSで、重めのPythonスクリプトを走らせた。データ処理系の重い計算。これがメモリを3GB持っていった。

常駐アプリ×2で約2GB、Docker上のn8nやDifyで約1.5GB。合計6.5GB。物理メモリ6GBのマシンで。

Swapが溢れ、OOM Killerが目を覚まし、一番メモリを食っていた常駐プロセスを仕留めた。プロセスが死ぬ→ログ書き込みが中途半端に止まる→ext4のジャーナルが壊れる→ファイルシステムがread-only。連鎖的に全部死んだ。

正直、やらかしたと思った。止められないプロセスを、メモリ制限もかけずに野放しにしていた自分が悪い。

復旧

VPSコンソールからシングルユーザーモードで入り、fsckをかけた。

fsck -y /dev/vda1

いくつかorphan inodeが出たが、致命的な破損はなかった。再起動して常駐プロセスを立ち上げ直す。幸い、中途半端な状態で止まっていたデータはなかった。あったら冷や汗では済まない。

復旧自体は30分で終わった。問題は「次にまた同じことが起きたらどうする」だ。

二度と殺させない設計

やることは3つ。OOM Killerの優先度を下げるsystemdで永続化するcgroups v2でメモリの檻を作る

1. oom_score_adj で守るプロセスを指定する

Linuxの OOM Killer は、各プロセスにoom_scoreというスコアをつけている。メモリを多く使っているプロセスほどスコアが高い。OOM発動時、スコアが一番高いやつから殺される。

oom_score_adjに負の値を入れると、そのプロセスのスコアが下がる。つまり「こいつは後回しにしろ」と指示できる。

# 守りたいプロセスのPIDを取得してoom_score_adj設定
for pid in $(pgrep -f "myapp"); do
  echo -500 > /proc/$pid/oom_score_adj
done

-500は「かなり殺されにくい」レベル。-1000にすると完全に対象外になるが、それはやりすぎだ。どうしようもないときは殺してもらわないと、カーネルパニックで全滅する。

設定を確認する。

for pid in $(pgrep -f "myapp"); do
  echo "PID $pid: $(cat /proc/$pid/oom_score_adj)"
done

ただし、この方法には弱点がある。プロセスが再起動すると設定が消える。VPS再起動のたびに手で設定するのは現実的じゃない。

2. systemd drop-in で永続化

systemdサービスに、drop-inファイルでOOMScoreAdjustを追加する。

mkdir -p /etc/systemd/system/myapp.service.d/
# /etc/systemd/system/myapp.service.d/oom-protect.conf
[Service]
OOMScoreAdjust=-500
systemctl daemon-reload
systemctl restart myapp

これでプロセスが再起動しても、自動的にoom_score_adj=-500が適用される。drop-inファイルなので、サービス本体の.serviceファイルを直接いじらなくていい。アップデートで上書きされる心配がない。

3. cgroups v2 memory slice でメモリに上限を設ける

ここが本丸。OOM Killerの優先度を下げるだけでは、根本的に解決していない。暴走プロセスがメモリを食い尽くせば、結局全体が巻き添えになる。

やるべきは「守りたいプロセスをsliceに入れてメモリ上限を設ける」こと。檻の中で動かせば、暴走しても他に影響しない。逆に、他が暴走してもslice内のメモリは守られる。

systemdのsliceを使う。

# /etc/systemd/system/myapp-memory.slice
[Unit]
Description=MyApp Memory Limit Slice

[Slice]
MemoryMax=3G
MemoryHigh=2560M

MemoryMaxはハードリミット。3GBを超えたらOOM Killerが介入する。MemoryHighはソフトリミット。2.5GBを超えるとカーネルがメモリ回収を積極的にかけ始める。この2段構えが地味に効く。

サービスファイルで、このsliceに所属させる。

# myapp.service の [Service] セクションに追加
[Service]
Slice=myapp-memory.slice
systemctl daemon-reload
systemctl restart myapp

これで、常駐プロセスは合計3GBの檻の中で動く。Pythonスクリプトが暴走しても、常駐アプリのメモリ領域には手を出せない。

現在の使用量はsystemctl status myapp-memory.sliceで見える。

systemctl status myapp-memory.slice
# Memory: 1.8G (max: 3.0G available: 1.2G)

現在の監視体制

cgroups v2で檻を作った後も、Swapの使用量は注視している。

free -h
#               total   used   free   shared  buff/cache   available
# Mem:          5.8Gi  4.2Gi  312Mi    48Mi      1.3Gi      1.3Gi
# Swap:         2.0Gi  1.4Gi  624Mi

Swap 1.4GB/2.0GB。正直まだ高い。常駐アプリ + Docker群を6GBに詰め込んでいる以上、ある程度のSwap使用は避けられない。ただ、MemoryHighのおかげで急激なスパイクは抑えられている。

n8nのワークフローで30分おきにSwap使用率をチェックして、80%を超えたらChatworkに通知を飛ばすようにした。次の一手はVPSのメモリ増設か、重い計算処理の別環境分離だろう。

まとめじゃないけど

止められないプロセスを守れないサーバーは、サーバーじゃない。

oom_score_adjは5分で設定できる。cgroups v2のsliceも、systemdの設定ファイルを2つ書くだけだ。30分もあれば終わる。その30分をサボったせいで、深夜2時にfsckを叩く羽目になった。

VPSでWine経由のプロセスを動かしている人がどれだけいるかわからないが、Wine経由のプロセスはoom_scoreが高くなりがちだ。メモリの檻、作っておいて損はない。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?