はじめに (対象読者・この記事でわかること)

この記事は、Linuxカーネルのメモリ管理に興味がある中級者以上のエンジニアを対象にしています。
サーバ運用中に突然スワップが発生し、レスポンスが劣化した経験はありませんか? そんなとき「なぜこのプロセスのページが選ばれたのか?」と疑問に思ったことはないでしょうか。
この記事を読むことで、Linuxがメモリ不足時にどのページをスワップアウトするかを決定するアルゴリズム(LRUリスト、kswapd、ページ老化など)の全体像と、実際にカーネルソースコードでどう実装されているかがわかります。カーネルパラメータのチューニングポイントも紹介するので、今日から運用サーバのスワップ挙動を意図的に制御できます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Linuxの仮想記憶(ページ、ページテーブル、ページフォルト)の基本概念 - プロセスごとに独立した仮想アドレス空間があること - /proc/meminfo/proc/sys/vm の存在を知っていること

スワップが発生するまでの流れ:kswapdと水位マーク

Linuxは「メモリがなくなる前にスワップを開始する」設計です。
カーネルは各NUMAノードごとに「水位マーク」と呼ばれる3段階のしきい値を設けており、空きページ数が「Low」以下になるとkswapdというカーネルスレッドが自動的に目を覚まします。kswapdは優先度の高い「ゾーン(ZONE_DMA、ZONE_NORMAL等)」から順に、空きページ数が「High」まで回復するまでスワップアウトを継続します。
水位マークは /proc/sys/vm/min_free_kbytes で基準値を変更でき、値を大きくすると早期に回収が始まり、レイテンシのばらつきは減りますが、無駄なI/Oが増えるトレードオフです。

ページは「年齢」と「参照ビット」で選ばれる:LRUリストの仕組み

1. アクティブ/非アクティブの双方向LRUリスト

カーネルは各ゾーンごとに「アクティブLRU」「非アクティブLRU」の2本の双方向リストを持ち、すべてのページフレームをいずれかに所属させます。
ページが最初に確保されるときはアクティブLRUに追加され、ここに留まっている限り「若く」扱われスワップ対象になりません。
kswapdがスキャンを開始すると、非アクティブLRUの古い方から順にページを検討し、条件を満たせばスワップアウトします。
アクティブLRUのページも一定時間参照がなければ「老化(aging)」と呼ばれる処理で非アクティブリストに移動されます。移動基準は /proc/sys/vm/page_age で調整可能です。

2. 参照ビット(PG_referenced)の役割

各ページには PG_referenced フラグが存在し、CPUがページにアクセスするたびにハードウェアが自動セットします(アーキテクチャによってはカーネルがページフォールトで代用)。
kswapdがページをスキャンする際、まずこのフラグをチェックし、立っている場合は「まだ使われている」と判断してフラグをクリアしページをリスト先頭に移動(リジュビネート)します。フラグが立っていないページは「参照されていない」と見なし、スワップ候補に上がります。
この仕組みにより、ループで繰り返し参照されるホットページはリスト先頭に留まり、コールドページは後方に追いやられて最終的に追い出されるという、LRU的な振る舞いを実現しています。

3. プロセスごとの「スワップ傾向」スコア

カーネルは mm_struct 内に swap_cntswap_avg を保持し、過去に多くのページをスワップアウトしたプロセスほど「次もこのプロセスから選びやすい」というスコアを加味します。これにより、メモリを大量に確保するだけでほとんど触らないようなプロセス(バッチ処理など)が優先的に追い出され、インタラクティブなプロセスのページは残りやすくなります。
スコアの重みは /proc/sys/vm/swappiness で調整でき、0に近づけるとプロセスのページは出来るだけ追い出さず、100に近づけると積極的にスワップアウトします。

ハマった点:cgroup v1のmemsw.limit_in_bytesでswapが増える理由

コンテナ環境で memory.memsw.limit_in_bytes を設定すると、メモリ上限に達する前にスワップが異常に増えることがあります。これは、cgroup v1のメモリコントローラが「メモリ+スワップ」の合計値のみを見るため、kswapdが「メモリはまだ空いているが合計で上限に近い」と判断し、積極的にページを追い出してしまうためです。
結果として、ホットページまでスワップアウトされ、コンテナ内のプロセスがページフォールトを頻発し性能が劣化します。

解決策:cgroup v2のmemory.highを使う

cgroup v2では memory.high が導入され、「メモリのみ」のしきい値として扱われるようになったため、スワップ領域を安定的に使えます。
また、v2では memory.swap.high を別途設定できるため、スワップ量だけを抑えたい場合はこちらを低く設定して、メモリ本体は memory.max で緩く縛ることで、スワップストームを回避できます。
移行時は systemd のデフォルトがv1のままのディストロも多いので、GRUBカーバイオプション systemd.unified_cgroup_hierarchy=1 を忘れずに有効化してください。

まとめ

本記事では、Linuxがメモリ不足時にどのページをスワップアウトするかを決定する仕組み(kswapd、LRUリスト、参照ビット、スワップ傾向スコア)を解説しました。

  • 空きページが「Low」水位を下回るとkswapdが非アクティブLRUの古いページをスキャンする
  • ページは「アクティブ/非アクティブ」の双方向LRUで管理され、参照ビットでリジュビネートされる
  • swappinessやcgroup v2のmemory.highを調整することで、スワップストームを防げる

この記事を通して、サーバ運用時のスワップ挙動を「なぜこのプロセスが選ばれたのか」と理論的に説明できるようになり、チューニングパラメータを意図的に変更できるようになります。
次回は、NUMA環境でkswapdがノード間でどう連携するかや、zswap/zramによる圧縮スワップの内部実装について掘り下げる予定です。

参考資料