カーネルのメモリ管理動作を理解してチューニングするには、まず動作に関する概要と、他のサブシステムとの連携を理解するところから始めるのがよいでしょう。
メモリ管理サブシステムは仮想メモリマネージャとも呼ばれ、下記では 「VM」 と略しています。 VM の役割は、カーネル全体とユーザプログラムに対する物理メモリ (RAM) の割り当て管理です。それだけではなく、ユーザプロセスに対して、仮想メモリ環境を提供する責任も負っています (Linux 拡張付きの POSIX API を介して管理します) 。最後に、 VM はメモリが枯渇した場合に、キャッシュを解放したり 「匿名」 メモリをスワップアウトしたりすることで、メモリを空ける処理も行います。
VM の調査やチューニングに際して理解しておくべき最重要事項は、キャッシュの管理方法についてです。 VM のキャッシュの基本的なゴールは、スワップやファイルシステムの操作(ネットワークファイルシステムを含みます) を行うことによって生成される I/O のコストを、最小化することにあります。これは I/O を排除するか、もしくはよりよいパターンで I/O を送信することによって達成します。
空きメモリは、必要に応じてキャッシュとして使用されます。キャッシュや匿名メモリとして使用できるメモリ領域が大きければ大きいほど、より効率的にキャッシュやスワップを制御できるようになります。しかしながら、いったんメモリが枯渇してしまった場合は、キャッシュを解放するか、メモリをスワップアウトする必要があります。
適切な負荷範囲であれば、性能を改善するにあたって最初にやるべきことは、メモリを増やしてキャッシュの効率を上げ、キャッシュの解放やスワップアウトの頻度を減らすのがよいでしょう。次にやるべきことは、カーネルのパラメータを変更して、キャッシュの管理方法を変えることです。
最後に、負荷それ自身についても調査してチューニングする必要があります。アプリケーション側で多くのプロセスやスレッドを動作させることができる場合、各プロセスがファイルシステム内の独自の領域を利用していると、 VM の効率が落ちてしまいます。また、メモリによるオーバーヘッドも増えてしまいます。アプリケーション側で独自のバッファやキャッシュを持っている場合、そのキャッシュを大きくしてしまうと、 VM 側に割り当てることのできるサイズが小さくなってしまいます。しかしながら、プロセスやスレッドの数を増やすことで、 I/O の重複度合いやパイプライン処理の機会が増えることがありますので、これによってマルチコア環境で性能が向上することもあります。従って、最適な結果を得るためには、実験が必要ということになります。
メモリの割り当ては 「ピン済み」 (「回収不可」 と呼ばれることもあります), 「回収可」, 「スワップ可」 に分類することができます。
匿名メモリはプログラムのヒープやスタックメモリ (例: >malloc()
) として使用されるメモリで、 mlock
のような特殊ケースや、利用可能なスワップ領域が存在しないような場合を除いて、回収可能なメモリとして位置づけられます。また、匿名メモリは回収される前にスワップに書き込まなければなりません。また、スワップの I/O (ページのスワップインおよびスワップアウト) は、割り当てとアクセスのパターンにより、ページキャッシュの I/O よりも効率が落ちる傾向があります。
ファイルデータのキャッシュです。ディスクやネットワークからファイルが読み込まれると、その内容がページキャッシュ内に保存されます。ページキャッシュ内の内容が最新のものであれば、ディスクやネットワークへのアクセスは不要となります。また、 tmpfs や共有メモリセグメントについても、ページキャッシュとしてカウントします。
ファイルに対して書き込みが行われる場合、ディスクやネットワークに書き戻される (つまりライトバックキャッシュを作成する) 前に、ページキャッシュにも保存が行われます。まだディスクやネットワークに書き込まれていない新しいデータが存在する場合、そのページは 「dirty」 であるとされます。逆に dirty ではないページは、 「clean」 であるとされます。 clean やページキャッシュのページは、メモリが枯渇した場合、単純に解放するだけで回収することができます。 dirty なページの場合は、回収できるようにするためにまず clean にしなければなりません。
ブロックデバイス (例: /dev/sda) に対するページキャッシュの一種です。ファイルシステムでは、inode テーブルやアロケーションビットマップなど、ディスク内のメタデータにアクセスする際にバッファキャッシュを使用します。バッファキャッシュはページキャッシュと同様に回収することができます。
バッファヘッドは小さな補助構造で、一般的にページキャッシュへのアクセス時に割り当てられるものです。ページキャッシュやバッファキャッシュが clean であれば、これらも一般に容易に回収することができます。
アプリケーションがファイルに書き込みを行う場合、ページキャッシュが dirty となるほか、バッファキャッシュについても必要に応じて dirty となります。 dirty なメモリの量が指定したページ数 (バイト単位で vm.dirty_background_bytes で指定します) を越えるか、メモリの全体量との比率が指定した値 ( vm.dirty_background_ratio ) を越えるか、もしくは指定した時間 ( vm.dirty_expire_centisecs ) より長く dirty であり続けた場合、カーネルは最初に dirty になったページのファイルからページの書き戻しを始めます。なお、 bytes の設定と ratio のパラメータは相互に排他な仕組みであり、一方を変更すると他方を上書きすることになります。また、 flusher スレッドは裏で書き戻しを行う仕組みであるため、アプリケーションは通常通り動作し続けることができます。ただし、 I/O の速度よりもアプリケーションがページを dirty にする速度のほうが早い場合、 dirty なデータが致命的な水準 ( vm.dirty_bytes もしくは vm.dirty_ratio ) を越えると、アプリケーションの速度が抑制され、 dirty なデータが過剰に生み出されないようにします。
VM はファイルのアクセスパターンを監視し、必要に応じて先読みを行おうとします。先読みはその名前の通り、まだ要求されていない範囲のものをファイルシステムからページキャッシュにページを読み込むものです。これは、より少ない回数でより大きな I/O を送信できるようにする (これによって効率化を図る) ためのものです。また、 I/O をパイプライン化する (アプリケーションの実行と同時に I/O を実施する) ためのものでもあります。
これは各ファイルシステムに対する inode の構造をメモリ内にキャッシュしておくものです。この中にはファイルサイズやパーミッション、所有者情報やファイルデータに対するポインタなどを保持しています。
これはシステム内でのディレクトリエントリのメモリ内キャッシュです。この中には名前 (ファイル名) のほか、それが指し示す inode と、子エントリが含まれます。このキャッシュは、ディレクトリ構造をたどる場合と、名前でファイルにアクセスする場合に使用されるものです。
openSUSE Leap 15.7 で動作しているアプリケーションは、以前のバージョンに比べてより多くのメモリを割り当てることができます。これは glibc
がユーザスペースメモリの割り当てに際して、既定の動作を変更したことによるものです。これらのパラメータについて、詳しくは https://www.gnu.org/s/libc/manual/html_node/Malloc-Tunable-Parameters.html (英語) をお読みください。
以前のバージョンの動作に戻したい場合、 M_MMAP_THRESHOLD の値を 128*1024 に設定する必要があります。これはアプリケーション側から mallopt() を利用することによって実現できるほか、アプリケーションの起動前に MALLOC_MMAP_THRESHOLD_
環境変数を設定しても実現することができます。
回収可能なカーネルメモリ (上述のとおりキャッシュなど) については、メモリの枯渇時に自動的に解放が行われます。その他のカーネルメモリについては容易に縮小することができますが、カーネルに与えられた負荷の特性によって決まります。
ユーザスペースの負荷要件を減らす (プロセスを減らす、ファイルやソケットを減らすなど) ことで、カーネルのメモリ使用量も削減することができます。
メモリ cgroup の機能が不要である場合は、カーネルのコマンドラインに cgroup_disable=memory を追加することで、無効化を行うことができます。これにより、カーネルが使用するメモリを少しだけ削減することができます。また、メモリ cgroup が利用できるにもかかわらず設定していない場合、少しだけメモリのオーバーヘッドが存在するため、少しだけ性能改善をはかることもできます。
VM をチューニングする場合、パラメータが実際の処理に反映されるまでには、しばらく時間がかかるものがあることに注意してください。また、負荷が 1 日を通して変化するような場合、時間帯によっては異なる振る舞いになることもあります。それだけでなく、特定の環境でスループットを改善できるパラメータが、別の環境ではスループットを悪化させてしまうようなこともあります。
/proc/sys/vm/swappiness
この変数は、ページキャッシュやその他のキャッシュに比べて、カーネルがどれだけの比率で匿名メモリを積極的にスワップアウトさせるかを指定するものです。この値を増やすことで、スワップ量が増えることになります。既定値は 60
です。
スワップの I/O は一般に、その他の I/O よりもずっと効率が落ちるものです。しかしながら、ページキャッシュのページによっては、あまり使用されない匿名メモリよりも、頻繁にアクセスされるものがあります。ここでは適切な比率を設定してください。
速度が低下した際にスワップの動作が観測できた場合、このパラメータを減らしてみることをお勧めします。また、多くの I/O 動作が存在する環境で、システム内のページキャッシュの量が比較的少ない場合や、動作しているものの休眠中の巨大なアプリケーションが存在するような環境では、この値を増やすことで性能を改善できるかもしれません。
ただし、データのスワップアウト量が増えれば増えるほど、必要とされた際にスワップアウトされたデータを取り戻すのに時間がかかることに注意してください。
/proc/sys/vm/vfs_cache_pressure
この変数は、ページキャッシュやスワップに比べて、カーネルが VFS キャッシュに使用しているメモリの回収傾向を制御するためのものです。この値を増やすと、 VFS キャッシュの回収比率を高めることができます。
ただし、試してみる以外の方法では、適切な値を推測することは難しい値でもあります。 slabtop
コマンド (procps
パッケージ内に含まれています) を使用することで、カーネル側で使用しているメモリオブジェクトを多い順に並べることができます。このコマンド内で、 vfs キャッシュは "dentry" と "*_inode_cache" として表示されます。ページキャッシュに比べてこれらのメモリが巨大になっている場合、この圧力値を増やしてみることをお勧めします。これにより、スワップ処理を減らすことにもなります。既定値は 100
です。
/proc/sys/vm/min_free_kbytes
この変数は、 「アトミックな」 (回収を待つことができない) 割り当てなど、特別に予約しておく必要のあるメモリ量を制御します。これは通常、メモリ使用率について注意深くチューニングしている場合を除いて、減らすべきではない値です (通常はサーバ用途ではなく、組み込み用途で設定すべき値です) 。もしもログ内に 「ページ割り当て失敗」 に関するメッセージとスタックトレースが頻繁に現れているような場合、 min_free_kbytes をエラーが出なくなる程度まで増やしてみることをお勧めします。このようなメッセージが頻繁に現れたりしていなければ、特に気にする必要はありません。既定値は搭載されている RAM の量に従って決められます。
/proc/sys/vm/watermark_scale_factor
大まかに言うと、空きメモリには高/低/最小の水準が設定されています。低い水準に達した場合、 kswapd
が動作して裏でメモリの回収を行うようになります。この回収作業は、高いほうの水準に達するまで続けられます。最小の水準に達した場合、アプリケーションの動作は一時的に停止させられます。
watermark_scale_factor
は、 kswapd が動き出す際のノードもしくはシステム内のメモリ量を表す値と、そこから kswapd が休眠状態に戻るまでにどれだけの量のメモリを解放するのかを表す値です。単位は 10,000 の対する比率で、既定値の 10 は 水準間の長さがノード/システム内で利用できるメモリの 0.1% 分であることを示します。最大値は 1000 で、 10% 分を表します。
直接的な回収処理で処理速度が遅くなってしまっている場合、 /proc/vmstat
内の allocstall
の値が増えますが、この場合はこのパラメータを変更することで、問題を回避できるかもしれません。同様に kswapd
が早く休眠状態になってしまう場合、 kswapd_low_wmark_hit_quickly
の値が増えますが、この場合はアプリケーションの一時停止を回避するために空いているページ数が、少なすぎることを表しています。
以前の openSUSE Leap バージョンからのライトバック関連の主な変更点として、ファイルに結びつけられた mmap() メモリが、即時に dirty なメモリとして判断されるようになった (つまりライトバックの対象となる) ことがあげられます。以前のバージョンでは、 munmap() でマップが解除された場合や msync() システムコールが呼び出された場合、もしくはメモリへの圧力が大きい場合にのみ書き戻しが行われていました。
アプリケーションによっては、このような書き戻し動作への変更を期待していないものもあり、場合によっては性能が低下してしまうことがあります。
/proc/sys/vm/dirty_background_ratio
この変数は空き領域や回収可能なメモリの全体量の割合を表すものです。この割合以上に dirty なページキャッシュが存在していると、ライトバックスレッドが dirty なメモリを書き込み始めるようになります。既定値は 10
(%) です。
/proc/sys/vm/dirty_background_bytes
この変数は、裏で動作するカーネルのライトバックスレッドが、その書き込みを始める割合を表すものです。 dirty_background_bytes
は dirty_background_ratio
に対応するもので、一方を設定すると他方が自動的に 0
に設定されます。
/proc/sys/vm/dirty_ratio
dirty_background_ratio
に似た意味を持つ割合値です。ここで指定した割合を超過すると、ページキャッシュに対して書き込みを行いたいアプリケーションの動作が一時的に止められ、カーネルのライトバックスレッドが dirty なメモリを書き込んで clean に戻すまで待機するようになります。既定値は 20
(%) です。
/proc/sys/vm/dirty_bytes
この変数は dirty_ratio
と同じようなチューニングパラメータですが、ここでは割合ではなくバイト単位でサイズを指定します。 dirty_ratio
と dirty_bytes
は同じチューニングパラメータであることから、一方を設定すると他方が自動的に 0
になります。 dirty_bytes
に設定可能な最小値は 2 ページ分 (ただしバイト単位) で、それより小さい値を設定しようとしても、それは無視されて元の値に戻ってしまいます。
/proc/sys/vm/dirty_expire_centisecs
この変数は、ここで設定した値よりも長い時間 dirty であり続けたメモリが存在した場合、次回のライトバックスレッドの起床時に書き込みが行われるようになるものです。ここでの期限設定はファイルの inode に設定された最終更新日時を基準にします。そのため、同じファイルに対して発生した複数の dirty ページが存在した場合、この期限を超過するとそれら全てが書き込まれるようになります。
dirty_background_ratio
と dirty_ratio
は、いずれもページキャッシュのライトバック動作を設定するためのものです。これらの値を増やした場合、システム内に dirty なメモリがより多く、かつ長く保持されることになります。システム内により多くの dirty なメモリを保持できる環境であれば、ライトバックの I/O を減らして、より最適な I/O パターンでの書き込みを増やすために、これらの値を活用できる場合があります。ただし dirty なメモリが多く存在していると、メモリを回収する必要がある場合や、ディスクに書き込む必要が発生した場合に一貫性を確保するタイミング ( 「同期ポイント」 ) で、遅延が発生する場合があります。
/sys/block/ブロックデバイス/queue/read_ahead_kb
1 つもしくは複数のプロセスがファイルを順次読み込みしている場合、カーネルはそれらのデータを前もって読み込み (先読みし) 、プロセスがデータを待つ時間を減らすように処理を行います。先読みのデータ量は、どれだけ 順に I/O を行っているのかに従って、動的に計算されます。このパラメータは、カーネルが先読みを行う際、 1 ファイルあたりの最大データ量を設定するためのものです。巨大なファイルの順次読み込みが十分に早いものであるとは思えない場合、この値を増やしてみることをお勧めします。ただし、大きくしすぎると、先読みスラッシングと呼ばれる現象が発生し、先読みで使用したページキャッシュが、使用される前に回収されてしまうことがあります。また、意味のない I/O が発生して速度が落ちてしまうこともあります。既定値は 512
[KB] です。
Transparent HugePages (THP) は動的な huge page の割り当て方法で、プロセス側から要求によって割り当てたり、後から khugepaged
カーネルスレッドを介して遅延割り当てを行ったりするためのものです。この方式は hugetlbfs
の使用とは区別されていて、こちらは割り当てと使用を手作業で管理する方式です。連続したメモリのアクセスパターンが存在する負荷の場合、 THP によって大きく性能を改善できる可能性があります。連続したメモリのアクセスパターンで一括処理を行うと、ページフォルトを 1000 倍減少させることもできてしまいます。
逆に THP が望ましくない場合もあります。たとえばメモリ内のあちこちをアクセスするようなパターンの場合、 メモリの使用量が増える結果になってしまうため、かえって性能が落ちてしまいます。たとえばフォルトごとに 4 KB ではなく、フォルト発生時点で 2 MB のメモリを使用してしまうことがあり、最終的にはページの回収を早める結果になってしまいます。
THP の動作は transparent_hugepage=
のカーネルパラメータか、 sysfs 経由で設定することができます。たとえばカーネルのパラメータに transparent_hugepage=never
を追加して grub2 の設定を再構築し、システムを再起動します。すると、下記のように入力して実行することで、 THP が無効化されていることを確認できます:
#
cat /sys/kernel/mm/transparent_hugepage/enabled
always madvise [never]
無効化されている場合、上記の例のように never
が [] で囲まれて表示されます。 always
に設定すると THP を常に使用するようになりますが、割り当てが失敗した場合は khugepaged
に従います。 madvise
に設定すると、アプリケーション側で明示的に指定されたアドレス空間に THP を割り当てるだけになります。
/sys/kernel/mm/transparent_hugepage/defrag
このパラメータは、 THP を割り当てる際にアプリケーションがどれだけの努力を行うのかを制御するものです。 always
は openSUSE 42.1 およびそれ以前 の THP に対応するリリースでの既定値でした。もしも THP が利用できない場合、アプリケーションはメモリのデフラグを実施しようとします。つまり THP が利用できない場合、メモリがフラグメントされていると、アプリケーションの動作が潜在的に一時的ながら停止する可能性があることになります。
値に madvise
を設定すると、 THP の割り当てリクエストでは、アプリケーション側から明示的に要求された場合にのみ、デフラグを実施します。こちらが openSUSE 42.2 およびそれ以降のバージョンでの既定値となります。
defer
は openSUSE 42.2 およびそれ以降で利用できるようになった値で、 THP が利用できない場合には小さなページを使用して処理するように動作します。これにより kswapd
と kcompactd
の各カーネルスレッドを起床させ、裏でデフラグを行うことで、後から khugepaged
が THP を割り当てる動作になります。
最後の値である never
は、 THP が利用できない場合には単純に小さなページを利用して処理を行うもので、それ以外の処理は行わない意味になります。
khugepaged は transparent_hugepage
の値が always
もしくは madvise
である場合に自動的に開始され、 never
である場合には自動的に停止します。通常は低頻度で動作するものではありますが、動作をチューニングすることもできます。
/sys/kernel/mm/transparent_hugepage/khugepaged/defrag
値に 0 を設定すると、フォルトの時点で THP が使用されていても khugepaged
を無効化します。これは遅延に敏感なアプリケーションにとっては重要な設定で、 THP による利点を受けるものの、 khugepaged
がアプリケーションのメモリ使用を更新しようとする際に、一時的に動作が止まってしまう事象を防ぐことができるからです。
/sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
この変数は、 khugepaged
が 1 回の処理でスキャンするページ数を制御するためのものです。スキャン処理では小さいページを検出して、 THP で再割り当てができないかどうかを確認します。この値を増やすことで、 CPU の使用率が上がるものの、裏でより高速に THP を割り当てることができるようになります。
/sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
この変数は、 khugepaged
が 1 回の処理を完了した後に待機する時間を設定するためのもので、 CPU の使用率が過剰に上がらないようにするためのものです。この値を小さくすると、 CPU の使用率が上がる代わりに、裏でより高速に THP を割り当てることができるようになります。
/sys/kernel/mm/transparent_hugepage/khugepaged/alloc_sleep_millisecs
この変数は、 khugepaged
が kswapd
と kcompactd
が動作するのを待っている間、裏で THP が割り当てに失敗した際に休眠する時間を指定するためのものです。
khugepaged
に対するその他のパラメータは、性能チューニングにあたってはほとんど用途のないものではありますが、 /usr/src/linux/Documentation/vm/transhuge.txt
ファイル (英語) で詳しく説明しています。
VM に関連するチューニングパラメータに関する完全な一覧については、 /usr/src/linux/Documentation/sysctl/vm.txt
ファイル (英語, kernel-source
パッケージ内に含まれています) をお読みください。
VM の動作の監視には、下記のようなシンプルなツールを使用することができます:
vmstat: このツールは、 VM が現在何をしているのかをわかりやすく表示することができます。詳しくは 2.1.1項 「vmstat
」 をお読みください。
/proc/meminfo
: このファイルは、メモリの使途を分解して示しているファイルです。詳しくは 2.4.2項 「詳細なメモリ使用率情報の取得: /proc/meminfo
」 をお読みください。
slabtop
: このツールは、カーネルの slab メモリについて、その使用状況を詳細に表示することができます。 buffer_head, dentry, inode_cache, ext3_inode_cache などの値が主なキャッシュです。このコマンドは procps
パッケージ内に含まれています。
/proc/vmstat
: このファイルには VM の内部動作を詳しく分解して表示したものが含まれています。含まれている情報は実装に依存して決まるものであり、環境によって表示される内容が異なります。いくつかの項目は /proc/meminfo
でも表示されていますが、それ以外の項目はユーティリティを使用してわかりやすく表示するのがよいでしょう。また、情報を最大限に活用するため、このファイルは変化率を観察するために長時間監視しておくことをお勧めします。他の情報源からは導き出すのが難しい主な情報は下記のとおりです:
pgscan_kswapd_*, pgsteal_kswapd_*
これらのレポートには、システムが起動してからスキャンしたページ数の合計と、 kswapd
が回収したページ数の合計が書かれています。これらの値の比率は回収効率として解釈することができ、この値が低い場合は、システムがメモリを回収するのに苦労していて、おそらく使用する前に回収されてしまっていることを表しています。処理が軽い場合は、あまり気にする必要はありません。
pgscan_direct_*, pgsteal_direct_*
これらのレポートには、アプリケーションが直接スキャンしたページ数の合計と、回収したページ数の合計が書かれています。これは allocstall
のカウンタ値と相関しています。これらのイベントが多く発生している場合、プロセスの動作が一時的に止まっていることを示すことから、 kswapd
での処理より深刻な問題となります。 kswapd
での処理が重く、 pgpgin
, pgpout
の値が高いか、もしくは pswapin
や pswpout
の値が高い場合は、システムが過剰にスラッシングしている (実際に使用される前に回収されてしまっている) ことを表しています。
より詳しい情報を得たい場合は、トレースポイントを使用してください。
thp_fault_alloc, thp_fault_fallback
これらのカウンタは THP がアプリケーションから直接割り当てられた回数と、 THP が利用できずに小さいページを使用した回数を表しています。 thp_fault_fallback の値が大きい場合でも、アプリケーションが TLB の圧力に敏感でない限り、有害ではありません。
thp_collapse_alloc, thp_collapse_alloc_failed
これらのカウンタは、 khugepaged
が割り当てた THP の回数と、 THP が利用できずに小さなページを使用した回数を示しています。 failed の割合が高い場合、システムがフラグメント状態にあり、アプリケーション側が許可したメモリ使用量であるにもかかわらず、 THP が使用されていないことを表しています。アプリケーションが TLB の圧力に敏感である場合にのみ、問題となる項目です。
compact_*_scanned, compact_stall, compact_fail, compact_success
これらのカウンタは THP が有効化され、システムがフラグメントしている場合に増えるものです。 compact_stall
は THP の割り当てに際してアプリケーションの動作が一時的に停止した場合に増加します。それ以外のカウンタは、スキャンしたページの数と、成功もしくは失敗したデフラグイベントの数を表しています。