最悪のタイミングでバックアップが失敗するとき
毎晩エラーなしで実行されているバックアップジョブは、守られているという安心感を与えてくれる。実際に必要になる日が来るまでは。ダンプが壊れていた。あるいは、30分で戻ると思っていたデータベースのリストアに6時間かかった。または、最新のクリーンなバックアップが4日前のものだった——ジョブが気づかれないまま失敗し続けていたのに、誰も気づいていなかったのだ。
これは不運ではない。バックアップをゴールとして扱い、リカバリを後回しにした当然の結果だ。ほとんどのチームはスケジュールを丁寧に設計し、設定したあとは放置する。テストリストアを実行するチームはいない。根本原因はシンプルだ——チームはバックアップを所有している。だが、リカバリを所有しているチームはどこにもいない。
バックアップアプローチの比較
すべての環境に合う万能な戦略は存在しない。それぞれのアプローチは、速度・リカバリの粒度・ストレージコスト・運用の複雑さをそれぞれ異なるトレードオフで提供する。適切な組み合わせを選ぶには、まず選択肢を正確に理解することから始めるべきだ。
論理ダンプ(pg_dump、mysqldump)
最も一般的な出発点であり、それには理由がある。論理ダンプはデータをSQLステートメントまたはポータブルなバイナリ形式でエクスポートする。人間が読めて、メジャーバージョンをまたいで移植可能で、単一コマンドで簡単に自動化できる。
# PostgreSQLの圧縮ダンプを作成する
pg_dump -U postgres -d mydb -Fc -f mydb_backup.dump
# 新しいデータベースにリストアする
createdb mydb_restored
pg_restore -U postgres -d mydb_restored mydb_backup.dump
落とし穴もある。大規模データではダンプが遅い。300GBのPostgreSQLデータベースをダンプするのに3〜4時間かかり、リストアはさらに長くなることが多い。ポイントインタイムリカバリにも対応していない——ダンプが実行された瞬間にしか戻れない。小規模な環境には十分だが、トラフィックの多い本番システムには大きな欠点だ。
物理バックアップ(pg_basebackup、Percona XtraBackup)
SQLをエクスポートする代わりに、物理バックアップは生のデータディレクトリを直接コピーする。同じ300GBのPostgreSQLデータベースでも、pg_basebackupなら20〜30分で完了する——pg_dumpの数時間と比べると歴然だ。本番システムにとって、この差は無視できない。
# PostgreSQLの物理バックアップ
pg_basebackup -U postgres -D /backups/base -Fp -Xs -P
# MySQL用Percona XtraBackup
xtrabackup --backup --target-dir=/backups/mysql_base
xtrabackup --prepare --target-dir=/backups/mysql_base
制約もある。物理バックアップはリストア時にメジャーデータベースバージョンが一致している必要があり、人間が読める形式ではない。しかし、ダウンタイムが直接コストに響く場合、その速度差は追加の複雑さを上回るメリットがある。
WAL/バイナリログストリーミング(ポイントインタイムリカバリ)
WALストリーミングは、本番環境のバックアップが本格的になる領域だ。物理ベースバックアップを取得し、Write-Ahead Logファイル(PostgreSQL)またはバイナリログ(MySQL)を継続的に別の場所に転送する。結果として、直近のスケジュールバックアップウィンドウではなく、任意の秒単位でリカバリできる。
# postgresql.conf: WALアーカイブを有効化する
wal_level = replica
archive_mode = on
archive_command = 'cp %p /wal_archive/%f'
# recovery.conf: 特定の時刻にリストアする
restore_command = 'cp /wal_archive/%f %p'
recovery_target_time = '2026-03-14 14:30:00'
recovery_target_action = 'promote'
pgBackRestやBarmanのようなツールは、WALのライフサイクル全体——圧縮・検証・保持期間管理——を手動スクリプトなしで処理してくれる。
スナップショットベースのバックアップ
クラウドボリュームスナップショット——AWS EBS、GCP Persistent Disk——やZFS・LVMのようなファイルシステムレベルのスナップショットは、データベースサイズに関係なく数秒で完了する。10TBのボリュームも10GBのものと同じ速さでスナップショットが取れる。この速さがインフラレベルのセーフティネットとして優れている。トレードオフは、特定のテーブルやトランザクションではなく、ボリューム全体をリストアする点だ。より広い戦略の1つのレイヤーとして有用だが、主要なリカバリパスとしては適していない。
各アプローチの長所と短所
- 論理ダンプ:設定が簡単で、バージョンをまたいで移植可能。50GB未満のデータベースやクロスバージョン移行に適している。リカバリが遅くPITRに対応していない。大規模な本番データベースにはこれだけに頼らないこと。
- 物理バックアップ:高速で一貫性があり、大規模データベースに適している。リストア時にメジャーバージョンが一致する必要があり、データベースエンジンをまたいで移植できない。本番PostgreSQLやMySQLの主要バックアップとして最適だ。
- WAL/バイナリログストリーミング:RPOがほぼゼロで、柔軟なリカバリターゲットを持つ。ストレージと運用オーバーヘッドが高い。数分以上のデータ損失が許容できない場合は必須だ。
- スナップショット:データベースサイズに関わらずほぼ瞬時に完了し、強固なインフラ保護を提供する。粒度が粗く、特定のクラウドやファイルシステム技術に依存する。データベースネイティブな戦略と組み合わせる補完レイヤーとして最適だ。
推奨構成:3-2-1ルール+リカバリテスト
3-2-1ルール——データの3コピー、2種類の異なるメディアタイプ、1コピーをオフサイト——は、ほとんどの本番データベースでは次のように実現する:
- 一般的な障害からの高速リカバリのために、物理バックアップを毎日ローカルに保存する
- ポイントインタイムリカバリのために、WALまたはバイナリログをオブジェクトストレージ(S3、GCS)に継続的に転送する
- 地域的な災害に備え、週次フルバックアップを別のクラウドリージョンまたはプロバイダーにレプリケートする
ほとんんどのチームが省略していること——実際にリストアをテストすることだ。月次で別の環境へのリカバリ訓練を実施し、データを検証しよう。重要なシステムでは、それを週次で自動化する。誰も一度もリストアしたことのないバックアップは、理論にすぎない。一度実行すれば、どこにギャップがあるか正確にわかる。
実装ガイド
PostgreSQL用pgBackRestのセットアップ
pgBackRestは物理バックアップ・WALアーカイブ・暗号化・並列処理を1つのツールで処理する。パッケージをインストールしたあと、次のように設定する:
# /etc/pgbackrest.conf
[global]
repo1-path=/backups/pgbackrest
repo1-retention-full=2
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=あなたの強力な暗号化キー
[mydb]
pg1-path=/var/lib/postgresql/14/main
# 初期化して最初のフルバックアップを実行する
pgbackrest --stanza=mydb stanza-create
pgbackrest --stanza=mydb --log-level-console=info backup --type=full
# 日次増分バックアップ
pgbackrest --stanza=mydb backup --type=incr
# 特定の時刻にリストアする
pgbackrest --stanza=mydb --delta restore \
--target="2026-03-14 14:30:00" \
--target-action=promote
XtraBackupによるMySQLバックアップの自動化
#!/bin/bash
# /usr/local/bin/mysql_backup.sh
BACKUP_DIR="/backups/mysql/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
xtrabackup --backup --target-dir="$BACKUP_DIR" \
--user=backup_user --password="$MYSQL_BACKUP_PASSWORD"
xtrabackup --prepare --target-dir="$BACKUP_DIR"
# S3にアップロードし、同期成功後にローカルコピーを削除する
aws s3 sync "$BACKUP_DIR" "s3://my-bucket/mysql-backups/$(date +%Y%m%d)/" \
--delete && rm -rf "$BACKUP_DIR"
echo "$(date): バックアップ完了" >> /var/log/mysql_backup.log
# crontab -e
# 毎日午前2時にフルバックアップを実行する
0 2 * * * /usr/local/bin/mysql_backup.sh >> /var/log/mysql_backup.log 2>&1
バックアップの整合性確認
バックアップファイルが存在することと、リストア可能であることは別物だ。論理ダンプの場合、少なくともリスティングチェックとテストデータベースへのリストアを実行すること:
# pg_dumpファイルの簡易整合性チェック
pg_restore --list mydb_backup.dump | head -30
# 完全検証:隔離されたテストデータベースにリストアする
createdb mydb_verify
pg_restore -U postgres -d mydb_verify mydb_backup.dump
# 本番環境との行数をスポットチェックする
psql -U postgres -d mydb_verify -c "SELECT COUNT(*) FROM users;"
psql -U postgres -d mydb -c "SELECT COUNT(*) FROM users;"
# 検証後にクリーンアップする
dropdb mydb_verify
リストア後、私は期待値に対する簡易チェックスクリプトを実行するために、レコードのサンプルをCSVにエクスポートすることがある。そのデータをJSONを期待するバリデーションパイプラインに渡す必要がある場合、toolcraft.app/ja/tools/data/csv-to-jsonを使う——ブラウザ上で直接変換できるので、機密性の高い本番データが自分のマシンの外に出ることはない。リカバリ訓練で実データを扱う際には、これが非常に重要だ。
サイレント障害の監視
サイレント障害は、ほとんどのチームが積極的に監視していない最大のリスクだ。アラートを設定する価値のある3つのシグナルを紹介する:
- バックアップジョブの終了コード——ゼロ以外の終了は即座にアラートを発動させるべきだ
- バックアップファイルの経過時間——最新バックアップが25時間以上前のものであれば、何か問題が発生している
- バックアップサイズの異常——突然のサイズ減少は、空テーブルや部分的な実行を意味することが多い
#!/bin/bash
# /usr/local/bin/check_backup_age.sh
BACKUP_FILE="/backups/mydb_latest.dump"
MAX_AGE=90000 # 25時間(秒単位)
if [ ! -f "$BACKUP_FILE" ]; then
echo "CRITICAL: バックアップファイルが見つかりません" | mail -s "DBバックアップアラート" [email protected]
exit 1
fi
FILE_AGE=$(( $(date +%s) - $(stat -c %Y "$BACKUP_FILE") ))
if [ "$FILE_AGE" -gt "$MAX_AGE" ]; then
echo "WARNING: バックアップが古くなっています(経過時間: ${FILE_AGE}秒、最大: ${MAX_AGE}秒)" | \
mail -s "DBバックアップアラート" [email protected]
fi
# cronで毎時チェックを実行する
0 * * * * /usr/local/bin/check_backup_age.sh
リカバリこそが真の指標
初めて設定する場合は論理ダンプから始めよう。扱いやすく、理解しやすく、1時間以内に動くものを用意できる。データベースが50GBを超えたら、WALまたはバイナリログストリーミングと組み合わせた物理バックアップに移行する。どのサイズでもインフラレベルの保護のためにクラウドスナップショットを追加しよう。
インシデントをうまく乗り越えるチームには共通点がある——練習していることだ。彼らは実際のリカバリ時間を知っている——ランブックの見積もりではなく、訓練での実測値を。バックアップを信頼しているのは、実際にリストアを目で確認したからだ。午前2時にプレッシャーの中でやることになる前に、その自信を築いておこう。

