Skip to content
Published on

Linuxメモリ階層ハック — swap、zram、そしてVRAMをスワップに使う逆転の発想

Authors

はじめに — 余っているVRAMをスワップに使う?

最近、Hacker Newsでnbd-vramというプロジェクトがフロントページに上がり、ひとしきり議論を巻き起こしました。アイデアは単純かつ挑発的です。最近のGPUにはVRAMが16GB、24GBも載っているのに、ゲームやLLM推論を回していない時間はほとんど遊んでいます。ならばこの高速なメモリをNBD(Network Block Device)として公開し、Linuxのスワップデバイスとして使ったらどうか? SSDスワップよりはるかに速いスワップ階層がタダで手に入るわけです。

コメント欄は二分されました。「PCIe 4.0 x16なら理論上毎秒32GB、NVMeより5倍速いスワップというのは本当だ」という興奮と、「その金でRAMを買い足せ」という冷笑が共存していました。しかしこの論争が本当に価値を持つ理由は別にあります。スワップとは何か、Linuxがメモリをどう階層化しているか、zramとzswapはどこに割り込むのかを正しく理解して初めて、この逆転の発想の合理性を判断できるからです。

この記事はnbd-vramをフックに、Linuxメモリ管理の基礎からスワップチューニング、圧縮メモリ(zram/zswap)、OOM対応、cgroup v2メモリ制御、コンテナ環境の罠まで一気に整理します。

Linuxメモリ管理の基礎 — スワップへの誤解から

ページキャッシュと匿名ページ

Linuxが扱うメモリページは大きく2種類です。

┌──────────────────────── 物理RAM ─────────────────────────┐
│                                                          │
│  ファイルベースのページ (page cache)   匿名ページ (anonymous) │
│  ─ 実行ファイル、ライブラリ            ─ ヒープ(malloc)、スタック│
│  ─ 読んだファイルのキャッシュ          ─ 共有メモリ            │
│  → ディスクに原本がある               → ディスクに原本がない    │
│  → 回収: 捨てればよい                → 回収: スワップに書く必要 │
│    (dirtyならwriteback)                                   │
└──────────────────────────────────────────────────────────┘

核心はこれです。ファイルベースのページはディスクに原本があるので、メモリが足りなくなればそのまま捨てられます(変更されたページはファイルに書き戻してから)。一方、匿名ページには原本がないので、捨てるには内容をどこかに保管しなければなりません。その保管場所がスワップです。

「スワップは遅くなる原因」という誤解

よくある誤解と違い、スワップがなければシステムが速くなるわけではありません。スワップがないと、カーネルはメモリ圧迫時に匿名ページを回収する手段がなく、ファイルベースのページ(コードやライブラリ!)ばかり捨て続けることになります。その結果、たった今捨てた実行コードをディスクから読み直す作業が繰り返される — これが悪名高いthrashingです。一度しか実行されない初期化コードのような冷たい匿名ページをスワップに送り、その場所にホットなページキャッシュを維持する方が、全体性能に有利なことが多いのです。

要約すると、スワップは「メモリが足りないときの非常口」ではなく「メモリ回収の選択肢を広げる装置」です。カーネル開発者Chris Downの「In defence of swap」という記事がこの観点をよく説明しています。

swappiness — 何と何のバランスか

vm.swappinessは0〜200の範囲の値で(古い文書では0〜100)、メモリ回収時に匿名ページとファイルページのどちらをより積極的に回収するかのバランスを決めます。「どれだけ早くスワップを始めるか」ではありません。

# 現在の値を確認
cat /proc/sys/vm/swappiness

# 一時的に変更
sudo sysctl vm.swappiness=100

# 恒久的に設定
echo 'vm.swappiness = 100' | sudo tee /etc/sysctl.d/99-swap.conf
sudo sysctl --system

値の直感は次のとおりです。

意味適した環境
0〜10匿名ページの回収を極度に忌避スワップが非常に遅いHDD、DB専用サーバー
60デフォルト、ファイルページの回収を優先一般的なサーバー
100匿名とファイルを同等に扱うzram/zswapなど高速スワップ
100〜200匿名ページの回収をより優先非常に速いスワップ+大きなページキャッシュ需要

zramのようにスワップがRAM速度に近い環境では、swappinessを100以上に上げるのが定石です。コストモデルが変わったからです。

zram vs zswap — 圧縮メモリ階層の整理

どちらも「圧縮でRAMを増やす」技術ですが、アーキテクチャが異なります。混同しやすいので図で比較します。

[zram]                              [zswap]
                                  
 匿名ページの回収                      匿名ページの回収
      │                                  │
      ▼                                  ▼
 zramブロックデバイス                  zswap圧縮プール (RAMキャッシュ)
 (= RAM内の圧縮スワップ)                  │ 満杯になったら/古くなったら
      │                                  ▼
      ▼                              実際のスワップデバイス
   終わり。(後ろにディスクなし*)        (SSD/HDDへwriteback)
                                  
 * writebackオプションで補助デバイス指定は可能
項目zramzswap
形態圧縮RAMブロックデバイススワップ前段の圧縮キャッシュ
ディスクスワップ不要 (単独動作)必須 (後ろに実スワップが必要)
冷たいページの扱いRAMを占有し続けるディスクへ追い出す (LRU)
圧縮不可ページRAMにそのままディスクへ迂回
適した環境ディスクスワップのない機器、ノートPCスワップディスクのあるサーバー
代表的な採用例ChromeOS、Android、Fedoraデフォルト一部のサーバーディストリビューション

zramの実践設定

Fedoraがデフォルト採用したzram-generator方式が最もすっきりしています。

# /etc/systemd/zram-generator.conf
[zram0]
# RAMの半分、最大8GiB
zram-size = min(ram / 2, 8192)
compression-algorithm = zstd
swap-priority = 100

手動で構成するなら次のとおりです。

# モジュールをロードしてデバイスを構成
sudo modprobe zram num_devices=1
echo zstd | sudo tee /sys/block/zram0/comp_algorithm
echo 8G   | sudo tee /sys/block/zram0/disksize
sudo mkswap /dev/zram0
sudo swapon -p 100 /dev/zram0

# 動作確認 — 圧縮率(DATA対COMPR)に注目
zramctl
NAME       ALGORITHM DISKSIZE  DATA COMPR TOTAL STREAMS MOUNTPOINT
/dev/zram0 zstd            8G  1.2G  310M  330M       8 [SWAP]

圧縮アルゴリズムはlzo-rle(速い、圧縮率低め)とzstd(やや遅い、圧縮率高め)が二大選択肢です。最近のCPUならzstdがおおむね優勢です。圧縮率はワークロードによりますが2〜4倍の範囲が一般的です。

zswapの実践設定

zswapはカーネルビルトインなので、有効にするだけです。ただし、後ろに実際のスワップデバイスがあって初めて意味を持ちます。

# 起動時に有効化 — カーネルコマンドラインに追加
# zswap.enabled=1 zswap.compressor=zstd zswap.max_pool_percent=20

# ランタイムでの切り替え
echo 1    | sudo tee /sys/module/zswap/parameters/enabled
echo zstd | sudo tee /sys/module/zswap/parameters/compressor
echo 20   | sudo tee /sys/module/zswap/parameters/max_pool_percent

# 統計の確認 (debugfs)
sudo grep -r . /sys/kernel/debug/zswap/

選択基準はシンプルです。ディスクスワップを置きたくない、または置けない機器(ストレージ寿命が心配なノートPC、組み込み)はzram、すでにSSDスワップがあるサーバーでスワップIOを減らしたいならzswapです。両方を同時に使うのは圧縮を2回することになるアンチパターンなので避けてください。

NBD — ネットワークブロックデバイスの原理

nbd-vramを理解するにはNBDを知る必要があります。NBDは「ブロックデバイスの読み書き要求をソケット越しのサーバーに転送する」シンプルなプロトコルです。

 ┌─────────── クライアント ──────────┐      ┌────── サーバープロセス ─────┐
 │  アプリ → /dev/nbd0 (ブロック)     │      │  要求を受け取って           │
 │         │                        │      │  「ストレージ」に反映        │
 │     nbdカーネルモジュール          │ ソケット│                          │
 │         └──── read/write ────────┼─────▶│  ストレージの正体は自由:    │
 │                                  │      │  ファイル、RAM、リモートディスク│
 │                                  │      │  ...あるいはGPU VRAM      │
 └──────────────────────────────────┘      └──────────────────────────┘

サーバーが要求をどこに保存しようとクライアントの知ったことではない、という点が核心です。nbd-vramはこのサーバーをCUDA/OpenCLで実装し、ブロック書き込みをVRAMバッファへのcudaMemcpyに変換することで「VRAMディスク」を作ります。同一ホスト内でUnixソケット接続すればネットワークオーバーヘッドも最小化されます。

# 概念的な流れ (nbd-vramのREADME基準)
# 1. VRAMをバックエンドとするnbdサーバーを起動 (例: 8GB割り当て)
./nbd-vram-server --size 8G --socket /run/nbd-vram.sock &

# 2. クライアント接続 → /dev/nbd0 が生成される
sudo nbd-client -unix /run/nbd-vram.sock /dev/nbd0

# 3. スワップとして使用
sudo mkswap /dev/nbd0
sudo swapon -p 50 /dev/nbd0   # SSDスワップより高い優先度

VRAMスワップの性能算数 — PCIe帯域で考える

このアイデアが合理的かどうかは数字で判断できます。メモリ階層ごとのおおよその帯域とレイテンシを比較してみましょう。

階層帯域 (おおよそ)レイテンシ (おおよそ)
DDR5 RAM50〜80 GB/s80〜100 ns
GPU VRAM (PCIe 4.0 x16経由)理論32 GB/s、実効20〜25 GB/sマイクロ秒単位
NVMe SSD (PCIe 4.0 x4)5〜7 GB/s数十マイクロ秒
SATA SSD0.55 GB/s数百マイクロ秒
zram (zstd、RAM内)CPU依存、数GB/s圧縮/解凍コスト

算数は明快です。PCIe 4.0 x16を経由するVRAMアクセスは、NVMeスワップより帯域で3〜5倍速く、レイテンシは一桁以上低い。PCIe 5.0 x16(理論64 GB/s)なら差はさらに広がります。ただし忘れてはならないのがNBD経路のオーバーヘッドです。ページフォルト → ブロック層 → nbdカーネルモジュール → ユーザースペースサーバー → CUDAコピーという経路は、各段階でコンテキストスイッチとコピーを伴うため、理論帯域をフルには引き出せません。それでも「NVMeより速いスワップ」という結論自体はおおむね有効です。

合理的な場合とそうでない場合

合理的なのはこういう場合です。

  • デスクトップにゲーミングGPUがあり、平日の業務時間中はVRAMが遊んでいる場合
  • RAM増設が物理的に不可能な機器(スロット限界、ノートPC)での一時的な大容量作業
  • 実験と学習目的 — ブロック層、NBD、GPUメモリを一度に学べる優れた教材

避けるべき場合はもっと明確です。

  • GPUを実際に使うワークロード(LLM推論、ゲーム)との並行 — VRAM競合とスワップデバイス消失のリスク
  • プロダクションサーバー — GPUドライバやプロセスの障害がそのままスワップデバイス障害になり、スワップ内の匿名ページを失えばカーネルは該当プロセスを殺すしかありません
  • サスペンドが頻繁なノートPC — サスペンド中のVRAM内容の保持は保証されません

要するに「面白くて教育的で、特定の状況では実用的だが、RAM増設が可能ならRAMを買うのが正解」というHNコメント欄の中論が妥当です。

OOM — カーネルの最終手段とユーザースペースの補完

カーネルOOM killerの問題

メモリが本当に底をつくと、カーネルOOM killerがスコア(oom_score)の最も高いプロセスを殺します。問題は発動タイミングが遅すぎることです。カーネルOOM killerは「最後のページまで使い果たした」瞬間にようやく動くため、その直前の数分間、システムはthrashingで事実上止まっていることが多いのです。

# プロセスごとのOOMスコアを確認
cat /proc/1234/oom_score

# 重要なプロセスをOOM対象から保護 (-1000なら免除)
echo -500 | sudo tee /proc/1234/oom_score_adj

earlyoomとsystemd-oomd

そこで「もっと早く、もっと賢く」介入するユーザースペースデーモンが登場しました。

# earlyoom — 空きメモリ/スワップが閾値を下回ると先制kill
sudo apt install earlyoom
# メモリ10%、スワップ5%未満で発動、特定プロセスを優先終了
sudo tee /etc/default/earlyoom <<'EOF'
EARLYOOM_ARGS="-m 10 -s 5 --avoid '(^|/)(sshd|systemd)$' --prefer '(^|/)(chrome|java)$'"
EOF
sudo systemctl enable --now earlyoom

systemd-oomdはさらに一歩進んでPSI(Pressure Stall Information)を見ます。「メモリが何%残っているか」ではなく「メモリのせいでプロセスがどれだけ待たされているか」を基準に、cgroup単位で丸ごと終了させます。

# PSIを直接見る — some: 一部のタスクが待機、full: 全タスクが待機
cat /proc/pressure/memory
some avg10=0.00 avg60=0.12 avg300=0.05 total=8123456
full avg10=0.00 avg60=0.03 avg300=0.01 total=2345678

# systemd-oomdの状態
oomctl
systemctl status systemd-oomd

デスクトップならearlyoomかsystemd-oomdのどちらかは有効にしておくことを強くおすすめします。「ブラウザのタブ1つがシステム全体を人質に取る」事態を防いでくれます。

cgroup v2 — プロセスグループ単位のメモリ制御

システム全体のチューニングよりも精密な道具がcgroup v2のメモリコントローラです。核心のノブは4つです。

ノブ意味超過時の動作
memory.min絶対保証 (回収免除)他のcgroupが譲る
memory.lowソフト保護余裕があるときだけ保護
memory.highソフト上限スロットル+積極回収 (killなし)
memory.maxハード上限超過時にOOM kill

memory.highが特に有用です。上限に達するとプロセスを殺す代わりに割り当て速度を落とし回収を強制するので、「バッチジョブがメモリを食いすぎるが殺したくはない」という状況にぴったりです。

# systemdサービスに適用するのが最もきれい
sudo systemctl set-property batch-job.service MemoryHigh=4G MemoryMax=6G

# 一回限りのコマンドを制限付きcgroupで実行
systemd-run --scope -p MemoryHigh=2G -p MemoryMax=3G ./heavy-script.sh

# cgroupごとのメモリ使用状況
systemd-cgtop -m
cat /sys/fs/cgroup/system.slice/batch-job.service/memory.current

スワップもcgroup単位で制御できます。memory.swap.maxを0にすればそのグループだけスワップを禁止でき、「DBはスワップ禁止、バッチはスワップ許可」のようなポリシーが可能です。

コンテナ環境のメモリの罠

コンテナでは上記の概念が微妙にねじれます。よくハマる罠を3つ挙げます。

罠1 — コンテナ内のfreeはホストの数値を表示する。 コンテナの中でfree -hを打つと、cgroupの上限ではなくホスト全体のメモリが見えます。アプリケーションが「メモリは十分ある」と誤判断してキャッシュを増やし、OOM killされるという古典的なパターンです。JVMや.NETのようなコンテナ対応(container-aware)ランタイムはcgroupの上限を読みますが、freeを直接パースするスクリプトはすべて間違えます。

# コンテナ内で本当の上限を見る方法
cat /sys/fs/cgroup/memory.max        # ハード上限
cat /sys/fs/cgroup/memory.current    # 現在の使用量

罠2 — ページキャッシュもコンテナのメモリに含まれる。 コンテナがファイルをたくさん読むと、ページキャッシュがmemory.currentに計上されます。「うちのアプリは1GBしか使っていないのに、なぜ上限に近いのか?」の正体はたいていこれです。キャッシュは圧迫時に回収されるので通常は無害ですが、memory.maxベースのOOM判定の手前にどれだけ回収可能なキャッシュがあるか知らないと、モニタリンググラフを誤読します。ワーキングセット指標(Kubernetesのcontainer_memory_working_set_bytes)を見てください。

罠3 — Kubernetesは長らくスワップを切ってきた。 Kubernetesはノードのスワップをデフォルトで無効化しており、1.28以降になってようやくNodeSwap機能がベータ入りしました。ホストでzramを有効にしたらkubeletがスワップ検知で起動を拒否した事例、requests/limitsの算定がスワップを考慮しないことによる混乱などが、依然として現場に残っています。コンテナノードで圧縮メモリを使うなら、kubeletの設定(failSwapOn=false、swapBehavior)も合わせて検討する必要があります。

可観測性 — /proc/meminfoとsmemの読み方

チューニングの前提は計測です。最低限、これらの指標は読めるようになっておきましょう。

$ grep -E 'MemTotal|MemAvailable|Buffers|^Cached|SwapTotal|SwapFree|Dirty|AnonPages' /proc/meminfo
MemTotal:       32768000 kB
MemAvailable:   18234560 kB   # ← freeではなくこれを見るべき
Buffers:          512000 kB
Cached:          9876000 kB   # ページキャッシュ
SwapTotal:       8388604 kB
SwapFree:        7340032 kB
Dirty:             12340 kB   # writeback待ちのdirtyページ
AnonPages:      10240000 kB   # 匿名ページの総量

MemFreeが小さいからといってメモリ不足ではありません。ページキャッシュはいつでも回収可能なので、「今すぐ新しいワークロードに渡せる量」であるMemAvailableが実質的な指標です。

プロセスごとのメモリはRSSの罠(共有ライブラリの重複計上)を避け、PSSを見るのが正確です。

# smem — PSS(比例配分された実質占有)基準の上位プロセス
sudo smem -rs pss | head
  PID User     Command                         Swap      USS      PSS      RSS
 2143 app      java -Xmx4g ...                    0  3145728  3167234  3210240
 1021 app      postgres: writer                   0   102400   145000   512000

# スワップを誰が使っているかプロセス別に確認
sudo smem -rs swap | head

# システム全体のスワップイン/アウトの推移 (si/so列)
vmstat 1 5

vmstatのsi/soが継続的に非ゼロならスワップthrashingを疑い、PSI(/proc/pressure/memory)で実際の遅延影響を確認する、という順番が定石です。

実験ベンチマークの方法論 — VRAMスワップを検証するなら

nbd-vramのような実験的構成を評価するときの方法論を整理します。核心は「スワップ性能はスループットではなくレイテンシ分布で測る」です。

# 1. ベースライン: デバイス自体の性能 (fio、4Kランダム — スワップIOパターンを模倣)
sudo fio --name=swaptest --filename=/dev/nbd0 --rw=randrw --bs=4k \
  --iodepth=32 --runtime=60 --time_based --direct=1 --group_reporting

# 2. 実際のスワップ負荷: RAMより大きいワーキングセットを強制的に作る
stress-ng --vm 4 --vm-bytes 120% --vm-method flip --metrics-brief -t 120

# 3. 観測: 負荷中のスワップイン/アウトとPSIを同時に記録
vmstat 1 > vmstat.log &
while true; do cat /proc/pressure/memory >> psi.log; sleep 1; done &

# 4. 体感指標: 負荷中の無関係な作業の遅延を測定 (例: シェルの応答)
time ls -R /usr/share > /dev/null

比較群は最低3つ用意してください。NVMeスワップ単独、zram単独、そして実験構成(VRAMスワップ)です。各構成で同一のstress-ngシナリオを回し、平均ではなくp99レイテンシとPSI full時間を比較して初めて、実使用の体感と一致する結論が得られます。もう1つ、再起動間でキャッシュ状態を揃えるため、計測前にページキャッシュを落とすのを忘れずに。

# 計測前のキャッシュドロップ (ベンチマーク以外では使わないこと)
sync && echo 3 | sudo tee /proc/sys/vm/drop_caches

付録 — ディスクスワップのクイックレシピ

zram/zswap以前の基本であるスワップファイルの作成も整理しておきます。パーティションの再構成なしに即座に追加できるため、緊急対応に特に有用です。

# 4GBのスワップファイルを作成 (ext4/xfs)
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 起動時の自動有効化
echo '/swapfile none swap defaults 0 0' | sudo tee -a /etc/fstab

# btrfsは専用コマンドで (CoW/圧縮属性の処理を自動でやってくれる)
sudo btrfs filesystem mkswapfile --size 4g /swap/swapfile

# 優先度の確認 — 数字が大きいほど先に使われる
swapon --show
cat /proc/swaps

複数のスワップデバイスがあるとき、カーネルは優先度の高い方から使います。zramを100、ディスクスワップを10のように設定すれば、「速い階層を先に、あふれたらディスクへ」という自然な滝構造ができあがります。

実務推奨構成のまとめ

環境別に整理すると次のとおりです。

環境推奨構成
デスクトップ/ノートPC (16GB以下)zram (RAMの半分、zstd) + swappiness 100〜180 + earlyoom
デスクトップ (32GB以上)zram 4〜8GB + systemd-oomd、ディスクスワップは少量
一般サーバー (SSDあり)zswap + NVMeスワップパーティション + memory.highでサービス別上限
DBサーバースワップ少量維持 + DBのcgroupのみmemory.swap.max=0 + swappiness低め
Kubernetesノード基本はスワップなしでrequests/limitsを精密化、NodeSwapは慎重に
GPUが遊んでいるワークステーション(お遊びで) nbd-vramスワップ、優先度はzramより低く

おわりに

nbd-vramは、ある意味おもちゃです。しかし良いおもちゃが常にそうであるように、遊んでいるうちに本物の知識が付いてきます。スワップは悪ではなくメモリ回収の選択肢だということ、圧縮メモリ(zram/zswap)はすでに主流ディストリビューションのデフォルトになったこと、OOMはカーネル任せにせずPSIベースで先制対応すべきこと、そしてコンテナ時代のメモリ観測はcgroupファイルを直接読むことから始まるということまで。

2026年の私たちは、AIワークロードのおかげでかつてないほどメモリ階層に敏感になりました。VRAMとRAMの境界が曖昧になる時代(ユニファイドメモリアーキテクチャの普及)に、ブロックデバイスという抽象化ひとつでGPUメモリをスワップに変身させるこのプロジェクトは、Linuxの抽象化設計の優雅さを示す好例でもあります。週末に一度、自分で転がしてみることをおすすめします。ただし、プロダクションでは絶対にやめましょう。

参考資料