every Tech Blog

株式会社エブリーのTech Blogです。

OOM-Killer (Out-Of-Memory Killer) について学ぶ

この記事の概要

エブリーTIMELINE開発部の内原です。

サービスを運用していると時々遭遇するOOM-Killerについて、改めて学んでみたのでまとめます。

OOM-Killerはどういう理由で発生するのか、なにが起きているのか、どう対処すればいいのか、などを解説します。

なおこの記事では、Linux上での説明を前提としています。

OOM-Killerとは

サーバ上で、以下のようなメッセージを見たことがあるのではないでしょうか。

[ 2291.984774] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/system.slice/amazon-ssm-agent.service,task=python3,pid=28268,uid=1001
[ 2291.988154] Out of memory: Killed process 28268 (python3) total-vm:536656kB, anon-rss:243172kB, file-rss:4kB, shmem-rss:0kB, UID:1001 pgtables:544kB oom_score_adj:0

このメッセージからは、python3プロセスがOOM-Killerによって強制終了されたことがわかります。

OOM-Killerが発生する理由

当然ながら、OOM-Killerはメモリが足りなくなったときに発生します。ただ、メモリが足りなくなったとは具体的にどのような状況かというと、以下の条件に当てはまった場合です。

  • プロセスによるメモリアクセスでページフォルトが発生し
  • オーバーコミットにより新たなメモリの割り当てが必要となり
  • 物理メモリ、スワップメモリいずれにおいても必要な領域が確保できない場合

プロセスに対するメモリ割り当てを行うタイミングについて

メモリが割り当てられるタイミングとは特定プロセスがメモリを必要とした時ということにはなりますが、その瞬間は実装コードにおいてメモリを使用している状況とは一致しないことも多いです。

理由として、OSはプロセスのメモリを仮想アドレス空間(後述)として管理しており、プロセスにおけるメモリ空間と物理メモリ空間との対応が異なっているためです。

実装コード上で一見大量のメモリを確保しているように見えても、あくまで仮想アドレス空間上での割り当てのみ行われており、そのメモリが実際に必要となるタイミングになるまで物理メモリとの割り当てが行われないことがあります。

このような処理は、オーバーコミットやオンデマンド・ページングといった技術(いずれも後述)で実現されています。

仮想アドレス空間とページフォルト

仮想アドレス空間とは、プロセスが認識しているメモリ空間のことです。プロセスはこの空間を使ってメモリにアクセスをしますが、仮想アドレスと物理アドレスとは一致しておらず、OSが対応管理表を用いてアクセス時にアドレス変換を行います。

仮想アドレス空間に物理メモリとの対応付けが行われていない場合、そのアドレスにアクセスした場合にページフォルトが発生します。このページフォルトをトリガーとして実際のアクセスすることになるメモリ領域に割り当てが行われます。

これにより例えば以下のようなことが可能になります。

  • 物理メモリをスワップファイルに退避することによって、物理メモリを仮想的に拡張することができる
  • プロセス間でメモリ空間を隔離することができる(個々のプロセスは個別の物理メモリ割り当てを持っているため)
  • プロセス間でメモリ空間を共有することができる(共有メモリ)

なおメモリ割り当てはページという単位(一般的には4KB)で行われます。

オーバーコミット

オーバーコミットとは、物理メモリ容量を超えてプロセスに仮想メモリ空間を割り当てることです。プロセスがメモリを要求したとしても、実際にそのメモリを使用しないケースも多いため、問題になることは少ないという考え方です。

その代わり、実際にメモリを使用するタイミングでメモリ不足に陥る可能性があります。

オンデマンド・ページング

オンデマンド・ページングとは、プロセスがメモリを使用するタイミングで初めて仮想メモリ空間と物理メモリ空間との割り当てを行うことです。

オーバーコミットと組み合わせて用いることで、必要な物理メモリ使用量を削減することができます。

OOM-Killerが行っていること

OOM-Killerが発動した場合、起動しているどれかのプロセスを強制終了させることでメモリ確保を試みます。

この際にOSは、以下のような状態のプロセスを優先的に選択しようとします。

  • 使用メモリ量が多いプロセス
  • OOM補正値が高いプロセス
    • /proc/<pid>/oom_score_adj で設定される値。-1000 ~ +1000 の範囲で、値が高いほどOOM-Killerの対象となりやすい。-1000 であればOOMスコアが0になる
  • プロセスnice値やその他ヒューリスティックな要素

正確には上記を考慮して /proc/<pid>/oom_score というOOMスコアがリアルタイムで算出されるのですが、OOM-Killerが発動したタイミングでOOMスコアが最も高いプロセスが選択されます。

実例

stress コマンドでメモリ消費を行い、OOM-Killerが発動する様子を確認してみます。

空きメモリ容量は1GB未満(700MB程度)、スワップファイルはなしの環境とします。

$ free -m
               total        used        free      shared  buff/cache   available
Mem:             949         159         696           0          92         673

512MBのメモリを消費します。

$ stress --vm 1 --vm-bytes 512M --vm-keep
stress: info: [32679] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd

空きメモリ容量は200MB程度になりました。

$ free -m
               total        used        free      shared  buff/cache   available
Mem:             949         668         187           0          92         164

ここで新たに512MBのメモリ消費します。

$ stress --vm 1 --vm-bytes 512M --vm-keep
stress: info: [33059] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd

すると元々起動していたほうの stress コマンドが強制終了され、OOM-Killerが発動したことがわかります。

$ stress --vm 1 --vm-bytes 512M --vm-keep
stress: info: [32679] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [32679] (425) <-- worker 32680 got signal 9
stress: WARN: [32679] (427) now reaping child worker processes
stress: FAIL: [32679] (461) failed run completed in 67s

今回のケースだと、先に起動していたほうがOOMスコアが高い傾向があり、結果として新しく起動したプロセスが残る状況でした。

対処1

メモリ使用量を削減できないか検討します。

対処2

物理メモリを増設できないか検討します。

対処3

スワップファイルを設定することで、物理メモリ容量を超えてメモリを利用できるようにします。ただしスワップメモリの性能は物理メモリよりも低いためパフォーマンスが極端に低下することが多いです。

瞬間的にメモリ使用量が増加するようなケースであればスワップメモリを利用することでOOM-Killerの発生頻度を抑えることができますが、恒常的にメモリが不足しているような状況であれば対処1や対処2を実施することをお勧めします。

スワップファイルを設定する手順は以下の通りです。

# dd if=/dev/zero of=/swapfile bs=1M count=512
# chmod 600 /swapfile
# mkswap /swapfile
# swapon /swapfile
$ free -m
               total        used        free      shared  buff/cache   available
Mem:             949         619         248           0          81         219
Swap:            511          43         468

対処4 OOMスコアを調整する

特定のプロセスに対してOOMが発動しづらい状態にすることができます。なるべく常時起動しておいて欲しいプロセスに対して実施しておくと安定性が向上するかもしれません。

下記の pidstress コマンドのプロセスIDです。

# echo -1000 >/proc/<pid>/oom_score_adj

この状態で再度512MBのメモリ消費を行おうとしますが、既存のプロセスのOOMスコアが低いため、新しく起動したプロセスが強制終了されます。

$ stress --vm 1 --vm-bytes 512M --vm-keep
stress: info: [35408] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: FAIL: [35408] (425) <-- worker 35409 got signal 9
stress: WARN: [35408] (427) now reaping child worker processes
stress: FAIL: [35408] (461) failed run completed in 6s

まとめ

この記事では、OOM-Killerはどういう理由で発生するのか、なにが起きているのか、どう対処すればいいのか、といったことを解説しました。

安定したサービス運営を行うためには、OOM-Killerに対する理解と対策が必要です。この記事がその一助となれば幸いです。