家庭内の分散AI推論基盤「Coordinator」では、タスクの状態・結果・モデル統計を PostgreSQL に貯めている。日次の pg_dump バックアップは 2026-06-06 に整備済みで、毎日 02:00 に AI-Core から公開サーバー mars へ転送し、7世代を保持している。仕組みとしては完成している――はずだった。
だが、ひとつ宿題が残っていた。一度も復元したことがない。このシリーズで繰り返してきた合言葉どおり、「復元したことのないバックアップは、ただの仮説」である。今回はその仮説を実測で潰す。
大原則:テストリストアで本番DBに触ってはいけない
結論から言うと、テストリストアの最大の罠は「本番DBへ流し込んでしまうこと」だ。バックアップは 02:00 時点のスナップショットなので、本番の coordinator に上書きすれば、その後に増えたデータを古い状態で潰しかねない。これは、このシリーズで何度も痛い目を見てきた「サイレント故障」の典型例になり得る。
そこで設計の芯はシンプルにした。テストは必ずスクラッチDB coordinator_restore_test に入れ、本番 coordinator は件数照合のために読むだけ。スクリプトには「テストDB名が本番DB名と一致したら即中止」という安全弁も入れた。徹底的に、本番には指一本触れない。
着手前に、まず実機の pg_backup.sh を読む
引き継ぎ資料にはバックアップの要約しか書いていなかったため、復元手順を仮説で書く前に、配備済みの pg_backup.sh を直接読んだ。前回も学んだとおり、引き継ぎ資料は実機より古いことがある。確認すべきは「単体 pg_dump か pg_dumpall か」「--clean / --create の有無」だ。
PGPASSWORD="..." pg_dump -h localhost -U coordinator coordinator | gzip -9 > coordinator_YYYYMMDD_HHMMSS.sql.gz
判明したのは、単体DBのプレーンSQLを gzip しただけで、--clean も --create も付いていないということ。つまり復元は pg_restore ではなく、空のDBへ psql で流し込む形になる。カスタム形式(-Fc)と勘違いして pg_restore を叩いていたら、いきなり詰まっていた。フォーマットの確定は、手順を分ける最初の分岐点だ。
もうひとつ、ダンプの先頭にこんな1行があった。
\restrict PDXesNqIegUpkbFbW0VhkrBrc0jnt4kZKhZ7ToKmDCVWuoKHPK8LfwPPwDpS1FA
これは比較的新しい pg_dump(PostgreSQL 18系)が付ける psql メタコマンドで、ダンプ経由のコマンド注入を防ぐためのもの。ランダムトークンはダンプごとに変わる。psql 経由なら無加工で素通りするが、逆に言えば「手作業の sed で加工して流す」ような雑なことはできない、という念押しでもある。
非破壊テストリストアを6ステップで回す
実行ホストは PostgreSQL が同居する AI-Core。スクリプトは次の6段で、すべて本番に触れずに完結する。
- ① mars から最新の
.sql.gzを取り戻す。これでリモート保管分の「到達性・可読性」も同時に検証できる。 - ②
gzip -tで CRC 健全性チェック。転送途中の破損をここで弾く。 - ③ ダンプ先頭40行を表示。フラグ確定の二重確認。
- ④ スクラッチDBを drop → 空作成(本番名一致なら即中止の安全弁つき)。
- ⑤
zcat | psql -v ON_ERROR_STOP=1で流し込む。最初のエラーで止めるので、部分復元を「成功」と誤認しない。 - ⑥ 復元DBの全テーブル件数を本番と照合。
流し込みは ON_ERROR_STOP に引っかからず最後まで通り、restore done。ここまでは順調だった。
予測が外れた ― 「件数がぴったり一致」で一瞬手が止まる
事前に手順書へはこう書いていた。「バックアップは 02:00 のスナップショットだから、復元DBの件数は本番よりやや少ないはず」。本番はその後タスクが増えているからだ。ところが実測はこうだった。
table restore prod
comparison_results 289 289
comparison_sessions 58 58
model_domain_stats 27 27
tasks 309 309
全テーブル、完全一致。 一瞬「まさかテスト側が本番DBを読んでしまったのか?」と疑った。だが、ここで早合点しないのがこのシリーズの規律だ。条件反射で「バグだ」と断罪する前に、まず切り分ける。
切り分けると、答えは拍子抜けするほど単純だった。ステップ④で coordinator_restore_test を 空から作り直し(ログにも does not exist, skipping → CREATE DATABASE と出ている)、ステップ⑤で gzip ファイルからそのDBだけに流し込んだ結果が 289件なのだ。つまりこれは、バックアップファイルに実データが確かに入っていて、独立したDBへ正しく復元された動かぬ証拠である。本番を読んだのではない。
では、なぜ一致したのか。理由は「02:00 以降、このクラスタに新規タスクが一件も発生していなかった」だけ。家庭内ラボの、誰も使っていない朝の時間帯。件数の一致は異常ではなく、「スナップショットが最終活動時点まで完全」を意味していた。
ダメ押しの独立性チェックとして、両DBの最新タイムスタンプも見ておく。
SELECT max(created_at) FROM tasks; -- restore 側 / prod 側
両方が 02:00 以前なら「02:00 以降トラフィックなし」が確定し、一致の説明がつく。計測してから信じる。そして計測結果を、思い込みで断罪しない。予測が外れたときこそ、慌てて結論に飛びつかず、観測事実を一段ずつ積む――この一連が、今回いちばんの収穫だった。
ついでに見えた、自動化への伏線
スクラッチDBの作成で sudo のパスワードプロンプトが出た。対話実行では何の問題もないが、将来このテストリストアを定期ジョブ化(CI/CD の「検証規律のジョブ化」)するなら、非対話で CREATE DATABASE を通す必要がある。postgres 限定の passwordless sudo か、coordinator ロールへの CREATEDB 付与か――そのときの宿題として記録しておく。
仮説が、検証済みの事実になった
これで item 9 のコア、「一度、実リストアまで通す」は達成できた。バックアップは「あるはず」から「復元できると確かめた」へ昇格した。あわせて、本番DBが失われたときの破壊的な災害復旧手順(書き込みを止める → 復元 → 「DB → Worker → Coordinator」の順で戻す)も手順書に明文化した。ただしこちらは本番でしか使わない手順なので、今回は実行しない。順序ミスで二次被害を出してきた過去があるからこそ、紙の上で順序を固めておく。
派手な機能追加ではない。だが、「壊れたときに本当に戻せるか」を一度でも実測した基盤は、しなかった基盤とは別物だ。仮説をひとつ、事実に変えた回だった。