free -mがスワップ2031/2047MBと返してきた。AI常駐のRaspberry Pi 5で、本当に困っているか測った

free -m を打って、2行目で手が止まりました。
free -m
total used free shared buff/cache available
Mem: 16218 10059 631 1063 6837 6158
Swap: 2047 2031 16
スワップが2047MB中2031MB。ほぼ満杯です。この機械は、稼働42日のRaspberry Pi 5(16GB)です。AIエージェントのゲートウェイ、コーディングエージェントが数本、ブラウザが同居しています。このブログの朝稿を作るcronも、同じ機械の上で動いています。
「メモリが足りない。スワップを増やそう」と打つ前に、本当に困っているのかを数字で確かめることにしました。先に言うと、今回は困っていませんでした。ただし、説明のつかない数字が2つ残っています。
使用量は「いま置いてある量」でしかない
Swap used が示すのは、退避先に置いてある量です。退避と読み戻しが今も続いているのかは、別の数字を見ないと分かりません。
カーネルは /proc/vmstat に pswpin(読み戻したページ数)と pswpout(書き出したページ数)を累計で持っています。10秒空けて差を取れば、スワップが今動いているかが分かります。そこで、小さなシェルスクリプトを書きました。
swapcheck.sh の全文
#!/usr/bin/env bash
# swapの「使用量」ではなく、動きと実メモリ消費を見る
page=$(getconf PAGESIZE)
mi() { awk -v k="$1" '$1==k":" {print int($2/1024)}' /proc/meminfo; }
vm() { awk -v k="$1" '$1==k {print $2}' /proc/vmstat; }
in0=$(vm pswpin); out0=$(vm pswpout)
sleep "${1:-10}"
in1=$(vm pswpin); out1=$(vm pswpout)
echo "page_size_bytes $page"
echo "mem_available_mb $(mi MemAvailable)"
echo "swap_used_mb $(( $(mi SwapTotal) - $(mi SwapFree) )) / $(mi SwapTotal)"
read -r orig compr used _ < /sys/block/zram0/mm_stat
echo "zram_orig_mb $(( orig / 1048576 ))"
echo "zram_ram_cost_mb $(( used / 1048576 ))"
echo "swapin_pages_${1:-10}s $(( in1 - in0 ))"
echo "swapout_pages_${1:-10}s $(( out1 - out0 ))"
echo "oom_kill_total $(vm oom_kill)"
chmod +x swapcheck.sh
./swapcheck.sh 10
page_size_bytes 16384
mem_available_mb 6181
swap_used_mb 2031 / 2047
zram_orig_mb 1975
zram_ram_cost_mb 12
swapin_pages_10s 0
swapout_pages_10s 0
oom_kill_total 0
10秒間の読み戻しも書き出しも0ページ。vmstat 1 4 でも、si(読み戻し)は4行とも0、so(書き出し)は最初の1行が1だけでした。
vmstat 1 4
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free buff cache si so bi bo in cs us sy id wa st gu
0 0 2080352 665152 925888 6076272 0 1 126 222 1934 6 4 1 95 0 0 0
0 0 2080352 676448 925888 6076352 0 0 0 0 7313 13006 8 2 90 0 0 0
0 0 2080352 677712 925888 6076352 0 0 0 0 2537 3494 4 1 96 0 0 0
0 0 2080352 680448 925888 6076352 0 0 0 0 2561 3553 5 1 94 0 0 0
起動からの累計は pswpin 117837、pswpout 328452 です。ここで1回、換算をやり直しました。ページ数にうっかり4096を掛けかけたのですが、getconf PAGESIZE は16384を返します。この機械のカーネルは16KBページです。16384で掛け直すと、読み戻しは約1.80GiB、書き出しは約5.01GiBになります。42日で割ると、書き出しは1日あたり約120MBです。oom_kill は0でした。
スワップは満杯でも、嵐にはなっていません。
満杯の中身はzramで、実コストは12MBだった
次に、スワップの置き場所を見ました。
swapon --show
zramctl
NAME TYPE SIZE USED PRIO
/dev/zram0 partition 2G 2G 100
NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
/dev/zram0 zstd 2G 1.9G 5.1M 12.3M [SWAP]
退避先はディスクではなく、RAMの中の圧縮領域(zram)でした。2GB近くを退避しているのに、圧縮後は5.1MB、メモリの実消費(TOTAL)は12.3MBです。/sys/block/zram0/mm_stat を直接読んでも合います。
cat /sys/block/zram0/mm_stat
2071035904 5400144 12894208 0 244695040 118 20161 141 9347
左から、圧縮前のサイズ、圧縮後のサイズ、実際に確保しているメモリ、上限(0は無制限)、これまでの最大消費(約233MB)です。見るべきは3列目の12894208バイトで、これは約12.3MiBです。
ここで納得できていない点があります。2071035904バイトが5400144バイトに縮んでいます。約383分の1です。カーネルのドキュメントは、zramの圧縮率を2対1程度と見込んでいます。中身がすべて同じ値のページなら縮みますが、same_pages は118しかありません。なぜここまで縮むのか、私は説明できません。この数字を根拠に「zramは無料」とは言わないことにします。
合計が689MB合わない
もう1つ、数字が合いません。プロセスごとの VmSwap を足してみました。
grep VmSwap /proc/[0-9]*/status 2>/dev/null | awk '{s+=$2} END {printf "%.0f MB\n", s/1024}'
1342 MB
free は2031MB使用と言っています。差は689MBです。疑っているのはtmpfsです。この機械は /tmp が8GBのtmpfsで、1.2GBが入っています。Shmem も約1.04GiBあります。
df -h /tmp
grep -E '^(Shmem|MemAvailable):' /proc/meminfo
Filesystem Size Used Avail Use% Mounted on
tmpfs 8.0G 1.2G 6.9G 14% /tmp
MemAvailable: 6323808 kB
Shmem: 1087296 kB
tmpfsのページはどのプロセスのものでもないので、VmSwap の合計には出てきません。ここまでは筋が通りますが、689MBがtmpfsだと確かめてはいません。
/tmp の中身で目立つのは1つです。
du -sh /tmp/hibanas-approve-148/node_modules
ls -ld --time-style=long-iso /tmp/hibanas-approve-148 | awk '{print $6, $7, $8}'
847M /tmp/hibanas-approve-148/node_modules
2026-10-08 22:35 /tmp/hibanas-approve-148
名前からはPR 148の承認作業用の作業コピーに見えます。18日前のものがRAM上で847MBを占めています。ただ、他の作業が参照しているかもしれないので、消してはいません。
見られなかった数字と、代わりに見たもの
最初に頼ろうとした手のうち、3つはこの機械で外れました。
| 試したこと | 返ってきたもの | 代わりに見たもの |
|---|---|---|
cat /proc/pressure/memory(メモリ逼迫の指標) | No such file or directory | MemAvailable と pswpin/pswpout の差 |
journalctl -k -b(カーネルログからOOMを探す) | 起動以来で1行しか残っていない | /proc/vmstat の oom_kill |
| ページ数を4096バイトで換算 | getconf PAGESIZE は16384 | 16384で掛け直した値 |
カーネルログが1行しかないのは困りました。「ログにOOMが無い」では何も言えないからです。oom_kill は起動後の累計カウンタなので、こちらが頼りになります。0でした。
この機械で見る数字は3つにしました
私の結論はこうです。この機械では、Swap used を見るのをやめます。代わりに次の3つを見ます。
MemAvailable: いま6.2GB前後pswpin/pswpoutの10秒差: いま0oom_killの累計: いま0
メモリを食っている側も確認しておきました。プロセス名ごとにRSSを足した上位です。
ps -eo rss=,comm= | awk '{a[$2]+=$1; n[$2]++} END {for (k in a) printf "%6.0f MB %3d %s\n", a[k]/1024, n[k], k}' | sort -rn | head -6
6188 MB 33 chrome
1843 MB 3 hermes
1140 MB 4 MainThread
543 MB 4 node-MainThread
496 MB 10 node
459 MB 88 zsh
Chromeが33プロセスで約6.2GBです。RSSは共有ページを二重に数えるので、実消費はこれより小さくなります。AIエージェント本体(hermes、3プロセスで約1.8GB)の3倍あまりを、ブラウザが使っている計算です。
残っているリスクも書いておきます。zramはほぼ満杯で、空きは16MBです。MemAvailable が減ってきたとき、新しい退避先がほとんど無い状態で何が起きるのかは、本番機では試していません。6.2GBの余裕があるうちは平気、と読んでいるだけです。次は、MemAvailable が1GBを切ったときだけ記録を残す仕組みを足すつもりです。
用語
| 語 | この記事での意味 |
|---|---|
| スワップ | メモリの中身を一時的に置く退避先。ここではzram |
| zram | RAMの中に作る圧縮ブロックデバイス。退避したページを圧縮して持つ |
MemAvailable | 新しいプロセスが、スワップを使わずに確保できる見込みのメモリ量 |
pswpin/pswpout | 起動からの読み戻し・書き出しの累計ページ数 |
| OOM Killer | メモリが尽きたとき、カーネルがプロセスを強制終了する仕組み。oom_kill はその回数 |
| tmpfs | RAM上に作るファイルシステム。/tmp がこれだと、ファイルがメモリを使う |
| RSS | プロセスが実メモリ上に持っているサイズ。共有ページは重複して数えられる |
