スワップの弁護:よくある誤解を解く

(This post is also available in English.)

この記事は In defence of swap: common misconceptions著者の Chris Down さんの許可 を得て Hiroaki Nakamura が日本語に翻訳したものです。 原文のライセンスCC BY-SA 4.0 であり、翻訳のライセンスも同じく CC BY 4.0 とします。


長文を読みたくない方への要約:

  1. スワップを持つことは正しく機能するシステムのかなり重要なポイントです。 スワップが無ければ、まともなメモリ管理を実現することは難しくなります。
  2. スワップは一般的に緊急事態用のメモリを取得するためのものではなく、メモリの回収を平等に効率的に行うためのものです。 実のところ「緊急事態用のメモリ」は一般的に盛大に悪影響を及ぼします。
  3. スワップを無効にすることはメモリ争奪の状況下においてディスク I/O が問題になることを回避してくれるわけではありません。 単にディスク I/O スラッシングの対象を anonymous ページから file ページにシフトさせるだけです。 これは効率が悪くなるだけでなく、回収元の選択対象となるページのプールが小さくなり、発端となっているメモリ争奪の状況をより悪化させることに寄与してしまいます。
  4. 4.0 以前のカーネルのスワップ機構はたくさんの落とし穴がありました。 そして、必要以上にやたらとページをスワップアウトしたがるせいでスワップについてネガティブな認識を多くの人が持つようになりました。 4.0 以降のカーネルでは状況ははるかに良くなっています。
  5. SSD では anonymous ページをスワップアウトするのと file ページを回収するのはパフォーマンスとレイテンシの観点では実質的には同じです。 より古い回転ディスクではスワップからの読み込みはランダムリードになるためより遅くなります。 ですので vm.swappiness をより低い値に設定することが理にかなっています(vm.swappiness については後述)。
  6. スワップを無効にすることは OOM が近いときにを病的な挙動を防いでくれるわけではありません。 一方、スワップがあれば OOM の発動を引き延ばせる可能性があるのは事実です。 スワップありあるいはなしでシステムグローバルの OOM キラーが実行されるか、また実行される時期が早いか遅いか、に関わらず結果は同じです。 つまりあなたには挙動が予測できないシステムが残されることになります。 スワップを持たないことでこれを回避することはできません。
  7. cgroup v2 の memory.low などを使うことでメモリ逼迫の状況下でスワップの挙動を改善しスラッシングを防ぐことができます。

カーネルのメモリ管理と cgroup v2 を改善する仕事の一環として、私は多くのエンジニアとメモリ管理についての考え方、特にメモリ逼迫時のアプリケーションの挙動とメモリ管理のために内部で使われるオペレーティングシステムのヒューリティックについて、話し合ってきました。

これらのディスカッションで繰り返されるトピックはスワップです。 スワップは激しい論争の的になり、長年 Linux に取り組んでいる人たちにですらほとんど理解されていないトピックです。 多くの人はスワップを無意味だと考えていたりあるいは盛大に悪影響を及ぼすと考えています。 しかしそれはメモリが十分でなくディスクがページングに必要な容量を提供するための必要悪だった時代の古い考え方です。 この発言は近年いまだにかなりの頻度であちこちで見られ、私は同僚、友人、業界の仲間たちとディスカッションし、過去に比べて格段に増えた物理メモリを持つモダンなコンピュータ上でもスワップが引き続き有用な概念であることを理解するのを助けてきました。

スワップの目的については多くの誤解があります。多くの人はスワップを単に緊急事態の際に利用できる「遅い追加メモリ」として見ていて、通常の負荷の際にオペレーティングシステム全体として健全に稼働することに貢献できることを理解していません。

私たちの多くは「Linux はメモリをたくさん使いすぎる」、「スワップは物理メモリの 2 倍のサイズにすべき」などというよくある言い回しを聞いたことがあるでしょう。 これらを打ち消すのはささいなことですが、これらについてのディスカッションは近年微妙なニュアンスを持つようになってきており、「役に立たない」スワップの神話は単純なアナロジーで説明できる何かというよりヒューリスティックと神秘に根ざしたものになり、検証するためにメモリ管理についての理解以上の何かが必要となっています。

このポストは Linux システムを管理している人で、少なすぎるスワップやスワップなしで稼働させたり vm.swappiness を 0 に設定して稼働させることへの反論を聞くことに興味がある人を主に対象としています。

背景

Linux のメモリ管理内で動いている基本的な内部の仕組みの一部について共通の理解がなければ、通常のオペレーションにおいてスワップを持つこととページをスワップアウトすることがなぜ良いことであるかについて話すのは難しいです。 ですのでまず同じページに立つことを確実にしましょう。

メモリの種別

Linux には多くの異なるメモリの種別があり、それぞれが固有の特性を持っています。 これらの微妙な差異を理解することがなぜスワップが重要であるかを理解するための鍵です。

例えば、あなたのコンピュータ上で動いている各プロセスに対してコードを保持する責任を負う ページ(メモリの「ブロック」、たいていは 4KiB)があります。 またプログラムによってアクセスされるファイルに関連したデータやメタデータをキャッシュすることに責任を負うページもあります。 これらは ページキャッシュ の一部であり、ここでは file メモリと呼ぶことにします。

また、コード内部でメモリ割り当てに責任を負うページもあります。 例えば、 malloc によって新しいメモリが割り当てられたときや mmapMAP_ANONYMOUS フラグを使う時です。 これらは何かによる裏付けが無い(訳注:バッキングストアを持たない)ので "anonymous" ページと呼ばれます。 ここでは anon メモリと呼ぶことにします。

他のタイプのメモリもあります。共有メモリ、スラブメモリ、カーネルスタックのメモリ、バッファなどです。 ですが、 anonymous メモリと file メモリが最もよく知られており理解しやすいので、ここでの例ではそれらを使います。 が、ここでの例はこれらの他のタイプにも同様に当てはまります。

回収可能なメモリと回収不可能なメモリ

メモリの特定の種別について考えるときの最も根本的な質問の 1 つはそのメモリが回収可能であるかそうでないかということです。 ここでは「回収」とはシステムがデータを失うことなく物理メモリからその種別のページをパージすることを意味します。

あるページ種別に対しては、これはたいていはとても自明です。 例えば クリーン な(修正されていない)ページキャッシュメモリの場合は、パフォーマンスのためにディスク上の何かを単にキャッシュしているだけなので、何か特別な操作をする必要もなくページをドロップできます。

あるページ種別に対しては、これは可能ですが、自明ではありません。 例えば ダーティ な(修正された)ページキャッシュメモリの場合は、修正内容をまだディスクに反映していないので単にページをドロップは出来ません。 そのため回収を拒否するか、まず変更内容をディスクに反映してからこのメモリをドロップする必要があります。

あるページ種別に対しては、これは不可能です。 例えば、上で説明した anonymous ページの場合は、メモリ内にのみ存在し、他にバッキングストアが無いので、メモリ内に維持し続けるしかありません。

スワップの性質について

Linux のスワップの目的についての説明を探すと、多くの人がスワップを緊急事態の際に利用できる物理 RAM の単なる拡張であるかのように話しているのを否応なく目にするでしょう。 例えば、以下は私が Google で "what is swap" で検索した検索結果の上位からランダムに選んだものです。

スワップは実質的に緊急メモリです。 あなたが持っている RAM よりも多くの物理メモリをシステムが一時的に必要とするときのために取っておく領域です。 スワップは遅くて非効率という意味で「悪」と考えられており、システムがスワップを定常的に使う必要があるのであれば、それは明らかに十分なメモリを持っていないということです。 […] あなたの要求を全て処理するのに十分な RAM があり、それを超えることが絶対起こらないと言えるなら、スワップスペースなしでシステムを稼働することは完全に安全と言えるでしょう。

誤解のないように言うと、この記事の内容についてこのコメントの投稿者を責めるつもりは全くありません。 これは多くの Linux システム管理者で「常識」として受け入れられ、スワップについて話す際に彼らに聞くと誰かが答えるたぶんもっともありそうな答えの 1 つでしょう。 しかし、これは残念ながらスワップの目的と使い方、特にモダンなシステムでの、についての誤解でもあります。

上記で、 anonymous ページの回収が「可能ではない」ことについて述べました。 anonymous ページはメモリからパージしたときにフォールバックするバッキングストアがないという性質のため、回収してしまうとそれらのページのデータを完全に失うことになるからです。 でもそれらのページに対してそれができるストアを作れるとしたらどうでしょう?

実は、それこそがまさにスワップの目的なのです。 スワップはこれらの「回収不可能」に見えるページを必要に応じてストレージデバイスにページアウトすることを実現するためのストレージエリアです。 このことは anonymous ページがクリーンな file ページなど、より明らかに回収可能な種別のページと今や同程度に回収対象としてふさわしいと考えることができるようになったことを意味し、利用可能な物理メモリをより効率的に使用できるようになります。

スワップは主に回収の平等さのための機構であり、緊急事態の際の「追加のメモリ」ではないのです。スワップがあなたのアプリケーションを遅くする正体ではありません。全体的なメモリ争奪の状態に入ることがあなたのアプリケーションを遅くする正体なのです。

ではこの「回収の平等さ」の元でどんな状況が anonymous ページの回収を合法的に選択するのでしょう? 以下は抽象的ないくつかの珍しくないシナリオです。

  1. 初期化の間、長時間実行されるプログラムはたくさんのページを割り当てて利用するかもしれません。 これらのページはシャットダウンやクリーンアップの一部として使われるかもしれませんが、プログラムが一旦(各アプリケーションに固有の意味で)「開始」すると不要です。 これは初期化のために大きな依存を持つデーモンでは非常によくあることです。
  2. プログラムの通常の稼働中、たまにしか使われないメモリを割り当てるかもしれません。 これらのページに必要に応じてディスクからページインする メジャーフォールト を要求し、代わりにより重要な他のことにメモリを使うほうがシステム全体のパフォーマンスにとってはより合理的かもしれません。

スワップのあるなしで何が起こるかを調べる

典型的な状況でスワップのあるなしでパフォーマンスがどうなるかを見てみましょう。 cgroup v2 についての私の講演 で「メモリ争奪」についてのメトリクスについて話しています。

メモリ争奪がないか程度が低い時

中程度あるいは高度にメモリ争奪がある時

メモリ使用が一時的にスパイクしている時

オーケー、システムスワップが必要なことは分かったけど、個別のアプリケーションに対してどのように調整すれば良いでしょうか?

まさかこの記事全体を通して私が cgroup v2 の話を差し込まずに終わるとは思って無いですよね? ;-)

もちろん、一般的なヒューリスティックなアルゴリズムがいつも正しくあることは難しいです。 ですので、あなたがカーネルに助言を与えられることが重要です。 歴史的には実行可能な唯一のチューニングはシステムレベルで vm.swappiness を使うことだけでした。 これには 2 つの問題があります。 vm.swappiness はより大きなヒューリスティックなシステムの小さな一部に注入するだけなのと、 プロセス群のより小さなセットではなくシステム全体に対する設定であるので、理解するのが信じられないほど難しいです。

mlock を使ってページをメモリ内にロックすることも出来ますが、これはプログラムコードを修正するか、 LD_PRELOAD で楽しく遊ぶか、実行時にデバッガで恐ろしいことをするかのいずれかが必要です。 さらに VM ベースの言語ではこれはあまりうまく動きません。 というのは、一般的にはアロケーションについてあなたが制御できないので結局 mlockall を使わざるを得なくなり、それでは実際に大切にしたいページに対して正確に制御できないからです。

cgroup v2 は memory.low の形式で cgroup 単位の調整が可能で、これによりメモリ使用量がある閾値を下回ったら他のアプリケーションからメモリを回収することを優先するようカーネルに伝えることができます。 これによりカーネルが私たちのアプリケーションの部分をスワップアウトするのを防ぐだけでなく、メモリ争奪時に他のアプリケーションから回収することを優先するようにできます。 通常の状況下では、カーネルのスワップのロジックは一般的にはかなり良く、ページを機会に応じてスワップアウトするのを許可することは一般的にシステムのパフォーマンスを増大します。 大量のメモリ争奪下でのスワップのスラッシングは理想的ではないですが、それはスワップ機構の問題というよりも単にメモリ全体を使い切ったことによる特性です。 これらの状況では、メモリ逼迫の圧力が積みあがるときに致命的でないプロセスが自分をキルすることでフェールファーストすることがたいていは望ましいでしょう。

これには単に OOM キラーに頼るということはできません。 OOM キラーはシステムが深刻的に不健全になりしばらくそれが続く状態に 既に 入った時に切迫した障害の状況になったときにしか実行されないからです。 あなたは OOM キラーについて一度でも考える前に、適切な機会にあなた自身で状況を処理する必要があります。

しかし、従来の Linux のメモリカウンタを使ってメモリ逼迫が起こっているを判断することはやや難しいです。 メモリ消費量、ページスキャン、などある程度関係する何かはありますが、なんとか関係している程度でしかありません。 そしてこれらのメトリックだけから効率的なメモリ配置からメモリ争奪への切り替わり傾向が起きていることを判断するのは非常に難しいです。 Facebook では Johannes が先陣を切る私たちのグループがあり、メモリ逼迫をもっと容易に明らかにする新しいメトリクスの開発に取り組んでいますので、将来にはこれが役立つでしょう。 これについてもっと聞くことに興味があれば cgroup v2 についての私の講演で検討中の 1 つのメトリックについて詳細に触れています

チューニング

ではスワップのサイズはどれぐらい必要なのでしょう?

一般に、オプショナルなメモリ管理に必要なスワップスペースの最小の量は、アプリケーションによってめったに再びアクセスされないメモリにピン止めされる anonymous ページの数とそれらの anonymous ページを回収する回数に依存します。 後者は主にアクセス頻度の低い anonymous ページに道を譲るためにどのページがもうパージされなくなるかの問題です。

山ほどのディスクスペースと最近 (4.0+) のカーネルをお持ちでしたら、より大きなスワップは小さなスワップよりほぼ常に良いです。 より古いカーネルの kswapd 、これはスワップを管理する責任を負うカーネルプロセスの 1 つです、は歴史的にスワップをより多く持つほどメモリをアグレッシブにスワップアウトすることに非常に熱心すぎるものでした。 最近では、大きな容量のスワップスペースがある時のスワッピングの振る舞いは著しく改善されています。 カーネル 4.0+ を稼働していれば、モダンなカーネル上でより大きなスワップを持つことは熱心すぎるスワップに終わることはないはずです。 ですので、スペースに余裕があれば、数 GB のスワップサイズを持つことはモダンなカーネルでは有効な選択肢でしょう。

もしディスクスペースにもっと制約があるなら、回答はあなたが決めるトレードオフと環境の性質に大いに依存します。 理想的にはあなたのシステムを通常とピークの(メモリの)負荷の際に最適に稼働させるために十分なサイズのスワップを持つべきでしょう。 私がお勧めするのは 2-3GB かそれ以上のスワップを持ついくつかのテストシステムをセットアップし、ささまざまな(メモリの)負荷状況の元で 1 週間程度の連続稼働をして何が起こるかをモニタリングすることです。 その週の間深刻なメモリ飢餓が起きない限りはテストがあまり有用でなかったということになりますが、おそらくは数 MB 程度スワップが使用される結果になるでしょう。 ですので、最低でもその程度のスワップが利用可能にしておくのと、それに加えて変化するワークロードのために少しのバッファを持つことはおそらく有用でしょう。 また atop をロギングモードで使うと SWAPSZ カラムでどのアプリケーションがどれぐらいのページをスワップアウトされたか見ることができるので、もしあなたのサーバで歴史的なサーバの状態をログ出力するためにまだ atop を使っていない場合はおそらくあなたはこの実験の一部としてこれらのテストマシンに atop をロギングモードでセットアップすることをお勧めします。 これはまたあなたのアプリケーションが いつ ページのスワップアウトを開始したかを教えてくれますので、それをログイベントや他のキーデータと紐づけることができます。

検討する価値のあるもう一つのことはスワップメディアの性質です。 どのページがいつ再フォールトするか正確に予想することはできないので、スワップの読み込みは非常にランダムになりがちです。 SSD ではこれはあまり問題になりませんが、回転ディスクでは物理的な移動を実行する必要があるのでランダム I/O は非常に高くつきます。 一方で、 file ページの再フォールトは比較的ランダムでない可能性が高いです。 というのは単一のアプリケーションが実行時に行う操作に関連するファイルは比較的断片化されていないからです。 これは回転ディスク上では、あなたは aononymous ページをスワップアウトする代わりに file ページを回収するようによりバイアスをかけたいと思うかもしれないことを意味します。 ですがこれもこのバランスがあなたのワークロードでどうなるかをテストおよび評価する必要があります。

スワップにハイバネーションしたいラップトップとデスクトップのユーザにとってはそれを考慮に入れる必要があります。 この場合はスワップファイルは最低でもあなたの物理 RAM のサイズ以上にするべきです。

swappiness の設定はどうするべきでしょうか?

まず、 vm.swappiness が何をするものかを理解することが重要です。 vm.swappiness はメモリ回収を anonymous ページの回収寄りにするのかあるいは file ページ寄りにするのかのバイアスを調整する sysctl です。 それはこれを file_prio (file ページを回収する優先度)と anon_prio ( anonymous ページを改選する優先度) という 2 つの異なる属性を使って行います。 vm.swappiness はその値が anon_prio のデフォルト値になり、また file_prio は 200 のデフォルト値から vm.swappiness の値を引いた値になります。 これは vm.swappiness = 50 という値を設定した結果、 anon_prio は 50 になり、 file_prio は 150 になることを意味します(値そのものはあまり意味を持たず、もう片方との相対的な重みが意味を持ちます)。

これは、一般的には vm.swappiness はあなたのハードウェアとワークロードに対して file メモリに比べて anonymous メモリを回収して再フォールトするのにどれぐらいのコストをかけるかの単なる比率である ことを意味します。 値が小さいほど、あなたのハードウェアではアクセス頻度の低い anonymous ページをスワップアウトとスワップインするのは高くつくとカーネルにより強く伝えることになります。 値が大きいほど、あなたのハードウェアでは anonymous ページと file ページをスワップするコストは同程度であるとカーネルにより強く伝えることになります。 メモリ管理システムはこの値よりもまずはメモリがいかにホットであるかに基づいて file か anonymous メモリのどちらをスワップするかを主に決めようとするでしょう。 しかしそのどちらになってもおかしくないときに、コストの計算をスワップするほうに寄せるかあるいはファイルシステムのキャッシュをドロップするほうに寄せるかのどちらにするかに swappiness は寄与します。 SSD では基本的にどちらも同程度のコストなので vm.swappiness = 100 (完全に平等)と設定することはうまく機能するかもしれません。 回転ディスクでは、スワップインは一般的にランダムリードを必要とするためスワッピングが著しく高くつくかもしれないので、より低い値にバイアスをかけたいと思うかもしれません。

現実にはほとんどの人は彼らのハードウェアの要求についてあまり意識していないのでこの値を直感だけに頼って調整するのは自明ではありません。 これは異なるいくつかの値を使って試してみる必要があるものなのです。 あなたのシステムとコアアプリケーションのメモリの構成と軽度のメモリ回収の状況下での振る舞いについて時間をかけて評価しても良いでしょう。

vm.swappiness について話すときに、最近では考慮すべき非常に重要な変更があり、それは 2012 年に Satoru Moriya さんによって vmscan に行われたこの変更 です。 これは vm.swappiness = 0 が処理される方法を非常に著しく変更します。

実質的には、このパッチは vm.swappiness = 0 と設定すると、深刻なメモリ争奪に既に出くわしているのではない限り、 anonymous ページをスキャン(そして回収)することを極端に避けるようにバイアスをかけます。 この記事で既に述べたように、これは一般的にはあなたが望むものではないでしょう。 というのは極端なメモリ逼迫が発生する前に回収の平等さを妨げるものであり、それが実はそもそもの極端なメモリ逼迫を 引き起こす かもしれないからです。 このパッチで実装されている anonymous ページのスキャンの特別な場合分けを実行しないようにするには vm.swappiness = 1 が設定可能な最小値です。

ここでのカーネルのデフォルト値は vm.swappiness = 60 です。 一般的にはこの値はほとんどのワークロードではそれほど悪くはありませんが、全てのワークロードに適した一般的なデフォルト値を持つことは難しいものです。 ですので、上記の「ではスワップのサイズはどれぐらい必要なのでしょう?」の項で言及した調整への有用な延長は vm.swappiness のいくつかの異なる値でこれらのシステムをテストし、あなたのアプリケーションとシステムのメトリクスを大量の(メモリ)負荷のもとでモニタリングすることでしょう。 近い将来、私たちがカーネル内に 再フォールト検知 のまともな実装を持つようになったら、 cgroup v2 のページ再フォールトメトリックを見ることでこのワークロードの検知がいまいちわからなかったのがわかるようになるでしょう。

2019-07 更新: カーネル 4.20+ でのメモリ逼迫メトリクス

上述の以前開発中だった再フォールトのメトリクスが 4.20 以降のカーネルには入っており、 CONFIG_PSI=y で有効にできます。 SREcon での私の講演の 25:05 付近を参照してください。

結論


この記事への幅広い提案とフィードバックに関して Rahul, Tejun, そして Johannes に多大なる感謝を。