LitestreamによるS3へのリアルタイムSQLiteバックアップ:RPOほぼゼロのリカバリを実現する完全ガイド

Database tutorial - IT technology blog
Database tutorial - IT technology blog

月額5ドルの単一VPS上でSQLiteを使ってWebアプリケーションを運用することは、私のお気に入りのデプロイメントパターンの1つになっています。ネットワークレイテンシを20マイクロ秒まで削減し、ホスティング費用をわずかな金額に抑え、スタック全体を単一のバイナリに凝縮できます。それでも、多くのエンジニアが導入をためらっています。常に付きまとう懸念はデータの安全性です。ホストのNVMeドライブが故障したり、クラウドプロバイダがインスタンスを喪失したりしたらどうなるでしょうか?

cronで定期実行するスナップショットバックアップには、危険なRPO(目標復旧時点)のギャップが残ります。例えば6時間間隔でダンプを取得するアプリを考えてみましょう。バックアップから5時間50分後にサーバーがダウンした場合、約6時間分のユーザーの購入履歴、コメント、アカウント登録データが消失してしまいます。Litestreamはこの問題を鮮やかに解決します。SQLiteの先行書き込みログ(WAL)フレームを、Amazon S3やS3互換オブジェクトストレージへ直接リアルタイムにストリーミング配信します。ここでは、1秒未満のRPOリカバリを実現するために、Litestreamをゼロから設定する手順を解説します。

クイックスタート(5分):SQLiteをS3へストリーミング

Linux上でLitestreamを起動するのにかかる時間は3分、コマンドもわずか数行です。バイナリをインストールし、保存先となるS3バケットを設定して、最初のライブレプリケーションストリームを検証します。

1. Litestreamのインストール

UbuntuやDebianでは、GitHubから直接リリースパッケージを取得します。

# Litestreamのdebパッケージをダウンロードしてインストール
wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.deb
sudo dpkg -i litestream-v0.3.13-linux-amd64.deb

# インストールを確認
litestream version

Alpine LinuxやFedoraへデプロイする場合は、コンパイル済みのtarballをダウンロードして展開し、バイナリを /usr/local/bin/litestream に配置してください。

2. AWS S3認証情報とバケットの設定

専用のAmazon S3バケット(例:my-sqlite-backups-bucket)を作成します。そのバケットに厳密にスコープを絞ったIAMユーザーまたはロールを設定してください。必要な権限は、s3:PutObject、s3:GetObject、s3:ListBucket、s3:DeleteObject の4つです。シェルセッションでキーをエクスポートします。

export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_REGION="us-east-1"

3. レプリケーションとデータリストアのテスト

テスト用データベースを作成します。Litestreamは状態の変更を追跡するためにWALフレームに依存しているため、直ちにWALモードを有効化してください。

mkdir -p /var/lib/myapp
sqlite3 /var/lib/myapp/app.db "PRAGMA journal_mode = WAL; CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT); INSERT INTO users (name) VALUES ('Alice');"

ターミナルから直接レプリケーションを開始します。

# app.dbをS3へレプリケート
litestream replicate /var/lib/myapp/app.db s3://my-sqlite-backups-bucket/app.db

LitestreamはベースラインとなるスナップショットをS3へプッシュした後、WALのコミットを監視しながら待機状態に入ります。別のターミナルウィンドウを開き、リストアの訓練を実施してみましょう。

# 最新の状態を新しいファイルパスへ復元
litestream restore -o /tmp/restored-app.db s3://my-sqlite-backups-bucket/app.db

# データが無事に復元されたか検証
sqlite3 /tmp/restored-app.db "SELECT * FROM users;"

詳細解説:Litestreamが1秒未満のRPOを実現する仕組み

本番環境のワークロードにLitestreamを導入する前に、その同期エンジンが実際にどのようにデータを転送しているのかを理解しておく必要があります。PostgreSQLのようなリレーショナルエンジンは、専用のレプリケーションデーモンがTCP経由でリードレプリカに先行書き込みログをストリーミング配信します。しかし、SQLiteの構造は根本的に異なります。SQLiteはローカルのディスクファイルに直接書き込みを行うインプロセスのCライブラリです。

SQLite WALモードの役割

デフォルトでは、SQLiteはロールバックジャーナルモードで動作し、書き込み処理中はデータベース全体をロックします。先行書き込みログ(PRAGMA journal_mode = WAL;)を有効にすると、そのI/Oパターンが一変します。データベースへの書き込みは、メインの .db ファイルを直接変更しなくなります。代わりに、隣接する .db-wal という名前のファイルへシーケンシャルに追記されます。

読み取り処理は書き込み処理をブロックすることなく .db から過去のページをクエリし続け、書き込み処理はログに新しいページを追記します。定期的なタイミングで、SQLiteはチェックポイントを実行します。これにより、.db-wal からベースファイルへとページがフラッシュされ、ログがリセットされます。標準的なcronバックアップツールはここで苦戦します。チェックポイントの途中でファイルをコピーしてしまい、ページの破損やバックアップファイルの破壊につながることが頻繁にあるためです。

継続的なフレームシャドウイング

Litestreamは、アプリケーションの横で軽量なデーモンとして稼働します。共有メモリロックカウンターを利用して、SQLiteのWALインデックスファイル(.db-shm)を監視します。トランザクションがコミットされるたびに、Litestreamは新しく書き込まれた4KBのフレームを即座に読み取ります。チェックポイントを待つことなく、新しいフレームを圧縮されたLitestream Snapshot(LSS)ファイルにまとめ、およそ100〜250ミリ秒以内にS3へプッシュします。

[アプリケーションの書き込み] 
        │
        ▼
  app.db-wal (ローカルNVMe)
        │
  [LitestreamがWALフレームを監視]
        │
        ├───────────────┬───────────────┐
        ▼               ▼               ▼
  スナップショット (ベース) WALフレーム 1   WALフレーム 2  --> S3ストレージ

この継続的な転送により、データ損失の危険にさらされる時間枠が数時間から1秒未満へと大幅に短縮されます。一括処理中であっても、LitestreamはディスクI/Oが許す限り高速に差分をストリーミングします。シードスクリプトのテスト時や生データのインポート準備時(ブラウザで変換したJSONデータセットをSQLiteテーブルに流し込むような場合など)でも、WALにコミットされたすべてのレコードは、インジェスト完了から数秒後にはS3へと届きます。

高度な実践:Docker、Systemd、マルチレプリカ構成

ローカルでのテストには手動のターミナルコマンドで十分ですが、本番サーバーにはsystemdによる監視、マルチクラウドの冗長性、そして整理されたコンテナのエントリーポイントが必要です。

1. litestream.ymlによる構成管理

データベースとレプリカの転送先は /etc/litestream.yml で管理します。

dbs:
  - path: /var/lib/myapp/app.db
    replicas:
      - type: s3
        bucket: my-sqlite-backups-bucket
        path: app.db
        region: us-east-1
        sync-interval: 1s
        retention: 72h
      # セカンダリのオフサイトレプリカ (Cloudflare R2)
      - type: s3
        endpoint: https://<account-id>.r2.cloudflarestorage.com
        bucket: my-dr-backup
        path: app.db
        access-key-id: ${R2_ACCESS_KEY_ID}
        secret-access-key: ${R2_SECRET_ACCESS_KEY}

Litestreamは標準で複数のエンドポイントへの同時ストリーミングに対応しています。この設定では、コミットされたトランザクションがAWS S3とCloudflare R2へ並行して分散配信されるため、単一プロバイダの障害から保護されます。

2. SystemdによるLitestreamの管理

/etc/systemd/system/litestream.service に常駐サービスファイルを作成します。

[Unit]
Description=Litestream SQLiteレプリケーションサービス
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=www-data
Group=www-data
EnvironmentFile=/etc/default/litestream
ExecStart=/usr/local/bin/litestream replicate -config /etc/litestream.yml
Restart=always
RestartSec=5s
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

バケットの認証情報を /etc/default/litestream に記述し、root権限のみにアクセスを制限(chmod 600)した上で、systemdをリロードしてサービスを開始します。

sudo systemctl daemon-reload
sudo systemctl enable --now litestream
sudo systemctl status litestream

3. Dockerコンテナ向けの「Exec」パターン

Docker内でLitestreamを実行するにあたり、複雑なマルチコンテナのオーケストレーションは不要です。Litestreamは litestream exec によるプロセス監視モードを提供しています。コンテナ起動時に、ローカルボリュームが空であれば最新のデータベースバックアップをダウンロードし、メインアプリケーションを子プロセスとして実行しながら、シャットダウンまでWALフレームをストリーミングし続けます。

以下は、本番環境向けのコンテナエントリーポイント(run.sh)です。

#!/bin/sh
set -e

DB_PATH="/data/app.db"

# ローカルにデータベースファイルが存在しない場合はS3からリストア
if [ ! -f "$DB_PATH" ]; then
  echo "ローカルデータベースが見つかりません。S3から復元しています..."
  litestream restore -if-replica-exists -o "$DB_PATH" s3://$S3_BUCKET/app.db
fi

# メインアプリケーションコマンドを監視するスーパーバイザーとしてLitestreamを実行
exec litestream exec -config /etc/litestream.yml -- ./my-web-server -db "$DB_PATH"

ホストマシンがクラッシュした場合は、別の環境で代替コンテナを起動するだけです。スクリプトが空のディレクトリを検知し、最後の1秒に至るまでのデータベース状態をS3から取得して、アプリケーションを立ち上げます。

本番運用の実践Tipsとトラブルシューティング

継続的なトラフィック下でLitestreamを運用するには、WALファイルの挙動、監視、クラウドストレージの保持期間に細心の注意を払う必要があります。

SQLiteのチェックポイントを制御下に置く

通常、SQLiteはWALファイルが1,000ページ(約4MB)に達すると自動的にチェックポイントをトリガーします。Litestreamはこれに問題なく追従します。しかし、30秒かかる分析レポートのような長時間実行される読み取りクエリがあると共有ロックが保持され、チェックポイントがブロックされてしまいます。こうなると、WALファイルが数ギガバイトを超えて膨らんでしまう恐れがあります。ファイルサイズは直接確認するようにしてください。

# 現在のWALファイルサイズを確認
ls -lh /var/lib/myapp/app.db*

SQLiteのコネクションプールには、必ずビジータイムアウト(PRAGMA busy_timeout = 5000; など)を設定してください。これにより、一時的なロック競合によってフレーム処理が遅延するのを防ぐことができます。

同期ヘルスの監視

Litestreamが書き込みを適切に追跡できているかどうかは、generations コマンドで確認できます。

litestream generations -config /etc/litestream.yml /var/lib/myapp/app.db

これにより、S3に保存されているバックアップ世代とその作成日時が一覧表示されます。リストが空である場合や同期のタイムスタンプが停止している場合は、直ちに journalctl -u litestream -n 100 でデーモンのログを調査し、認証エラーやネットワークタイムアウトが発生していないか確認してください。

S3のライフサイクルルールの設定

Litestreamは、設定内の retention の指定(retention: 72h など)に従って期限切れのスナップショットを削除します。ただし、マルチパートアップロードが中断された場合、参照されないチャンクの残骸がAWS上に残ってしまうことがあります。不完全なマルチパートアップロードを3日後に破棄するS3ライフサイクルルールを設定してください。これにより、不要なアップロードの残骸によって月々のAWS請求額が膨らむのを防ぐことができます。

SQLiteとLitestreamを組み合わせることで、ローカルNVMeストレージならではのサブミリ秒の読み取りレイテンシと、分散クラウドプラットフォームがもたらすディザスタリカバリの保証を同時に手に入れることができます。マルチノードのデータベースクラスタを運用する手間を一切かけずに、堅牢なデータ永続性を実現しましょう。

Share: