Dev Study
← 出して、戻して、直す — 変更と復旧の実務
レッスン 12 / 12·約 9 分·第24期 出して、戻して、直す

総合演習 — 出して、壊して、戻して、直す

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存今ここ

コースの最終回です。ここまでの回で扱った手を、1つのアプリの一生に並べ直します。

まず結論

小さなメモアプリを要件から出し、新版の不具合で本文を消し、戻して、秘密を入れ替えるところまで1本の実演で通します。フラグを切ると不具合は止まりましたが、消えた本文は1行も戻りませんでした。

行を戻せたのは、壊れる前に WAL のアーカイブを取り、フラグを入れる直前の時刻を控えていたからです。戻す手は、どれも壊れる前に置いたものでした。

? 読む前に予測

新版の不具合で、本文が空になった行が2件できました。原因を調べる前に機能フラグを切り、直後に本文が空の行を数えます。何件でしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

どの段にも、壊れる前に置いた手が1つずつある

題材はメモの一覧と追加だけのアプリです(Python + PostgreSQL 18.6 + nginx 1.30.4、Docker)。新機能は「メモにタイトルを付けられる。本文は変えない」。この1本を、要件から最後まで通します。

各段で使った手と、それを扱った回を並べます。

演習の段と、その段で使った手表
段 この演習でしたこと 扱った回
要件 「本文は変えない」と書く 第21期で扱う
動かす nginx → アプリ → DB の3段 第7期で扱う
出す v2 をフラグ Off で置く 本期1・3本目
スキーマ lock_timeout を付けて列を足す 本期4本目(順序は第16期で扱う)
壊れる タイトル編集で本文が空に —
緩和 原因を調べずフラグを切る 本期10・11本目
戻す WAL アーカイブから新しい実体へ PITR 本期7〜9本目
秘密 DB パスワードを入れ替える 本期5・6本目
直す 修正と事後の記録 第15期

第7期で扱うとおり、本番は開発サーバより段が多く、この演習もアプリの前に nginx を1段挟みます。

要件には「本文は変えない」と書きました。しかしタイトルだけ届いたときに本文の値をどう扱うかまでは仕様に落とさず、v2 は「無ければ空」で上書きしました。要件を書いても、実装が守るとは限りません(第21期で扱う、仕様に書かれない部分は実装が決める話)。

壊れる前に置いたものと、壊れた後に使えたもの
壊れる前に置いたフラグ・lock_timeout・WAL アーカイブ・目標時刻の控え
フラグを入れて、本文が空の行が2件
フラグを切る以後の破壊は止まる
空の行は2件のまま書かれた行は戻らない
控えた時刻まで、別の実体に戻す
壊れた2行だけ書き戻す後から書かれた正しい行は残る
  • ・壊れた後に打てた手は、上の段で置いたものだけ
  • ・書き戻しに使った時刻は、フラグを入れる直前に控えていた
具体実物

本教材の実演の出力(スクリプト非掲載) — 出して、壊れる

同じスクリプトを2回通し、件数・状態・エラー文は2回とも同じでした。1回目から段3〜6を抜粋します(行の省略あり)。

段3〜6(1回目から抜粋)コマンド出力
[09:41:55] ===== 3 v2 を置く(フラグ Off)。置いただけでは何も変わらない =====
{"version": "v2", "title_edit": false} [200]
{"error": "title edit is off"} [404]
[09:41:56] ===== 4 列を足す(lock_timeout を付ける)。長い読み取りが居座っていると諦めて出直す =====
ERROR: canceling statement due to lock timeout
alter exit=1
alter (retry) exit=0
[09:42:02] ===== 5 フラグ On の直前の時刻を控える(戻す目標)→ フラグ On → 利用者がタイトルを付ける =====
restore target: 2026-09-26 00:42:02.716457+00
[{"id": 1, "title": "買い物", "body": ""}, {"id": 2, "title": "会議", "body": ""}, {"id": 3, "title": null, "body": "設計の宿題: キャッシュの失効"}, {"id": 4, "title": null, "body": "新しいメモ(不具合の後に書かれた正しい行)"}] [200]
[09:42:03] ===== 6 検知(本文が空の行を数える)→ 緩和: 原因を調べる前にフラグを切る =====
empty bodies: 2
{"error": "title edit is off"} [404]
empty bodies after mitigation (データは戻っていない): 2

段3で v2 は置かれていますが、タイトル操作は 404 です。置くことと効かせることが分かれています。段4では、6秒の読み取りが居座る間に列追加が2秒で諦め、読み取りの後の出し直しで成功しました。列を足す時点で待ち行列に並び続けなかったのは、lock_timeout を先に付けていたからです。

段5の restore target が、この演習の要です。フラグを入れる前に時刻を控えました。段6でフラグを切ると新しい破壊は止まりますが、空の行は2件のままです。

✓ ここまでの確認

この演習では、title 列を足す前に v2 を置きました。それでも v2 の一覧表示が失敗しなかったのはなぜですか。

抽象原理

フラグが戻すのは以後の振る舞い。行は別の実体から取り出す

フラグで戻せたのは「これから何が起きるか」だけです。第16期で扱うとおり、コードを戻してもデータは戻りません。書かれてしまった行を戻す手は、DB の側に前もって置くしかありません。

PostgreSQL の文書は、WAL を途中で止めればその時点の一貫した状態が得られると書く一方で、制約も明記しています。

As with the plain file-system-backup technique, this method can only support restoration of an entire database cluster, not a subset.

本番のクラスタを丸ごと目標時刻へ戻すと、不具合の後に書かれた正しい行まで消えます。だから新しい実体に戻して、壊れた行だけを取り出すことにしました。

具体実物

本教材の実演の出力(スクリプト非掲載) — 戻して、入れ替える

段7の前に pg_switch_wal() で書きかけの WAL を閉じてアーカイブさせています。本期8本目で扱ったとおり、最後の WAL がアーカイブに届いていなければ目標時刻まで進めません。

段7〜9(1回目から抜粋)コマンド出力
2026-09-26 00:42:08.355 UTC [32] LOG: recovery stopping before commit of transaction 763, time 2026-09-26 00:42:03.832494+00
--- 復元した実体の notes:
1|(null)|牛乳を買う
2|(null)|会議のメモ: 次回は火曜
3|(null)|設計の宿題: キャッシュの失効
damaged ids: 1,2
COPY 2
UPDATE 2
empty bodies: 0
旧パスワードで新規接続:
OperationalError connection failed: connection to server at "172.20.0.2", port 5432 failed: FATAL: password authentication failed for user "memo_app"
開いたままの接続を使うアプリ(DB を読む):
[{"id": 1, "body": "牛乳を買う"}, {"id": 2, "body": "会議のメモ: 次回は火曜"}, {"id": 3, "body": "設計の宿題: キャッシュの失効"}, {"id": 4, "body": "新しいメモ(不具合の後に書かれた正しい行)"}] [200]

復元は最初の不具合の UPDATE のコミットの手前で止まり、復元側は3行です。id 4 はありません。本番へは where n.id = r.id and n.body = '' の条件で2行だけ書き戻し、UPDATE 2 の後、空の行は0件、id 4 も残りました。

段9では、貼ってしまった想定のダミーのパスワードを入れ替えました。別コンテナから旧パスワードで繋ぐと失敗します。同じ時点で、開いたままの接続を使うアプリは DB を読めて 200 でした。本期6本目の主題どおり、入れ替えは新しい接続にしか効きません。この演習ではアプリを新しいパスワードで起動し直して接続を閉じました。

この構成はアプリが1台です。本期2本目の対照群は作れず、起動・作り直しのたびに、起動待ちの確認で 502 が計3件出ていました。

✓ ここまでの確認

目標時刻まで戻した実体ができました。それを本番と丸ごと差し替えず、壊れた2行だけを書き戻したのはなぜですか。

⇄ 2つを見比べる
具体例 A

戻せたもの — フラグで止めた以後の振る舞い

フラグファイルを書き換えるだけで、タイトル操作は 404 に戻りました。原因を調べる前に打てた手です。ただし止まるのは以後の操作だけでした。

段6コマンド出力
empty bodies: 2
{"error": "title edit is off"} [404]
empty bodies after mitigation (データは戻っていない): 2
具体例 B

戻せなかったもの — DB に書かれた行

空の本文は、アプリをどう戻しても残ります。WAL アーカイブから新しい実体へ目標時刻まで戻し、壊れた2行だけを取り出して書き戻しました。

段8コマンド出力
damaged ids: 1,2
COPY 2
UPDATE 2
empty bodies: 0

左は一瞬で戻り、右は別の実体まで立てました。この差は、壊れた後の腕前の差でしょうか。

右の手順が使った材料は、それぞれいつ用意されたかを数えてみてください。

この章のまとめ

  • 置く・効かせる・列を足す・戻す・入れ替えるを、1つのアプリで通した。
  • フラグは以後の破壊を止めたが、書かれた行は1行も戻さなかった。
  • 行を戻せたのは、ベースバックアップ・WAL アーカイブ・フラグ前に控えた時刻があったから。PITR はクラスタ単位なので、新しい実体から必要な行だけを取り出した。
  • パスワードの入れ替えは新しい接続にしか効かない。確かめるならネットワーク越しに。
  • 残るのは直す段。修正と事後の記録は、緩和とは別の項目(第15期で見た記録の書式へ)。
  • SRE Workbook 第9章は The right time to create general-purpose mitigation tools is before an incident occurs, not when you are responding to an emergency. と書く。壊れたときに打てる手は、壊れる前に用意しておいた分しかない。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

演習の段4で `lock_timeout = '2s'` を付けた列追加は、長い読み取りが居座る間どうなりましたか。

用語集(3語)
緩和 (mitigation)
原因を直す前に、被害の広がりを止める手。SRE Workbook 第9章は原因の場所さえ分かれば打てるとし、ロールバックなどの汎用の手を事前に作っておくよう勧める。
PITR (point-in-time recovery)
ベースバックアップに WAL を途中まで再生し、指定した時刻の状態に戻すこと。PostgreSQL ではクラスタ全体の単位でしか戻せない。
trust 認証
pg_hba.conf の方式の1つで、パスワードを見ずに接続を認める。postgres 公式イメージでは local と 127.0.0.1 がこれになっていた。