VPSが9時間固まった夜 — コンテナ1つの再起動から ext4 読み込み専用まで

VPSが9時間固まった夜 — コンテナ1つの再起動から ext4 読み込み専用まで

「unhealthy だから再起動しとこう」。その一手で、VPS 全体が9時間応答しなくなり、最後はファイルシステムが読み込み専用になった記録。おまけに、復活したエージェントが勝手に API 課金を再開していた。

前作: OpenClaw_Docker環境の崩壊と復旧

環境

何が起きたか(時系列)

時刻 出来事
数週間前〜 OpenClaw が Up (unhealthy) のまま放置。OOM killer も何度か発動していた
15:01 docker restart で OpenClaw を再起動
15:01〜 ログが途絶。CPU スパイク、ディスク読込が 100MB/s で張り付き
〜23:58 Web もSSHもシリアルコンソールも無反応
23:58 管理パネルから再起動 → OS がシャットダウンに失敗 → 強制断
翌 00:00 起動3秒後に ext4 エラー → ルートが読み込み専用に → Docker 起動不可
翌 09:00 シャットダウン → イメージ保存 → 起動。起動時の自動 fsck で修復

症状から原因を読む

1. 「ポートは開くのに応答しない」=スラッシング

nc -z <host> 22     # succeeded
ssh <host>          # Connection timed out during banner exchange

TCP の受付はカーネルがやるので通る。でも sshd や nginx が返事をする余力がない。パネルのグラフではディスク読込が一定値に張り付いていた。メモリが足りず、ページをディスクから読み直し続けている状態。

Swap があると OOM killer が動かず、ログに Out of memory が残らないことがある。 「OOM ログがない=メモリの問題ではない」ではない。

2. 送信量のグラフで「侵入」を否定する

ログに海外 IP からの root 総当たりが大量に出ていて一瞬焦ったが、すべて [preauth] で切断。パネルの転送量(送信)も障害中ほぼゼロで、外部への持ち出しは見られなかった。

3. 読み込み専用の原因は「再起動の仕方」

EXT4-fs error (device vda3): ext4_mb_generate_buddy: group 479,
  block bitmap and bg descriptor inconsistent
Aborting journal on device vda3-8.
EXT4-fs (vda3): Remounting filesystem read-only

前回起動のログは23:58まで書けていた=固まっている間ディスクは正常。壊れたのは、固まった OS が正常に poweroff できず強制断になったとき。errors=remount-ro なので、不整合を見つけた ext4 が自分を読み込み専用にした。

役に立ったコマンド

# 前回起動のログ(persistent journal が有効なら読める)
journalctl --list-boots
journalctl -b -1 -n 15 --no-pager
journalctl -b -1 --since "YYYY-MM-DD 14:55" --until "YYYY-MM-DD 15:15" --no-pager

# 今回起動の最初の ext4 エラー(dmesg はリングバッファが上書きされがち)
journalctl -k -b 0 --no-pager | grep -iE "ext4|i/o error|read-only" | head

# 次回起動で自動 fsck が走るか
tune2fs -l /dev/vda3 | grep -i state
#   clean with errors → 走る見込み / clean → 走らない

# コンテナ ID から名前を引く(dockerd が止まっていても読める)
grep -o '"Name":"[^"]*"' /var/lib/docker/containers/<id前方>*/config.v2.json

復旧手順

  1. 読み込み専用のうちは何も書かない(データを読み出すには安全な状態)
  2. 管理パネルで「シャットダウン」→「イメージ保存」(バックアップ)
  3. 「起動」→ 起動時の自動 fsck で修復されることを期待
  4. 直らず (initramfs) 等に落ちたら、コンソールで fsck -y /dev/vda3
  5. tune2fs -l で clean を確認

Xserver VPS のパネルには「強制停止」やメモリのグラフはない。電源操作は起動・再起動・シャットダウンのみ。

おまけ1:白画面の正体は末尾スラッシュ

復旧後、https://example.com/openclaw がまっしろ。DevTools を見ると /assets/index-xxx.js が 404。

HTML が ./assets/... の相対パスで読んでいるので、

URL の末尾に / を付けるだけで直った。

おまけ2:復活したら API 課金も復活していた

ダッシュボードに入ると、ハートビート(定期的にエージェントを起こす仕組み)が Claude Sonnet で動いていた。コンテナが死んでいた2か月は止まっていたのに、復旧した瞬間に再開。

ざっくり試算(Sonnet 4 = 入力$3 / 出力$15 per 1M tokens)

設定 1回 30分間隔 6時間間隔
既定(会話履歴を毎回送る ≈100K tokens) ≈$0.3 ≈$15/日 ≈$1.2/日
isolatedSession + lightContext(≈2〜5K tokens) ≈$0.01 ≈$15/月 ≈$1/月
上記 + Haiku ≈$0.003 ≈$5/月 ほぼ0

API 従量課金で既定設定のまま放置すると、何もしていないのに枯渇する。

止める・安く再開する

# 停止(話しかけた時の返答や Cron Jobs は動く)
openclaw config set agents.defaults.heartbeat.every "0m"

# 再開するなら、長い間隔 + 履歴を送らない + 安いモデル
openclaw config set agents.defaults.heartbeat.every "6h"
openclaw config set agents.defaults.heartbeat.isolatedSession true
openclaw config set agents.defaults.heartbeat.lightContext true
openclaw config set agents.defaults.heartbeat.model "<provider>/<cheap-model>"

Docker なら先頭に docker exec <container> を付ける。キー名は公式ドキュメント(Heartbeat)準拠で、バージョンによって違う可能性あり。

ローカル LLM(Ollama)にすれば API 代はゼロだが、同じ 6GB の VPS に載せると今回と同じことが起きかねない。やるなら別マシンで。

再発防止

# 暴れても OpenClaw だけが落ちるように
docker stats --no-stream docker-openclaw   # 先に現在値を確認(通常 約470MB だった)
docker update --memory=1.5g --memory-swap=1.5g docker-openclaw

通常時ぎりぎり(1g)ではなく余裕を持たせたのは、事故の引き金が起動時の一時的なメモリ消費だったから。ぎりぎりだと起動のたびに OOM kill → 再起動ループになりうる。

docker update はコンテナ再作成で消えるので、compose にも書く:

openclaw:
  mem_limit: 1.5g
  memswap_limit: 1.5g

その他:

教訓

「とりあえず再起動」は、余裕のあるシステムでだけ効く。 メモリに余裕のない1台に全部載せている環境では、その一手が全体を巻き込む。