この記事の概要
エブリー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が発動しづらい状態にすることができます。なるべく常時起動しておいて欲しいプロセスに対して実施しておくと安定性が向上するかもしれません。
下記の pid
は stress
コマンドのプロセス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に対する理解と対策が必要です。この記事がその一助となれば幸いです。