プロセスが消えるとき:深夜2時のPagerDutyの悪夢
本番環境を監視している場面を想像してみてください。すべてが安定しているように見えますが、突然、ミッションクリティカルなサービス(主要なPostgreSQLデータベースや高トラフィックのNginxインスタンスなど)が消えてしまいます。ステータスを確認しても、プロセスは存在しません。アプリケーションのクラッシュログもなく、Grafanaのダッシュボードにも予兆はありません。これが、LinuxのOut of Memory (OOM) Killerによる典型的な挙動です。
Linuxは「使わなければ失う」という哲学でメモリを管理しています。RAMが完全に枯渇すると、カーネルはOS全体のフリーズを防ぐために、冷徹な選択を迫られます。つまり、プロセスを一つ強制終了させるのです。残念なことに、カーネルは往々にして最もメモリを消費しているプロセス、つまり最も重要なアプリケーションをターゲットにします。高可用性システムを運用する人にとって、このメモリダイナミクスを習得することは不可欠です。
最近、私は4GBのRAMしか搭載していないUbuntu 22.04ノードのフリートを管理しました。どのプロセスが犠牲になってもよいかをカーネルに教えることで、ハードウェアをアップグレードすることなく、予期せぬサービス中断を約85%削減することができました。
オーバーコミットの仕組み
OOM Killerに対処するには、まずなぜそれがトリガーされるのかを理解する必要があります。デフォルトでは、Linuxは「オーバーコミット(Overcommit)」と呼ばれる戦略を採用しています。カーネルは、ほとんどのプロセスが割り当てられたメモリを同時にはフル使用しないだろうと予測し、物理的に利用可能な量よりも多くのメモリ要求をアプリケーションに許可します。これは、数人の乗客がキャンセルすることを想定して航空券をオーバーブッキングする航空会社のようなものです。
しかし、すべてのプロセスが一斉にRAMを要求すると、システムは限界に達します。この時点で、oom_killer() 関数がすべてのプロセスに対して oom_score を算出します。主に以下の3つの要因に基づいてターゲットを優先順位付けします。
- RAM使用量: メモリ消費量が多いプロセスが最初のターゲットになります。
- プロセスの生存期間: カーネルは、長時間稼働しているシステムデーモンよりも、新しいプロセスを終了させることを好みます。
- ユーザーID: rootが所有するプロセスは、ユーザーレベルのタスクに比べてわずかな保護ボーナスが得られます。
ステップ1:証拠を探す
確証が得られるまで設定を変更しないでください。カーネルはすべての実行記録をシステムログに記録しています。午前3時15分にデータベースがクラッシュしたのであれば、まずそこを確認すべきです。
# カーネルバッファから最近のOOMイベントを確認
dmesg | grep -i oom
# DebianまたはUbuntuで履歴ログを検索
grep -i 'killed process' /var/log/syslog
# RHEL, CentOS, AlmaLinuxでログを検索
grep -i 'killed process' /var/log/messages
Out of memory: Kill process 1234 (mysqld) score 500 のような行を探してください。これがあれば、OOM Killerが原因であることが確定します。また、ログにはメモリダンプの全内容も表示され、強制終了が実行された瞬間にRAMとスワップがどれだけ残っていたかが正確に示されます。
ステップ2:重要なサービスを保護する
OOM Killerを完全に無効化すべきではありませんが、その「ヒットリスト」に影響を与えることは可能です。すべてのプロセスには、/proc/[PID]/oom_score_adj にスコア調整ファイルがあります。この値は -1000(絶対に終了させない)から 1000(真っ先に終了させる)の範囲で設定できます。
例えば、重要なSSHデーモンと、バックグラウンドのログ処理スクリプトがあるとします。問題を解決するためにログインできるように、SSHを確実に維持したいはずです。以下は、プロセスを手動で保護する方法です。
# サービスのPIDを取得
pidof sshd
# 事実上、強制終了されないように設定
echo -1000 > /proc/$(pidof sshd)/oom_score_adj
手動での変更は再起動後に消えてしまいます。systemd で永続的に修正するには、サービスファイルに直接調整値を記述します。これが優先順位を管理する最もクリーンな方法です。
[Service]
# 編集コマンド: systemctl edit mysqld
OOMScoreAdjust=-500
ステップ3:オーバーコミットポリシーの強化
サーバーが頻繁にメモリ不足に陥る場合は、カーネルがメモリ要求を処理する方法の変更を検討してください。これは sysctl による vm.overcommit_memory パラメータで調整できます。3つのモードをサポートしています。
- 0 (ヒューリスティック): デフォルトのモードで、カーネルが十分なRAMがあるかどうかを「推測」します。
- 1 (常に許可): カーネルは常にメモリ要求を許可します。これは特殊な科学計算アプリには有用ですが、一般的なサーバーには危険です。
- 2 (厳格): カーネルは特定の制限(スワップ + RAMの一定割合)までしかメモリを許可しません。
安定性が優先される本番環境のデータベースには、モード2を推奨します。これにより、後でカーネルがプロセスを殺すのではなく、malloc 呼び出しを失敗させることができます。適用するには以下のコマンドを使用します。
# 即座に適用
sudo sysctl -w vm.overcommit_memory=2
# 再起動後も維持
echo "vm.overcommit_memory = 2" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
ステップ4:スワップというセーフティネット
最もシンプルな解決策が最も効果的な場合もあります。4GB RAMのサーバーが限界に達している場合、2GBのスワップファイルを追加することで重要なバッファが確保されます。ディスクベースのスワップはRAMよりも大幅に低速ですが、軽微なスパイク時にOOM Killerが発動するのを防ぐ「圧力弁」として機能します。
# 2GBのスワップファイルを作成して有効化
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# 永続化のためにfstabに追加
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
スワップが有効になったら、swappiness を確認してください。この値(0〜100)は、カーネルがどの程度積極的にデータをディスクに移動するかを決定します。ほとんどの現代的なサーバーでは、10のような低い値が最適です。
# パフォーマンス向上のため swappiness を 10 に設定
sudo sysctl -w vm.swappiness=10
VFSキャッシュのチューニング
vm.vfs_cache_pressure 設定も隠れた名機能です。これは、ディレクトリやファイルのメタデータのキャッシュに使用されているメモリを、カーネルがどの程度積極的に回収するかを制御します。デフォルトは100です。これを50に下げると、カーネルはメタデータをより長くRAMに保持します。これにより、わずかなメモリ使用量の増加と引き換えに、ディスクI/Oを削減できます。
sudo sysctl -w vm.vfs_cache_pressure=50
最後に
Linuxのメモリ管理とは、境界線を設定することです。ログからOOMイベントを特定し、oom_score_adj で重要なアプリを保護し、厳格なオーバーコミットポリシーを設定することで、負荷を適切にいなすシステムを構築できます。
常に htop などでベースラインの使用状況を監視してください。サーバーが常にスワップファイルに頼っている状態であれば、どのようなカーネルチューニングも物理RAMの増設には勝てません。これらの微調整を安定稼働と時間稼ぎのために活用しつつ、ハードウェアが限界を訴えているときはその声に耳を傾けてください。

