前回の記事で、VPSのメモリを1回だけ測りました。ただしあれはアイドルに近い状態の1サンプルです。ピークは写っていません。

そこで、1分ごとに記録するスクリプトを仕掛けて放置しました。43時間、2,590サンプル、欠測ゼロ。

結果、予想は外れました。メモリのピークを作っていたのは、自分のアプリではありませんでした。

43時間のメモリとCPUの推移

上がメモリ、下が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最大
メモリ使用量415MB443MB466MB
ロードアベレージ(1分)0.020.151.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,15583.2%
systemd-journald41716.1%
unattended-upgrades120.5%
fwupd60.2%

時間の99%はアプリが1位です。 ところがピークを作るのは、残り0.7%の時間に動くOSのメンテナンスでした。

平均を見ていたら絶対に気づきません。1分刻みで記録して、最大値を見にいったから見えた話です。

発見2:自動更新が、アプリを再起動していた

9月4日 06:19〜06:28 の10分間を1分刻みで並べます。

時刻メモリ使用量1位のプロセスRSSCPUアイドル
06:18400MBpython72.0MB100%
06:19457MBunattended-upgr113.2MB67%
06:20459MBunattended-upgr113.2MB67%
06:21455MBunattended-upgr118.6MB91%
06:22641MBunattended-upgr ×2208.4 + 143.3MB67%
06:23587MBunattended-upgr208.8MB65%
06:24625MBunattended-upgr209.2MB66%
06:25586MBunattended-upgr209.4MB64%
06:26622MBunattended-upgr209.6MB66%
06:27594MBunattended-upgr264.1MB69%
06:28561MBunattended-upgr209.7MB63%
06:29444MBpython43.3MB100%

最後の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分間、fwupd188.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:0026.6MB
9/4 18:0041.6MB
9/5 00:0057.0MB
9/5 06:0070.0MB
9/5 12:0094.8MB27.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時間分あるので、載せた後の差分がそのまま「そのツールのコスト」になります。