OOMポリシーの習得:Systemdのアウトオブメモリイベントへの応答をチューニングする

systemdを使用してLinuxのアウトオブメモリ(OOM)キラーの動作を制御する方法を学びます。このガイドでは、`OOMScoreAdjust`および`OOMPolicy`ディレクティブを活用して、メモリ不足時に終了されるプロセスに影響を与え、重要なサービスを保護する方法を探ります。systemdのOOMチューニングを習得して、システムの安定性と回復力を向上させましょう。

OOMポリシーの習得:Systemdのアウトオブメモリイベントへの応答をチューニングする

アウトオブメモリの障害は、都合の良いタイミングで発生することはほとんどありません。バッチインポートで通常より大きなファイルが処理されたり、サービスが一晩でメモリリークを起こしたり、バックアップがトラフィックの急増と重なったり、デプロイでワーカープロセス数が倍増したりします。Linuxがメモリ割り当てのために十分なメモリを解放できない場合、カーネルはOOMキラーを起動し、マシンが動作し続けられるようにプロセスを終了させることがあります。

厄介なのは、デフォルトの犠牲者があなたが選んだであろうサービスではない可能性があることです。共有ホストでは、メインAPIが死ぬ前に、リトライ可能なキューワーカーが死ぬ方が望ましいかもしれません。データベースサーバーでは、SSHと監視が生き残り、マシンを復旧できるようにしたいかもしれません。Systemdは、そのような判断のために2つの調整機能を提供します:OOMScoreAdjust=OOMPolicy=です。

OOMScoreAdjust=は、どのプロセスが選択されるかに影響を与えます。OOMPolicy=は、サービスのプロセスが強制終了された後にsystemdが何を行うかを制御します。これらは異なる問題を解決するものであり、混同すると不適切な運用手順書につながります。

カーネルがスコアリングしているもの

すべてのLinuxプロセスにはOOMスコアがあり、/proc/<pid>/oom_scoreで確認できます。スコアが高いほど、そのプロセスがOOMの犠牲者になる可能性が高くなります。カーネルはメモリ使用量やその他のコンテキストからそのスコアを導き出し、/proc/<pid>/oom_score_adjの調整値を適用します。

SystemdのOOMScoreAdjust=は、起動するプロセスに対してその調整値を書き込みます。範囲は-1000から1000です。

  • -1000は最も強力な保護を提供し、そのプロセスのOOMキリングを事実上無効にします。
  • 負の値は、プロセスが強制終了される可能性を低くします。
  • 正の値は、プロセスが強制終了される可能性を高くします。
  • 0は調整をニュートラルに保ちます。

最も安全なアプローチは、通常「重要なものをすべて保護する」ことではありません。すべてのサービスが保護されている場合、ホストがすでにメモリ不足のときにカーネルが選択できる有用な選択肢が少なくなります。少数のサービスを保護し、使い捨て可能な作業を強制終了しやすくします。

プライマリAPIサービスの場合、適度な調整で十分なことがよくあります:

[Service]
OOMScoreAdjust=-300

ジョブをリトライできるキューワーカーの場合:

[Service]
OOMScoreAdjust=500

そのワーカーはメモリプレッシャー時に最初に死ぬかもしれませんが、それが目的です。失敗したジョブはキューに戻すことができます。データベースのダウンやホストへの到達不能は、より大規模なインシデントです。

OOMPolicyが実際に行うこと

OOMPolicy=はユニットを「クリティカル」とマークするものではなく、最初に強制終了するプロセスを選択するものでもありません。サポートされている値は、continuestopkillです。

  • continue:systemdはOOMイベントをログに記録し、プロセスが残っている場合はユニットを実行したままにします。
  • stop:systemdはイベントをログに記録し、ユニットをクリーンに停止します。
  • kill:ユニット内の1つのプロセスがOOMで強制終了された場合、そのユニット内の残りのプロセスはグループとして強制終了されます。

この設定を使用して、半分生きているサービスを回避します。マルチプロセスのWebサービスがワーカーを失い、壊れた状態でトラフィックを受け入れ続ける場合、continueは障害を隠す可能性があります。OOMPolicy=killは障害を明らかにし、Restart=on-failureがクリーンな状態でサービスを復旧できるようにします。

[Service]
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

ヘルパープロセスを持つバッチジョブの場合、stopは残りのプロセスにとってそれほど急激ではないかもしれません:

[Service]
OOMPolicy=stop

カーネルによって選択されたプロセスはすでに存在しません。stopは、systemdがサービスの残りの部分に対して何を行うかにのみ影響するため、これを優雅なセーブポイントとして依存しないでください。長時間実行されるジョブは、自身の作業をチェックポイントする必要があります。

実用的なチューニングパターン

まず、サービスを3つのグループに分類します。

最初に、ホストを復旧可能に保つサービスを特定します:SSH、ネットワーキング、監視、およびプライマリワークロード。最も重要なものだけに控えめな負の調整を与えます。

次に、リトライ可能なサービスを特定します:ワーカー、インポーター、レポートジェネレーター、イメージプロセッサー、キャッシュウォーマー、開発ヘルパー。これらには正の調整を与えます。

3番目に、各サービスが1つのプロセスが強制終了された後でも安全に実行を継続できるかどうかを判断します。できない場合は、OOMPolicy=killと再起動ポリシーを使用します。

現実的なワーカーオーバーライドは次のようになります:

# /etc/systemd/system/image-worker.service.d/oom.conf
[Service]
OOMScoreAdjust=500
OOMPolicy=kill
Restart=on-failure
RestartSec=10s

プライマリアプリケーションサービスは次のようになります:

# /etc/systemd/system/api.service.d/oom.conf
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
RestartSec=5s

障害モードをテストしていない限り、OOMScoreAdjust=-1000は避けることをお勧めします。保護されたサービスがメモリリークしている場合、マシンは依然として回復方法を必要とします。

変更の適用と確認

パッケージ化されたユニットファイルを編集する代わりに、ドロップインを使用します:

sudo systemctl edit api.service

オーバーライドを保存した後、systemdをリロードし、サービスを再起動します:

sudo systemctl daemon-reload
sudo systemctl restart api.service

マージされたユニットとsystemdが認識する値を確認します:

systemctl cat api.service
systemctl show api.service -p OOMPolicy -p OOMScoreAdjust

次に、実行中のプロセスを検査します:

PID=$(systemctl show api.service -p MainPID --value)
cat /proc/$PID/oom_score_adj
cat /proc/$PID/oom_score

oom_score_adjは設定した調整値と一致するはずです。oom_scoreはプロセスがより多くのメモリを使用したり解放したりするにつれて変化する可能性があります。

インシデント後は、ユニットログとカーネルログの両方を確認します:

journalctl -u api.service --since "1 hour ago"
journalctl -k --since "1 hour ago" | grep -i oom

systemd-oomdを使用するシステムでは、以下も確認します:

systemctl status systemd-oomd
oomctl

OOMポリシーはキャパシティプランニングではない

OOMチューニングは最後の防御線です。メモリ制限、アラート、および通常のスパイクに対する十分な余裕が依然として必要です。予測可能な境界を持つサービスの場合は、cgroupメモリ制御を検討します:

[Service]
MemoryHigh=1500M
MemoryMax=2G

MemoryHigh=はハードリミットの前にプレッシャーを適用します。MemoryMax=は上限です。正確な動作はsystemdのバージョンとcgroupのセットアップに依存しますが、運用上の考え方はシンプルです:1つのサービスがホストを消費する前にそれを封じ込めます。

スワップも同様に考慮する必要があります。スワップがないと、短いスパイクが突然のOOMキルに変わる可能性があります。遅いスワップが多すぎると、レイテンシが役に立たなくなる一方でホストを生かし続ける可能性があります。OOMポリシーをスワップ、メモリ制限、再起動動作、およびアラートと一緒に見直してください。

例:1台のホスト、3つのサービス

小さな本番ホストがAPI、Redisキャッシュ、およびバックグラウンドレポートワーカーを実行しているとします。レポートワーカーは便利ですが、作業をリトライできます。Redisはレイテンシを改善しますが、アプリケーションはデータベースにアクセスすることでいくつかのリクエストを処理できます。APIは顧客向けのサービスです。

妥当な最初のパスは次のようになります:

# api.service
[Service]
OOMScoreAdjust=-300
OOMPolicy=kill
Restart=on-failure
# redis.service drop-in(このRedisインスタンスがキャッシュのみの場合)
[Service]
OOMScoreAdjust=0
OOMPolicy=kill
# report-worker.service
[Service]
OOMScoreAdjust=600
OOMPolicy=kill
Restart=on-failure

これは、あらゆる可能なケースでワーカーが最初に死ぬことを保証するものではありませんが、意図を明確にします。レポートワーカーが大きくなりすぎると、より簡単なターゲットになります。APIがプロセスの1つを失った場合、systemdは残りを強制終了し、クリーンに再起動します。Redisがキャッシュのみの場合、それを過度に保護しないことを選択するかもしれません。Redisがプライマリデータストアである場合は、異なる決定を下すでしょう。

これが、OOMポリシーを製品名ではなくサービスの役割に結び付けるべき理由です。「Redis」は自動的にクリティカルまたは使い捨て可能というわけではありません。「再構築できるキャッシュ」と「セッション状態の唯一のコピー」は異なる運用オブジェクトです。

災害を起こさずにテストする

設定が適用されているかどうかを学ぶために、本番サーバーをクラッシュさせる必要はありません。検査から始めます:

systemctl show report-worker.service -p OOMScoreAdjust -p OOMPolicy
systemctl status report-worker.service

次に、実行中のプロセスを確認します:

PID=$(systemctl show report-worker.service -p MainPID --value)
cat /proc/$PID/oom_score_adj

より深いテストには、同じsystemdバージョンとcgroupモードを持つステージングホストまたは使い捨て仮想マシンを使用します。共有の本番ボックスではなく、そこで制御されたメモリプレッシャーツールを実行します。目標は、ワーカーが強制終了されやすく、メインサービスが半分生きている状態にならず、再起動動作がジャーナルに表示されるという、大まかな動作を確認することです。

コンテナを使用する場合は、デプロイするのと同じ形状でテストします。systemdの下で直接実行されるサービスは、独自のメモリ制限を持つコンテナ内のプロセスとまったく同じように動作するわけではありません。カーネルは、ホストがグローバルにメモリ不足になる前に、コンテナの制限を適用する場合があります。その場合、コンテナランタイム、Kubernetes、またはcgroup設定が、何が死ぬかを決定する最初のレイヤーになる可能性があります。

インシデント後の読み取り

OOMイベントの後、「もっとRAMが必要だ」とすぐに飛びつくのは避けてください。時にはそれが正解です。時にはキャッシュがTTLを忘れていたり、デプロイがワーカーの同時実行数を変更したり、永続化やバックアップアクティビティがコピーオンライトメモリを急増させたりします。

3つのことを確認します:

journalctl -k --since "2026-05-24 01:00" | grep -i oom
journalctl -u api.service --since "2026-05-24 01:00"
systemctl show api.service -p Result -p NRestarts

カーネルログは通常、どのプロセスが強制終了されたかを示します。ユニットログは、systemdがどのように反応したかを示します。再起動カウンターは、サービスがクリーンに回復したか、またはフラップしたかを示します。

次に、強制終了されたプロセスを意図した優先順位と比較します。保護されたサービスが使い捨てワーカーの前に死んだ場合、ワーカーが実際にチューニングしたユニットの下で実行されていたかどうか、オーバーライドがロードされていたかどうか、別のメモリ制限が最初に作動したかどうかを確認します。選択された犠牲者がポリシーと一致しているにもかかわらず、インシデントがユーザーに影響を与えた場合、サービスの分類を変更する必要があるかもしれません。

値だけでなく理由を文書化する

OOM設定は、悪い日までユニットドロップインに静かに置かれているため、忘れられがちです。オーバーライドまたはインフラストラクチャリポジトリに、調整の理由を説明する短いコメントを残してください。

[Service]
# リトライ可能なキューワーカー。ホストプレッシャー時にはapi.serviceの前にこれを強制終了することを優先。
OOMScoreAdjust=600
OOMPolicy=kill

そのコメントは、インシデントレビュー中の時間を節約します。それがなければ、誰かが正のOOMスコアを見て、それが意図的な優先順位の決定であったことに気づかずにゼロに「修正」してしまうかもしれません。

また、設定を最後にレビューした日時を記録してください。サービスは時間の経過とともに役割を変える可能性があります。かつて使い捨てのサムネイルを処理していたワーカーが、後で支払い、エクスポート、または顧客から見えるジョブを処理するかもしれません。OOMポリシーは、サービスの元の目的ではなく、現在のリスクに従うべきです。

一般的な悪い設定

悪い設定の1つは、データベース、API、ワーカー、キャッシュ、ログシッパー、および監視エージェントをすべて同時に保護することです。それは慎重に感じられますが、カーネルに与える選択肢を減らします。優先順位を選んでください。

別の悪い設定は、子プロセスの欠落に耐えられないサービスにOOMPolicy=continueを設定することです。プロセスマネージャー、Webサーバー、またはカスタムデーモンは、ワークロードの一部がなくなった後でもユニットをアクティブに保つ可能性があります。ロードバランサーがポートが開いているかどうかのみをチェックする場合、トラフィックは劣化したサービスに流れ続ける可能性があります。

3つ目の悪い設定は、リトライ動作なしの正の調整です。サービスを強制終了しやすくする場合は、強制終了が許容可能であることを確認してください。キューワーカーの場合、それはジョブが正常に処理された後にのみ確認応答されることを意味します。バッチジョブの場合、それはチェックポイントを意味します。キャッシュウォーマーの場合、それはキャッシュが後で再構築できることを意味します。

最後に、自動再起動だけでOOMイベントを隠すのは避けてください。リークしているサービスを再起動すると時間を稼げるかもしれませんが、メモリが上昇し、サービスが死に、ユーザーが定期的な障害を見るというループを作り出す可能性もあります。プロセスの状態だけでなく、再起動回数とメモリ増加に関するアラートを追加してください。

短いランブック

実際のサーバーをチューニングするときは、繰り返し可能なチェックリストを使用します:

  1. リカバリとユーザートラフィックに必要なサービスをリストアップします。
  2. 最初に強制終了できるリトライ可能なサービスをリストアップします。
  3. 使い捨て可能な作業に正のOOMScoreAdjust値を追加します。
  4. 保護に値する少数のサービスにのみ、控えめな負の値を追加します。
  5. 部分的に実行すべきでないサービスにはOOMPolicy=killを使用します。
  6. systemctl show/procを通じて適用された値を確認します。
  7. OOMイベントが発生する前にメモリプレッシャーに関するアラートを設定します。

目標は、OOMイベントを無害にすることではありません。目標は、それらを理解可能にすることです。OOMScoreAdjust=は犠牲者を選ぶのに役立ちます。OOMPolicy=は、ユニットの残りの部分に何が起こるかを定義するのに役立ちます。これらを組み合わせることで、メモリがすでに枯渇している場合に、より予測可能な障害順序が得られます。