PostgreSQL 17がやってくる(4) 増分バックアップ- 基本編

# はじめに
にゃーん

今回は、PostgreSQL 17develの「増分バックアップ」機能を簡単に調べてみた。

# 増分バックアップ
増分バックアップ(incremental Backup)は、前回行われたバックアップ（フルバックアップまたは増分バックアップ）から変更・追加のあった箇所のみをバックアップする機能である
PostgreSQL本体が提供しているオンラインバックアップコマンド(``pg_basebackup``)では、これまでフルバックアップのみ対応していた。しかし、データベースクラスタサイズが非常に大きい場合に、毎回フルバックアップを取得するのは時間もかかるし、世代管理しているなら世代数✕データベースクラスタサイズ分の容量をバックアップ先に確保しなければならず、正直なかなかしんどいものがあった。

## 増分バックアップ対応のツール
PostgreSQLには、従来から増分バックアップに対応したツールとして、[pg_rman](https://ossc-db.github.io/pg_rman/index-ja.html)がある。これを使えばPostgreSQL 16までのバージョンでも、増分バックアップは可能である。
ただ、残念ながらpg_rmanは公式にはWindows版環境では動作しない。
（また、pgBackrestやbarmanといった他のバックアップツールもWindows OSには正式対応はしていない）

今回、PostgreSQL本体機能として増分バックアップ機能が対応することで、Windows版PostgreSQLでも増分バックアップが利用可能になる。

# PostgreSQL 17で追加された増分バックアップ機能
## 追加された機能
ドキュメントをざった見た感じだと、以下の機能が追加されたようだ。

* ``pg_basebackup``コマンドに増分バックアップ用オプション``--incremental``が追加された。
* ``pg_combinebackup``コマンドが新規に追加された。このコマンドは増分バックアップからフルバックアップを再構築コマンドっぽい。
* 増分バックアップに関連するPostgreSQLパラメータの追加
  * summarize_wal
  * wal_summary_keep_time
* 増分バックアップに関連する待機イベントの追加

## 増分バックアップとリストアの大まかな手順
PostgreSQL文書の増分バックアップに関連する箇所（[Making an Incremental Backup](https://www.postgresql.org/docs/devel/continuous-archiving.html#BACKUP-INCREMENTAL-BACKUP)）をざっと見た感じでは、以下のような手順で増分バックアップを行うようだ。

1. ``pg_basebackup``コマンドでフルバックアップを取得する。
1. ``pg_basebackup``コマンドに``--incremental``オプションを付与して増分バックアップを取得する。
``--incremental``には以前のバックアップのマニフェストファイル(``backup_manifest``)を指定する。
``summarize_wal``パラメータが``off``になっているとエラーになる。[^1]
というエラーになる。
1. リカバリ時には、フルバックアップを取得したバックアップをリストアした後、``pg_combinebackup``コマンドを用いて、フルバックアップ→増分バックアップで取得したバックアップを順に指定して再構築する。
1. 再構築した後のデータベースクラスタを``pg_verifybackup``コマンドで検査する。
この手順は必須ではないが、実運用時にはたぶんやっておいたほうがいい。
1. 再構築後に、recovery.confを編集してリカバリモードでPostgreSQLサーバを再起動する。

# 動作検証
## 検証環境
### PostgreSQLバージョン
PostgreSQL 17develを用いて、増分バックアップおよび、増分バックアップを用いたリカバリを試してみる。
今回の検証で使用するPostgreSQLバージョンは2024-04-27にコミットされた(commit e00b4f79e7367272b436dea6f15d6f421f3d6e1a)を用いる。

### OS等

* 仮想マシン：AWS EC2(t2.medium)
* OS：Amazon Linux 2.3

## 格納領域
領域を以下のように設定する。

|領域|パス|備考|
|:--|:--|:--|
|データベースクラスタ|/data/pgdata/17||
|WAL領域|/data/pgdata/17_pg_wal|initdb時に-Xオプションで指定|
|アーカイブ領域|/data/pgdata/17_archive|archive_commandで指定するアーカイブ先|
|バックアップ格納先|/data/pgdata/backup|このディレクトリ下にフルバックアップ・増分バックアップを保存|

## PostgreSQL設定

``initdb``時に``-c``オプションで以下を設定したデータベースクラスタを作成する。

|パラメータ名|設定値|備考|
|:--|:--|:--|
|archive_mode|on||
|archive_command|cp %p /data/pgdata/pg17_archive/%f||
|summarize_wal|on|デフォルトはoff|

## 検証シナリオ

1. pgbenchを用いてScale Factor=100(約1.5GB)のデータを初期ロードする。
1. フルバックアップ：
初期ロード直後の状態でpg_basebackupを用いてベースバックアップ（フルバックアップ）を取得する。
1. pgbench実行1回目：
デフォルトトランザクションを1000トランザクション実行する（-c 2 -t 500）
1. 増分バックアップ1回目：
pg_basebackupを用いてベースバックアップ（増分バックアップ）を取得する。
1. pgbench実行2回目：
デフォルトトランザクションを1000トランザクション再び実行する（-c 2 -t 500）
1. pgbench実行3回目：
デフォルトトランザクションを1000トランザクション再び実行する（-c 2 -t 500）
1. 増分バックアップ2回目：
pg_basebackupを用いてベースバックアップ（増分バックアップ）を取得する。
1. pgbench実行4回目：
デフォルトトランザクションを1000トランザクション再び実行する（-c 2 -t 500）

この状態で、データベースクラスタを削除して、最新状態へのリカバリを行う。
最新状態へリカバリした場合、4000トランザクションが実行された状態に復旧する。

## データロード～増分バックアップ取得
少々長くなるが、データロード→フルバックアップ→トランザクション実行→増分バックアップ→トランザクション実行の一連の処理を実行したログを以下に示す。

まず、pgbenchをスケールファクタ=100で初期化する。
初期化後に、abalanceのSUM値とmtimeの最新値を取得する。
```bash
$ ./pgbench_and_backup.sh
start: Mon Apr 29 10:56:05 JST 2024
（中略）
SELECT sum(abalance) FROM pgbench_accounts
 sum
-----
   0
(1 row)

Null display is "(null)".
SELECT * FROM pgbench_history ORDER BY mtime DESC LIMIT 1
 tid | bid | aid | delta | mtime | filler
-----+-----+-----+-------+-------+--------
(0 rows)
```

この状態で``pg_basebackupコマンドによるフルバックアップを取得する。
取得時のコマンドラインは
```bash
pg_basebackup --checkpoint=fast --progress -D /data/pgdata/backup/full
```
である。
実行時のログはこんな感じ。
```bash
1562773/1562773 kB (100%), 1/1 tablespace
```
1回目のpgbenchを実行する。pgbenchのコマンドラインは以下。
```bash
pgbench bench -c 2 -t 500
```

実行ログは以下。
```bash
pgbench (17devel)
（中略）
tps = 1121.934035 (without initial connection time)
```

この状態で増分バックアップを取得する。
コマンドラインは以下。
```bash
pg_basebackup --incremental=/data/pgdata/backup/full/backup_manifest --checkpoint=fast --progress -D /data/pgdata/backup/inc1
```
注目すべきポイントは``--incremental``オプションに、フルバックアップ時に取得したバックアップディレクトリ内のマニフェストファイルを指定しているとこと、``-D``オプションで増分バックアップの取得先を指定しているとこ。

実行ログは以下。
```bash
backup inc1
  21442/1562971 kB (1%), 1/1 tablespace
```
フルバックアップのときとprogress表示が少し異なり、分子にあたる数字が小さい値になっている。
つまり、``pg_basebackup``の背景で一部のファイルのみをバックアップしているのがわかる。

増分バックアップ（1回目）取得後に、ふたたびpgbenchでトランザクションを1000回実行✕2回する。
```bash
pgbench (17devel)
（略）
tps = 1035.582619 (without initial connection time)
pgbench (17devel)
（略）
tps = 984.270375 (without initial connection time)
```

ここで2回目の増分バックアップを取得する。
このときのコマンドラインは以下。
```bash
pg_basebackup --incremental=/data/pgdata/backup/inc1/backup_manifest --checkpoint=fast --progress -D /data/pgdata/backup/inc2
```
注目すべきポイントは``--incremental``オプションに、1回目の増分バックアップ時に取得したバックアップディレクトリ内のマニフェストファイルを指定しているとこと、``-D``オプションで2回目用の増分バックアップの取得先を指定しているとこ。
つまり、このPostgreSQL 17の増分バックアップ機能を使った世代管理バックアップツールを作る場合には、最新の増分バックアップディレクトリがどこにあるのかを管理する必要がある、ということだ。

増分バックアップ2回目の実行ログは以下。
```bash
backup inc2
  37144/1563267 kB (2%), 1/1 tablespace
```
1回目の増分バックアップよりも影響しているブロックが多いためか、1回目よりもちょい大きな値になっている。

更にもう1回pgbenchで1000トランザクションを実行する。
```bash
pgbench (17devel)
（中略）
tps = 955.560694 (without initial connection time)
```

あとでリカバリした後のデータのチェック用に、この状態で、abalanceのSUM値とmtimeの最新の値を控えておく。
```sql
SELECT sum(abalance) FROM pgbench_accounts
   sum
---------
 -154480
(1 row)

SELECT * FROM pgbench_history ORDER BY mtime DESC LIMIT 1
 tid | bid |   aid   | delta |           mtime            | filler
-----+-----+---------+-------+----------------------------+--------
 382 |  87 | 3293274 | -4969 | 2024-04-29 10:57:45.692636 | (null)
(1 row)
```

やっと準備は終わった。

さて、今回フルバックアップを1回、増分バックアップを2回取得した。
それぞれの取得サイズは如何程か。
```bash
==== backup directory size ===
1578788 /data/pgdata/backup/full
40532   /data/pgdata/backup/inc1
56224   /data/pgdata/backup/inc2
```
pgbenchでスケールファクタ=100のデータをロードするとだいたいデータベースクラスタのサイズは1.5GB程度になる。
フルバックアップを3回取得した場合、約4.5GBのバックアップ領域が必要になるが、増分バックアップを使った場合にはサイズをかなり小さくすることができた。

:::note info
今回は増分バックアップまでの区間のトランザクション数が少ないので、影響を受けるブロック数が少ない＝増分バックアップのサイズが小さい。データベースファイルほぼ全体が更新されるような状態で増分バックアップをとったら、フルバックアップとあまり変わらないサイズになるかもしれない。
:::

## 最新状態へのリカバリ
最新状態へリカバリするためには、上記の手順で取得した3つのバックアップ(full, inc1, inc2)をマージしたものと、それ以降のアーカイブログ、そして最新のWALファイルが必要になる。

### バックアップファイルのマージ
フルバックアップと増分バックアップをマージするために、``pg_combinebackup``コマンドを使う。
このコマンドは`-o`オプションでマージした出力先を指定し、入力元となるフルバクアップ、増分バックアップのディレクトリ名を**時系列順に指定する。**

``pg_combainbackup``コマンドにはチェックだけ行って、実際にマージを行わないオプション``--dry-run``がある。実際にマージ処理をする前に、``--dry-run``オプションをつけてエラーが起きないことを確認してから、実際のデータベースクラスタのマージを行うのが安全だ。

実際に``pg_combainbackup``コマンドを使ってみる。
まず、dry-runモードで実行する。

```bash
$ pg_combinebackup -o /data/pgdata/17 /data/pgdata/backup/full /data/pgdata/backup/inc1 /data/pgdata/backup/inc2
$ 
```
何もエラーがでないのでマージをしても大丈夫そうだ。今度は``--dry-run``オプションを外して実行する。

```bash
$ pg_combinebackup -o /data/pgdata/17 /data/pgdata/backup/full /data/pgdata/backup/inc1 /data/p
gdata/backup/inc2
$ 
```
``pg_combainbackup``コマンドは寡黙なコマンドなので、何も異常がなければ何もメッセージを出力することなく終了する。コマンド実行中にどういったファイルがコピーされたのかは、``--debug``オプションをつけると表示されるが、非常に出力されるメッセージの量が多いので通常運用時には使わなくてもいい気がする。
なお、``pg_combainbackup``コマンドが正常に終了した場合には0が、何らかの異常があった場合には0でない値（例えば1）が環境に返却される。具体的にどんなケースでどんな値が返却されるのかは、現状のPostgreSQL文書には記載はないが・・・

### マージされたデータベースクラスタの検査
マージされたデータベースクラスタは、従来の``pg_verifybackup``コマンドで検査できる。
さきほどマージされたデータベースクラスタを検査する。

```bash
$ pg_verifybackup --progress /data/pgdata/17
1562616/1562616 kB (100%) verified
backup successfully verified
$
```
検査した結果、マージされたデータベースクラスタは大丈夫そうだ。

### リカバリ
このマージしたデータベースクラスタを元に、アーカイブファイルとWALを用いて最新状態へ復旧する。
このあたりの手順は従来のリカバリと同じである。

マージしたデータベースクラスタ内に``pg_wal``ディレクトリが存在するが、これはリカバリ時には不要なので削除する。そして、WALを保存しているディレクトリへのシンボリックリンクを設定する。

```bash
$ cd /data/pgdata/17
$ rm -fr pg_wal
$ ln -s /data/pgdata/17_pgwal pg_wal
$ ls -l pg_wal
$  ls -l pg_wal
lrwxrwxrwx 1 ec2-user ec2-user 23 Apr 29 14:35 pg_wal -> /data/pgdata/17_pg_wal/
$ 
```
リカバリ時にアーカイブファイルをコピーする``restore_command``をpostgresql.confに追記する。
そして、リカバリ起動用のフラグファイル``recovery.signal``をデータベースクラスタ内にtouchコマンド等で作成する。

```bash
$ echo "restore_command = 'cp /data/pgdata/17_archive/%f %p'" >> postgresql.conf
$ touch recovery.signal
$
```

PostgreSQLをリカバリモードで起動する。

```bash
$ pg_ctl start -D /data/pgdata/17 -l /tmp/pg17-rec.log
waiting for server to start.... done
server started
$
```

### 最新状態の値の確認
PostgreSQLのリカバリが終わり、起動できたので、psqlコマンドで``pgbench_accounts``テーブルの``abalance``列の値と、``pgbench_history``のmtime列の最新値を検索する。

```sql
$ psql -p 17001 bench -e -c "SELECT sum(abalance) FROM pgbench_accounts"
Null display is "(null)".
SELECT sum(abalance) FROM pgbench_accounts
   sum
---------
 -154480
(1 row)

$  psql -p 17001 bench -e -c "SELECT * FROM pgbench_history ORDER BY mtime DESC LIMIT 1"
Null display is "(null)".
SELECT * FROM pgbench_history ORDER BY mtime DESC LIMIT 1
 tid | bid |   aid   | delta |           mtime            | filler
-----+-----+---------+-------+----------------------------+--------
 382 |  87 | 3293274 | -4969 | 2024-04-29 10:57:45.692636 | (null)
(1 row)

$
```
最新状態に復元できていることが確認できた。めでたし、めでたし。

今回は最新状態へのリカバリのみを確認したが、時刻指定のリカバリの場合もほぼ同様の手順となる。
違いは、たぶん以下の2点くらいだと思う。
* 指定した時刻より前の増分バックアップまで指定して、``pg_combinebackup``を実行する。
* マージしたあとのデータベースクラスタの設定ファイルに``recovery_target_time``等を設定してリカバリする。

# おわりに
今回はPostgreSQL 17の目玉機能（と個人的に思っている）増分バックアップについて簡単に検証してみた。
記事の上の方に書いているけど、この機能が正式版で使えるようになれば、**Windows版PostgreSQLでも増分バックアップが使える**というのが何気に大きいと思う。
pg_rman等、既存のバックアップツールがこの機能にどう対応するのかも注目だ。

[^1]: 以下のようなエラーメッセージが出力してバックアップを中止する。
``ERROR:  incremental backups cannot be taken unless WAL summarization is enabled``