オンライン物理バックアップの排他モードによるサーバ起動失敗問題について、PostgreSQL15とpg_exclusive_backupで再現してみよう！

これは、[PostgreSQL Advent Calendar 2022](https://qiita.com/advent-calendar/2022/postgresql) の5日目の記事です。

下記の[リリースノート](https://pgsql-jp.github.io/current/html/release-15.html)の項目のとおり、PostgreSQL15でオンライン物理バックアップの排他モードが削除されました。排他モードが削除された理由として、リリースノートには「このモードでデータベース・サーバが突然停止すると、サーバの起動に失敗する可能性があります」とありますが、今回は具体的にどのようにその問題が発生するか確認します。

> Remove long-deprecated <link linkend="backup-base-backup">exclusive backup mode</link> (David Steele, Nathan Bossart)
《機械翻訳》長く非推奨排他バックアップモードを削除しました。 (David Steele, Nathan Bossart)
> 
> If the database server stops abruptly while in this mode, the server could fail to start. The non-exclusive backup mode is considered superior for all purposes. Functions <function>pg_start_backup()</function>/<function>pg_stop_backup()</function> have been renamed to <function>pg_backup_start()</function>/<function>pg_backup_stop()</function>, and the functions <function>pg_backup_start_time()</function> and <function>pg_is_in_backup()</function> have been removed.
《機械翻訳》**このモードでデータベース・サーバが突然停止すると、サーバの起動に失敗する可能性があります**。 非排他バックアップ・モードは、すべての目的に対して優れていると考えられます。 関数pg_start_backup()/pg_stop_backup()はpg_backup_start()/pg_backup_stop()に名前が変更され、関数pg_backup_start_time()pg_is_in_backup()は削除されました。

この問題は、排他モードを利用できるPostgreSQL14以前で発生しますが、今回はあえて、「PostgreSQL15以降でも排他モードをサポートする[pg_exclusive_backupエクステンション](https://github.com/MasaoFujii/pg_exclusive_backup)」を使ってPostgreSQL15上で再現してみます。

ちなみに、排他モードと非排他モードの違いや、排他モードが削除された背景などについては、[PostgreSQLアンカンファレンスでの発表資料](https://www.slideshare.net/nttdata-tech/postgresql-backup-mode-pgunconf34-nttdata)に説明がありますので、ぜひ参照してください。

## PostgreSQL15上のpg_exclusive_backupの準備
PostgreSQL15が稼働する環境上で、[こちら](https://github.com/MasaoFujii/pg_exclusive_backup#install)を参考にpg_exclusive_backup 1.0をインストールします。ソースアーカイブファイルは[ここ](https://github.com/MasaoFujii/pg_exclusive_backup/tags)から入手します。
```terminal
$ tar zxf pg_exclusive_backup-1.0.tar.gz
$ cd pg_exclusive_backup-1.0/
$ make USE_PGXS=1
$ make USE_PGXS=1 install
```

インストールできたら、CREATE EXTENSIONコマンドでpg_exclusive_backupを登録します。
```terminal
$ psql -c "CREATE EXTENSION pg_exclusive_backup"
```

## 排他モードによるバックアップ取得手順の確認
問題を再現させる前に、まずは排他モードによるバックアップ取得手順を確認します。以下の実行例のような手順になります。
1. バックアップを開始する。
   ```terminal
   $ psql -c "SELECT pg_start_backup('test', true)"
   ```
2. DBデータのバックアップを取得する。
   ```terminal
   $ cp -a $PGDATA /backup/
   ```
3. バックアップを終了する。
   ```terminal
   $ psql -c "SELECT pg_stop_backup()"
   ```

## 排他モードによるサーバ起動失敗問題の再現
排他モードによる問題を再現するには、バックアップの開始から終了までの間に、WALファイルの切り替えとチェックポイントを複数回発生させるなどして、バックアップ開始時点のWALファイルが削除されるようにします。その状態で(バックアップ終了の前に)PostgreSQLを異常終了させ、PostgreSQLをクラッシュリカバリで起動させます。以下、実行例です。

1. バックアップを開始する。
   ```terminal
   $ psql -c "SELECT pg_start_backup('test', true)"
   ```
2. 1.のpg_start_backup()で作成されるbackup_labelファイルから、バックアップ開始時点のWALファイルを確認する。以下では、000000010000000000000002がバックアップ開始時のWALファイルと確認できる。
   ```terminal
   $ cat $PGDATA/backup_label
   START WAL LOCATION: 0/2000028 (file 000000010000000000000002)
   CHECKPOINT LOCATION: 0/2000060
   BACKUP METHOD: pg_start_backup
   BACKUP FROM: primary
   START TIME: 2022-12-05 15:02:31 JST
   LABEL: test
   START TIMELINE: 1
   ```
3. WALファイルの切り替えとチェックポイントを複数回実行する。
   ```terminal
   $ psql -c "SELECT pg_switch_wal(); CHECKPOINT"
   $ psql -c "SELECT pg_switch_wal(); CHECKPOINT"
   $ psql -c "SELECT pg_switch_wal(); CHECKPOINT"
   $ psql -c "SELECT pg_switch_wal(); CHECKPOINT"
   ```
4. バックアップ開始時のWALファイルが削除されていることを確認する。まだ削除されていない場合は3.に戻る。
   ```terminal
   $ ls $PGDATA/pg_wal
   000000010000000000000006 000000010000000000000007 archive_status
   ```
5. (バックアップを終了する前に)postmasterプロセスにSIGKILLを送信して、PostgreSQLを異常終了させる。
   ```terminal
   $ kill -SIGKILL $(head -1 data/postmaster.pid)
   ```
6. PostgreSQLを起動する。**クラッシュリカバリがエラーとなり、起動が失敗する。**
   ```terminal
   $ pg_ctl start
   ...
   LOG:  database system was interrupted; last known up at 2022-12-05 15:04:30 JST
   LOG:  invalid checkpoint record
   FATAL:  could not locate required checkpoint record
   HINT:  If you are restoring from a backup, touch "/System/Volumes/Data/opt/pgsql-15.1/data/recovery.signal" and add required recovery options.
	If you are not restoring from a backup, try removing the file "/System/Volumes/Data/opt/pgsql-15.1/data/backup_label".
	Be careful: removing "/System/Volumes/Data/opt/pgsql-15.1/data/backup_label" will result in a corrupt cluster if restoring from a backup.
   LOG:  startup process (PID 65731) exited with exit code 1
   LOG:  aborting startup due to startup process failure
   LOG:  database system is shut down
   ```

6.では、$PGDATA内にbackup_labelファイルが存在する状態でPostgreSQLを起動することになります。この状態では、backup_labelに記録されているバックアップ開始時点のWALファイル(上記では000000010000000000000002)からPostgreSQLはクラッシュリカバリを開始しようとしますが、そのWALファイルはチェックポイントにより削除済のため、リカバリを開始できず、起動は失敗します。

この起動失敗が排他モード特有の問題で、排他モードが非推奨とされてPostgreSQL15で削除された理由のひとつになります。一方で、非排他モードでは、バックアップ開始時にbackup_labelが$PGDATA配下に作成されないため、この問題は発生しません。

> If you are not restoring from a backup, try removing the file "/System/Volumes/Data/opt/pgsql-15.1/data/backup_label".

ちなみに、6.の状態から起動を成功させるには、起動失敗時のHINTメッセージ(上記抜粋)に従ってbackup_labelを削除します。これにより、PostgreSQLは(バックアップ開始時点ではなく)最新のチェックポイント時点のWALファイルからクラッシュリカバリを開始でき、起動に成功します。
```terminal
$ rm $PGDATA/backup_label
$ pg_ctl start
...
LOG:  database system is ready to accept connections
```

## さいごに
オンライン物理バックアップの排他モードは、今回確認したようなサーバ起動失敗のリスクがあることなどから、PostgreSQL15で削除されました。このため、PostgreSQL14以前で排他モードを使っていたユーザは、PostgreSQL15以降へのアップグレード時に非排他モードへの移行を検討する必要があります。

一方で、排他モードの方が便利なケースもあり、pg_exclusive_backupを使ってPostgreSQL15以降でも排他モードを使う場合は、サーバ起動失敗のリスクを十分に考慮した上で使いましょう。