OOM Killer の目的は何か?
まずは何故OOM Killerが発生しているのかについて、ざっくりイメージをつかみましょう。linux kernelはプロセスからの「メモリくれ」という要求に対してたぶん足りそうだという場合に「OK」といって渡します。実際のメモリ割り当てはアクセスが発生するタイミングまで遅延させます。これを遅延アロケーションといい、だいたいにおいてうまく動きます。ただし必ずうまくいくと保証されているわけではないので破綻することがあります。OOM Killerはこの遅延アロケーションが破綻しそうなときに、適当にプロセスを殺すことでメモリを開放してやろう、そうやってメモリを開けて動作を継続したほうが全滅するよりはマシだろうという緩和策です。
「半端に動くんじゃなくて死んでくれた方がいいです」という場合には、 sysctl で以下のように設定して、OOM Killerを動作させるのではなくカーネルをpanicさせることができます。
vm.panic_on_oom = 1OOM Killerは破綻したときの緩和策だったので、OOM Killerを止めて破綻をより直接的にシステム停止に繋げています。きっちり死んでくれるのでOOM Killerよりわかりやすいですね。
何故OOM Killerが発生しているかを調査する
できればOOM Killerを避けたいですが、避けるためには何故OOM Killerが発生しているのかを調査する必要があります。これをおこなうためには、まずさきほどのpanic_on_oom設定をおこないます。加えてpanic時にメモリダンプを取得する仕組み(いろいろありますがRHELやFedoraだとkdump)を仕込みましょう。これでpanicしたときのダンプを調査することで、多くの場合OOM Killerの発生原因を推定できます。
RHELであれば「カーネルクラッシュダンプガイド」にダンプの設定と解析ツールについての解説があります。
https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/7/html/Kernel_Crash_Dump_Guide/index.html
対処その1. アプリケーションに対してストレージを速くする
メモリにはざっくり3種類あります。- ファイルやブロックデバイスをバックエンドとするfile mapped page
- バックエンドはなく、必要になるとswap領域にバックエンドを作るanonymous page
- 特に使われていないfree
アプリケーションが何かしらストレージへ書きだすとき、まずは書き出すべき情報をメモリ上に保持します。このようなメモリをDirtyと呼びます。
Dirtyなメモリはストレージへの反映による永続化が必要な情報を持っているので、ストレージに書きだしをおこなうまでは消せません。恒常的にストレージの負荷が高いとDirtyなメモリがどんどん増えていきます。
さきほどのメモリダンプを解析ツールに食わせると、dirtyなページが多くなっているのですぐわかります。このような場合、対処としては以下があります
- ストレージの障害をチェックする。何かデグレードが発生して遅くなっている可能性を排除しましょう。 高いストレージだとストレージ側で統計情報がとれます。これも確認しましょう。
- ストレージを増強する。根本対策です。
- アプリケーションの書きだしを遅くする。dirtyなページを書きだすプロセスの実行を遅くします。
アプリケーションがdirect I/Oとよばれる手法を利用している場合に限られますが、cgroupで制限することができます。systemdのunit内にBlockIOWriteBandWidth=で利用する帯域幅の上限を設定することができます。指定された上限にを越えて書きこみをしようとするとアプリケーションは一時停止します。
https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/7/html/Resource_Management_Guide/sec-Modifying_Control_Groups.html#sec-Modifying_Unit_Files
- 書きだしを遅くする(その2)。共有ストレージを持つクラスタ構成の場合、ストレージに対しての計算ノード数を減らします。
- 書きだしを遅くする(その3)。メモリがでかいときはvm.dirty_ratioを変更したりすると良いケースもあります。これはdirtyなページが何パーセント溜まったら、新規に書きだしを行おうとしているプロセスを一時停止するかの指標です。
- メモリを増やす。ストレージへの負荷が適度に増減する場合、次に負荷が減るまでなんとか持てばいいので、その余裕がある時間をかせぎます。場合によりますが、メモリがとても少ない場合をのぞいてあまり有効な対応ではありません。
対処その2. 積極的にswapさせる
利用しているアプリケーションのメモリ管理があまりうまくできていないと、実際の利用はそれほどでもないのにanonymousとして確保するメモリ量がじわじわ増大していきます。このような場合、アプリケーションによりますがswapを積極的に行うことも一つの手です。sysctlで、以下のようにするとRHELではfile mappedと同じ程度の基準でanonymousメモリをswapに吐きだします。
vm.swappiness = 100ゆっくりとしたメモリ消費増加への対策としてはswapはそれなりに有効です。もちろんswapがあふれると破綻しますから適宜swap使用量を監視する必要があります。
swapについてはとにかく発生させたくないと、拒否反応を示す人がいます。
「swapで遅くなる」という現象のほとんどは、1) anonymous pageを一旦swapへ書きだしてメモリから除いたあと、2) もう一度アクセスしようとしたときに発生します。
この2の動作をswap inと呼びます。vmstatだとsiと書いてある欄で表示されます。これが継続的に発生する場合、swapによるパフォーマンス劣化が発生していると考えてまず間違いありません。この場合、アプリケーションのメモリ利用パターンがswapの積極的な利用に向いていません。
swap inによるパフォーマンス劣化が継続的ではなくちょこちょこ程度で起きる場合、主要ではないアプリケーションがswap outしている可能性が考えられます。この影響を緩和したい場合「メモリを増設するのは高いのでswap領域をSSDにしてしのぐ」というアイデアには一考の余地があります。
利用しているアプリケーションに、ある程度予想がつくゆっくりとしたメモリ利用の増加がある場合にはswapと適当な頻度でのサービス再起動の組み合わせは有効です。ただしswap inが継続的に発生する場合はswapによる緩和策はあまり向いていません。
対処その3. アプリケーションのメモリ消費を制限する
swapが向いていないアプリや、メモリの大量消費が突発的に発生するアプリケーションもあります。後者は70%くらいバグな気がしますが、残念ながらそういうアプリケーションでも使わないといけないシチュエーションはあります。そのような場合、cgroupを利用してアプリケーションのメモリ消費を制限することが有効です。systemdを利用しているディストリビューションでは、systemdがcgroupの設定を管理します。 RHELであれば以下に和訳ドキュメントがあるので読みましょう。
https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/7/html/Resource_Management_Guide/index.html
てっとり早く答えにたどりつきたい人は以下ページを見て、MemoryLimitを設定しましょう。 これで、指定したサービスの消費メモリを制限できます。制限に触れるとOOM Killerが動作しますが、このcgroup内のプロセスだけを殺しますのでサービスだけが死にます。周りにあまり迷惑がかからないのはいいですね。
https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/7/html/Resource_Management_Guide/sec-Modifying_Control_Groups.html#sec-Setting_Parameters_from_the_Command-Line_Interface
これに加えて、サービス停止時の再起動設定もsystemdのunitに書くことができます。ただし自動再起動の設定をするのは「大量消費→OOM Killer→再起動→大量消費→……」 のタイトなループに陥る可能性が排除できている場合に限定しましょう。このループが数日や数ヶ月など、許容できる場合なら
Restart=yesとunitに書くことでsystemdが再起動の面倒もみてくれます。
対処その4. メモリを増やす
「さっきあまり有効でないって書いてたのに!?」と思うかもしれませんが、アプリケーションが必要とするメモリ量の見積りが単純に低すぎる場合があります。dirtyなページがあまり多いわけでもなく、swapさせてもswap inが発生しつづける場合は、アプリケーションが必要としている量のメモリを用意できていない場合が考えられます。可能であればアプリケーションの処理を勘案して必要サイズを推定しましょう。
メモリ使用量を調査するためのツールについては以下ドキュメントに紹介されています。
https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/7/html/Performance_Tuning_Guide/sect-Red_Hat_Enterprise_Linux-Performance_Tuning_Guide-Memory-Monitoring_and_diagnosing_performance_problems.html
メモリを大量消費するアプリケーションには、自身がどれくらいのメモリを消費するか調整するための設定が用意されていることが多いです。これを増減させて消費メモリ量の増減とパフォーマンスへの影響を観察すると落としどころがみつかるかもしれません。
HPCなどでは「でかいデータを扱うからでかいメモリが要るんだよ!」 みたいなケースもあります。こういうときは金と物理(メモリ)の力で殴るしかないです。
対処ではない: oom_score_adj
OOM killerの調整ということで検索すると名前がよくでてくるoom_score_adjですが、特定プロセスをOOM killerの対象外にしたり対象になる確率を上げたり下げたりということができます。しかしOOM killerが発生した原因であるメモリ逼迫については何も改善しないので、あまり意味はありません。
特定のプロセスが死ななければ他がランダムに殺されても大丈夫でしょうか? 普通はランダムに殺されても問題ないというプロセスはほぼ無いはずです。不要なプロセスが存在するなら、特にscoreとかいじる前に単純に起動しないようにする方がいくらかマシです。
oom_score_adjを活用できるシチュエーションをどうにか考えだしてみると、
1) 大事なプロセスに関連したものについてはOOM killerの対象外にしておく
2) メモリ逼迫を検出するためのカナリア役として優先的に殺されるプロセス(無意味にメモリを確保する)を仕込んでこれを監視に利用
3) カナリア役が殺され次第できるだけ正常な終了手順で大事なプロセスを終了
というような仕組みを作り込む時くらいではないかと思います。OOM killerが動きだしたあとに大容量メモリを確保しているであろう大事なプロセスの終了処理がうまく動くかは非常に疑問ですが、終了処理が追加メモリをあまり使わず動作する場合には使えそうです。
まれに有効: vm.overcommit_memory設定によるovercommitの禁止
vm.overcommit_memoryによるovercommitの禁止は「ほとんど全てのアプリケーションが必要より多くメモリをリクエストしてくる」ため、非常に効率は悪くなりますがメモリ確保の要求量(割り当て量ではない)が特定の値を越えるとOOM Killerによるプロセスの終了が発生します。早く失敗することで延々処理したあと死ぬよりはマシなのではないか、という考え方ですね。特定のアプリケーションがメモリのほとんどを占め、必要分ギリギリしか要求しないことが分かっている、なおかつ他のプロセスはほぼ動作していない。という前提が満たせる場合はいいのですが、なかなか厳しいです。実際のところアプリケーションによっては通常の利用時に消費されるメモリ量の数十倍から100倍くらいのメモリを要求することもごく普通にあります。極めて特殊な、アプリケーションを選ぶ対策と言ってよいかと思います。
おまけ
いろいろいじりたい人にはRHELのドキュメント「パフォーマンスチューニングガイド」がまずはおすすめ。
https://access.redhat.com/documentation/ja-JP/Red_Hat_Enterprise_Linux/7/html/Performance_Tuning_Guide/chap-Red_Hat_Enterprise_Linux-Performance_Tuning_Guide-Performance_Features_in_RednbspHat_EnterprisenbspLinuxnbsp7.html