前回の記事で、VPSのメモリを1回だけ測りました。ただしあれはアイドルに近い状態の1サンプルです。ピークは写っていません。
そこで、1分ごとに記録するスクリプトを仕掛けて放置しました。43時間、2,590サンプル、欠測ゼロ。
結果、予想は外れました。メモリのピークを作っていたのは、自分のアプリではありませんでした。

上がメモリ、下がCPUです。山が重なっていません。 これが今回いちばん面白かったところです。
測定条件
- サーバー:さくらのVPS 2Gプラン / Ubuntu 24.04 LTS
- 期間:2026-09-04 01:10 〜 09-05 20:19(43.1時間)
- 間隔:1分(cron)
- サンプル数:2,590(120秒を超える欠測ゼロ)
- 載せているもの:定期的にデータを取得して予測を回すバッチ処理と、静的サイト1本
使ったスクリプト
追加パッケージは不要です。free / vmstat / ps / awk だけで動きます。
#!/usr/bin/env bash
set -u
OUT="${1:-$HOME/vps-metrics.csv}"
if [ ! -f "$OUT" ]; then
echo "timestamp,mem_total_mb,mem_used_mb,mem_available_mb,swap_used_mb,load1,load5,cpu_idle_pct,p1_name,p1_rss_mb,p2_name,p2_rss_mb,p3_name,p3_rss_mb" > "$OUT"
fi
ts=$(date '+%Y-%m-%d %H:%M:%S')
mem=$(free -m | awk '/^Mem:/{print $2","$3","$7}')
swp=$(free -m | awk '/^Swap:/{print $3}')
ld=$(awk '{print $1","$2}' /proc/loadavg)
idle=$(vmstat 1 2 | awk 'NR==2{for(i=1;i<=NF;i++) if($i=="id") c=i} END{print (c?$c:"")}')
top=$(ps -eo comm=,rss= --sort=-rss | head -3 | tr -d ',' \
| awk '{printf "%s,%.1f,", $1, $2/1024}' | sed 's/,$//')
echo "$ts,$mem,$swp,$ld,$idle,$top" >> "$OUT"
cronに1行足すだけです。
* * * * * /home/ubuntu/vps-sampler.sh >/dev/null 2>&1
vmstat のCPUアイドル列は、カーネルやprocpsのバージョンで位置が変わります。列番号を決め打ちせず、ヘッダー行から id の位置を探してから読んでいるのはそのためです。
平常時のベースライン
まず、何も起きていないときの数字です。OSのメンテナンスが動いた時間帯を除いて集計しました。
| 指標 | 中央値 | p95 | 最大 |
|---|---|---|---|
| メモリ使用量 | 415MB | 443MB | 466MB |
| ロードアベレージ(1分) | 0.02 | 0.15 | 1.21 |
| CPUアイドル率 | 100% | 86%(p5) | 57%(最小) |
2GBのうち21%しか使っていません。 メモリの空き(available)は最小でも1,324MB、43時間を通して1.3GBを下回りませんでした。
発見1:メモリのピークは、全部OSのメンテナンスだった
メモリ使用量の上位10件を並べると、上位6件がすべて unattended-upgrades(Ubuntuの自動更新)でした。7位から10位は fwupd。自作のアプリは1件も入っていません。
全2,590分のうち、メモリ使用量1位のプロセスの内訳はこうなります。
| プロセス | 1位だった回数 | 割合 |
|---|---|---|
| python(アプリ本体) | 2,155 | 83.2% |
| systemd-journald | 417 | 16.1% |
| unattended-upgrades | 12 | 0.5% |
| fwupd | 6 | 0.2% |
時間の99%はアプリが1位です。 ところがピークを作るのは、残り0.7%の時間に動くOSのメンテナンスでした。
平均を見ていたら絶対に気づきません。1分刻みで記録して、最大値を見にいったから見えた話です。
発見2:自動更新が、アプリを再起動していた
9月4日 06:19〜06:28 の10分間を1分刻みで並べます。
| 時刻 | メモリ使用量 | 1位のプロセス | RSS | CPUアイドル |
|---|---|---|---|---|
| 06:18 | 400MB | python | 72.0MB | 100% |
| 06:19 | 457MB | unattended-upgr | 113.2MB | 67% |
| 06:20 | 459MB | unattended-upgr | 113.2MB | 67% |
| 06:21 | 455MB | unattended-upgr | 118.6MB | 91% |
| 06:22 | 641MB | unattended-upgr ×2 | 208.4 + 143.3MB | 67% |
| 06:23 | 587MB | unattended-upgr | 208.8MB | 65% |
| 06:24 | 625MB | unattended-upgr | 209.2MB | 66% |
| 06:25 | 586MB | unattended-upgr | 209.4MB | 64% |
| 06:26 | 622MB | unattended-upgr | 209.6MB | 66% |
| 06:27 | 594MB | unattended-upgr | 264.1MB | 69% |
| 06:28 | 561MB | unattended-upgr | 209.7MB | 63% |
| 06:29 | 444MB | python | 43.3MB | 100% |
最後の2行を見てください。
06:27に自動更新が264MBまで膨らみ、06:29には python のRSSが 72.0MB → 43.3MB に落ちています。
アプリが再起動されました。 依存パッケージが更新され、サービスが再起動されたのだと考えられます。
そして再起動後、python のRSSは43.3MBのまま。平常時の72MBに戻るまで数時間かかっています。 つまりあの72MBは、起動直後の値ではなく、動いているうちに積み上がった状態だったわけです。
自動更新は便利ですが、勝手にアプリを止めて起動し直します。 それが朝の6時22分に起きていて、私はまったく知りませんでした。1分刻みで記録していなければ、永久に気づかなかったはずです。
このとき mem_available は最小1,324MB。余裕があったので何も壊れませんでしたが、メモリが逼迫している環境で同じことが起きたら、OOM Killerが走ってもおかしくありません。
発見3:fwupd は 188MB まで跳ねる
前回の記事で、fwupd(物理デバイスのファームウェア更新デーモン)について私はこう書きました。
fwupdは測定の2分前に起動していました。常時この44MBを消費し続けているとは限りません。
慎重に書いておいて正解でしたが、実態は想定より大きかったです。
9月5日 01:07〜01:12 の6分間、fwupd は 188.8MB を占め、システム全体のメモリ使用量が578MBまで上がりました。
アプリ本体の最大値(87.9MB)の、2倍以上です。
43時間で fwupd が1位になったのは6分だけ。しかしその6分は、メモリ使用量の上位10件のうち4件を占めています。
VPSに物理ファームウェアはありません。更新するものが1つもないデーモンが、アプリの2倍のメモリを瞬間的に取っていく。前回「働きようがない」と書きましたが、働かないだけでなく、たまに大きく場所を取るというのが正確なところでした。
発見4:バッチ処理はCPUを使うが、メモリは使わない
グラフの下段を見てください。9月5日の 10:40〜11:30 に、約40分続くCPUの山があります。
- CPU使用率:15分移動平均で約35%(平常時は約2%)
- ロードアベレージ:1.0〜1.2(平常時は0.02)
- CPUアイドル率:最小 57%
はっきりした負荷です。ところが同じ時間帯のメモリは425〜442MB。平常時の中央値415MBとほとんど変わりません。python のRSSも85MB前後で動きません。
私は「バッチ処理が走ればメモリが跳ねる」と予想していました。外れました。 跳ねたのはCPUだけです。
考えてみれば当然で、データを1件ずつ読んで処理して書き出すタイプの処理は、全部をメモリに載せません。メモリを食うのは、大きなデータをまるごと展開する処理のほうです。
「重い処理=メモリが要る」は、いつも成り立つわけではない。 ここは測らないと分かりませんでした。
グラフで上下の山が重なっていないのは、そういう意味です。
発見5:計測そのものがメモリを食っていた
systemd-journald(ログ管理)のRSSの推移です。
| 時点 | RSS |
|---|---|
| 9/4 12:00 | 26.6MB |
| 9/4 18:00 | 41.6MB |
| 9/5 00:00 | 57.0MB |
| 9/5 06:00 | 70.0MB |
| 9/5 12:00 | 94.8MB → 27.3MB |
26MBから94MBまで肥大し、ログのローテーションで一気に戻りました。
原因は明らかで、1分ごとに走るcronが、毎回ログを吐いているからです。全2,590分のうち417分(16%)で、journaldがメモリ使用量1位になっていました。
つまり測るという行為が、測る対象を変えている。
1分間隔はやりすぎでした。5分間隔なら記録の密度はほとんど落ちず、journaldへの負荷は5分の1になります。これから同じことをやる人には、5分間隔を勧めます。
発見6:スワップは無駄にならなかった
このサーバーは、計測を始める前にスワップを2GB追加しました。さくらのVPSはスワップなしのイメージが提供される場合があるためです(前回の記事に書きました)。
43時間のスワップ使用量の推移です。
0MB → 1MB → 11MB → 40MB → 46MB → 30MB
最大46MB、実際に使われました。
メモリには1.3GB以上の余裕があります。それでもスワップが使われるのは、長時間触られていないページがディスクに退避されるからです。逼迫していなくても、Linuxは静かに整理をしています。
vm.swappiness=10(メモリが逼迫するまでスワップを控える設定)にしてあってこの量です。作らずにいたら、641MBのピーク時に緩衝材がまったくない状態でした。
まとめ
43時間・2,590サンプルから言えることです。
- 平常時はメモリ415MB(2GBの21%)、CPUアイドル100%、ロード0.02。43時間無停止、欠測ゼロ
- メモリのピークを作るのは、自分のアプリではなくOSのメンテナンス。上位10件のうち自作アプリはゼロ
- 自動更新はアプリを勝手に再起動する。それが1分刻みの記録に残っていた
fwupdは平常時44MB、ピーク188MB。アプリ本体の2倍以上- バッチ処理はCPUを使うがメモリは使わない。「重い処理=メモリが要る」は常に成り立つわけではない
- 1分間隔の計測はjournaldを肥大させる。5分間隔で十分
- スワップは余裕があっても使われる。最大46MB
そして、いちばん大事なこと
1回のスナップショットでは、この6つのどれも見えませんでした。
前回の記事は「ある瞬間のメモリは504MB」で終わっています。それは正しいけれど、朝6時に641MBまで上がっていることも、そのときアプリが再起動されていることも、写っていません。
サーバーの状態は、点ではなく線で見るものでした。仕掛けるのに5分、あとは放っておくだけです。
次は、この環境にAIの自動化ツールを載せて、同じ方法で測ります。ベースラインが43時間分あるので、載せた後の差分がそのまま「そのツールのコスト」になります。