PostgreSQL wal-g についての Tips

# はじめに
PostgreSQLでは標準で提供されるpg_basebackupの他に、OSSでさまざまなバックアップソリューションが存在します。今回はその中でも比較的多機能なwal-gについて、主にS3互換のクラウドストレージで利用する際のTipsを記載します。検証するうえで詰まったポイントがメインです。

wal-gの概要説明や基本的な使用方法などはWeb上に記事やマニュアルがありますので、ここでは記載を省略します。

# Tips

### 最新パッケージビルドについてのTips
基本的にUbuntu以外でそのまま利用可能なビルド版は2020年リリースのv0.2.19が最後となっています。
https://github.com/wal-g/wal-g/releases/tag/v0.2.19  
※ これ以降はUbuntu版のビルドパッケージのみ存在

そのため、それ以降のバージョンをRHEL系のOS等で利用するためには、ソースからのビルドが必要になります。基本的にはREADMEにある通りですが、いくつか注意点があります。

※ Go 1.22.9インストール済みのRocky Linux 9環境での実行です。

#### 圧縮系オプションの無効化について
オプションを無効化してmakeする場合は以下のようにします。

```bash
git clone https://github.com/wal-g/wal-g $(go env GOPATH)/src/github.com/wal-g/wal-g

cd $(go env GOPATH)/src/github.com/wal-g/wal-g

# 圧縮プロトコル系オプションの無効化
unset USE_BROTLI
unset USE_LIBSODIUM
unset USE_LZO

make deps
make pg_build
main/pg/wal-g --version
```

ポイントとなるのがオプションのunsetで、特にREADMEなどに記載はないのですが、例から推測してexport USE_BROTLI=0などでオプション抜きでビルドしようとすると、オプション込みでのビルドとなってしまい失敗するという事があります。

以下はMakefileからの抜粋ですが、ifdefで判断しているため環境変数の値がなんであれsetしてあればオプションは有効となってしまうためです。

```makefile
BUILD_TAGS:=

ifdef USE_BROTLI
        BUILD_TAGS:=$(BUILD_TAGS) brotli
endif

ifdef USE_LIBSODIUM
        BUILD_TAGS:=$(BUILD_TAGS) libsodium
endif

ifdef USE_LZO
        BUILD_TAGS:=$(BUILD_TAGS) lzo
endif
```
上述のようにunsetすればオプションは無効となりシンプルに導入できるため、特に圧縮オプションが必要なければunsetするのが良いと思われます。

#### 圧縮系オプションについてのtips

+ brotli
brotli-develを単純にインストールしただけではmakeが通りませんので、brotliもビルドしてldconfigでライブラリに反映させる必要がありました。手順としては以下になります。
```bash
wget -O brotli-1.0.9.tar.gz https://github.com/google/brotli/archive/refs/tags/v1.0.9.tar.gz
tar zxvf brotli-1.0.9.tar.gz
cd brotli-1.0.9
 
sudo yum install -y cmake make gcc
mkdir out && cd out
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF ..
make
 
# ldconfig でライブラリを反映させる
ls libbrotlidec-static.a libbrotlicommon-static.a libbrotlienc-static.a
sudo mkdir -p /usr/local/lib/brotli
sudo cp libbrotlidec-static.a libbrotlicommon-static.a libbrotlienc-static.a /usr/local/lib/brotli
sudo cp -r ../c/include/brotli /usr/local/include/
vi /etc/ld.so.conf.d/brotli.conf
===
/usr/local/lib/brotli
===
sudo ldconfig
sudo ldconfig -p | grep brotli
```
ちなみにbrotliはv1.1.0以上だと今回必要となる-staticがつくライブラリが生成されない問題があるようです。そのためv1.0.9を利用しました。 https://github.com/google/brotli/issues/1174
https://github.com/google/brotli/issues/1174
+ lzo
こちらはwal-e(wal-gの前のプロダクト)のバックアップ互換性のためのオプションとの記載がissueにあったため、明示的にunsetでも問題ないと判断しました。 

+ libsodium
こちらはlibsodium-devel等必要かと思われましたが、なくてもmakeはでき、libsodiumの利用も特にしていないためwal-gの利用には問題が出ませんでした。今回は特に利用しませんでしたのでこちらの詳細は未確認です。

各圧縮プロトコルとも特に利用しないようでしたら、前述の通りunsetするのが手っ取り早そうです。

### 利用についてのTips
#### Failed to get relfilenode for relation pg_roles(etc)
諸々の設定をしてbackup-pushすると以下のようなエラーが大量に発生します。
```bash
$ wal-g backup-push /postgres/pghome/data
INFO: 2024/11/12 10:35:01.450721 Backup will be pushed to storage: default
INFO: 2024/11/12 10:35:01.472109 Calling pg_start_backup()
INFO: 2024/11/12 10:35:01.473364 Initializing the PG alive checker (interval=1m0s)...
INFO: 2024/11/12 10:35:01.473407 Starting a new tar bundle
INFO: 2024/11/12 10:35:01.473425 Walking ...
INFO: 2024/11/12 10:35:01.473661 Starting part 1 ...
INFO: 2024/11/12 10:35:02.660082 Finished writing part 1.
：
INFO: 2024/11/12 10:35:37.692045 Finished writing part 10.
INFO: 2024/11/12 10:35:37.758529 Querying pg_database
WARNING: 2024/11/12 10:35:37.773333 Failed to get relfilenode for relation pg_roles: strconv.ParseUint: parsing "": invalid syntax★
WARNING: 2024/11/12 10:35:37.773366 Failed to get relfilenode for relation pg_shadow: strconv.ParseUint: parsing "": invalid syntax★
WARNING: 2024/11/12 10:35:37.773371 Failed to get relfilenode for relation pg_group: strconv.ParseUint: parsing "": invalid syntax★
WARNING: 2024/11/12 10:35:37.773374 Failed to get relfilenode for relation pg_user: strconv.ParseUint: parsing "": invalid syntax★
：
```
こちらはdebugモードで実行すると以下のようにメッセージが確認できます。
```
WARNING: 2024/11/12 11:06:26.289633 Failed to get relfilenode for relation pg_roles: strconv.ParseUint: parsing "": invalid syntax
DEBUG: 2024/11/12 11:06:26.289654 adding pg_roles as 12000 with filenode 0
```
これらはシステムビューなど実体のないファイルがエラーとなっているだけ（pg_relation_filepath()でNULLが返ってくる）で、実運用上は無視可能です。
```SQL
SELECT c.relfilenode, c.oid, pg_relation_filepath(c.oid), c.relname, pg_namespace.nspname FROM pg_class
-# AS c JOIN pg_namespace ON c.relnamespace = pg_namespace.oid
-# WHERE NOT EXISTS (SELECT 1 FROM pg_inherits AS i WHERE i.inhrelid = c.oid)
-# AND NOT EXISTS (SELECT 1 FROM pg_inherits AS i WHERE i.inhparent = c.oid)
-# AND c.relname = 'pg_roles';
 relfilenode │  oid  │ pg_relation_filepath │ relname  │  nspname
─────────────┼───────┼──────────────────────┼──────────┼────────────
           0 │ 12000 │ <NULL>               │ pg_roles │ pg_catalog
```
参考：該当部のコード（https://github.com/wal-g/wal-g/blob/5382e0ef9bbf5e421d09750bbd9beda5939989e8/internal/databases/postgres/query_runner.go#L58

#### wal-gが利用しているpg_backup_stopについて
他のバックアッププロダクトのbarman-cloud-backupなどでは、pg_backup_stop実行時にプライマリのアーカイブを待機する挙動となります。これはアーカイブWALにバックアップ中のWALを確実に含めるための挙動となります。

この挙動は実際は内部的に使用しているpg_backup_stopの第一引数の指定次第となりますが、wal-gではアーカイブを待たない（false指定）のため、バックアップの実行だけではアーカイブWALにバックアップ中のWALが確実に含まれることがないため注意が必要です。
```golang
func (queryRunner *PgQueryRunner) BuildStopBackup() (string, error) {
    switch {
    case queryRunner.Version >= 150000:
        return "SELECT labelfile, spcmapfile, lsn FROM pg_backup_stop(false)", nil★
    case queryRunner.Version >= 90600:
        return "SELECT labelfile, spcmapfile, lsn FROM pg_stop_backup(false)", nil
    case queryRunner.Version >= 90000:
        return "SELECT (pg_xlogfile_name_offset(lsn)).file_name," +
            " lpad((pg_xlogfile_name_offset(lsn)).file_offset::text, 8, '0') AS file_offset, lsn::text " +
            "FROM pg_stop_backup() lsn", nil
    case queryRunner.Version == 0:
        return "", NewNoPostgresVersionError()
    default:
        return "", NewUnsupportedPostgresVersionError(queryRunner.Version)
    }
}
```
wal-gではWALアーカイブもクラウドストレージに置くような構成が想定されていますが、今回はそのような構成での利用は考えていなかったため、バックアップ後に明示的にアーカイブし、そちらを別途バックアップするようにして対処しました。

#### s3互換ストレージバケット上のバックアップ単位について
wal-gではpg_backup_start()の戻り値（startlsn、バックアップ開始時点のlsn）からwalfilenameを割り出してS3ストレージ上のディレクトリを作成します。

また、PostgreSQL側の仕様としてプライマリでバックアップを実行する場合は必ずstartlsnが切り替わりますが、スタンバイ実行ではバックアップ契機では切り替わりません。
```SQL
-- primary
postgres=# select * from pg_backup_start('label',true);
 pg_backup_start
-----------------
 154/10000028
(1 row)
 
postgres=# select * from pg_backup_stop(false);
     lsn      |                            labelfile                             | spcmapfile
--------------+------------------------------------------------------------------+------------
 154/100032A0 | START WAL LOCATION: 154/10000028 (file 000000070000015400000002)+|
：
 
postgres=# select * from pg_backup_start('label',true);
 pg_backup_start
-----------------
 154/20000028★切り替わっている
(1 row)
 
postgres=# select * from pg_backup_stop(false);
     lsn      |                            labelfile                             | spcmapfile
--------------+------------------------------------------------------------------+------------
 154/20003188 | START WAL LOCATION: 154/20000028 (file 000000070000015400000004)+|
：

-- standby
postgres=# select * from pg_backup_start('label',true);
 pg_backup_start
-----------------
 154/30000028
(1 row)
 
postgres=# select * from pg_backup_stop(false);
     lsn      |                            labelfile                             | spcmapfile
--------------+------------------------------------------------------------------+------------
 154/380009C8 | START WAL LOCATION: 154/30000028 (file 000000070000015400000006)+|
：
 
postgres=# select * from pg_backup_start('label',true);
 pg_backup_start
-----------------
 154/30000028★切り替わっていない
(1 row)
 
postgres=# select * from pg_backup_stop(false);
     lsn      |                            labelfile                             | spcmapfile
--------------+------------------------------------------------------------------+------------
 154/38001100 | START WAL LOCATION: 154/30000028 (file 000000070000015400000006)+|
：
```
今回の構成では負荷を考慮してスタンバイからのバックアップ取得を考えていました。しかし、この仕様では負荷の低いDBなどでWALスイッチがなされないままバックアップが連続で取得されると、同じディレクトリにバックアップが配置されてしまいます。
```bash
-- primary で何もしなかった場合(同じディレクトリにまとめられる)
INFO: 2024/11/18 18:10:56.131020 Wrote backup with name base_000000070000015400000014 to storage default
INFO: 2024/11/18 18:12:07.690392 Wrote backup with name base_000000070000015400000014 to storage default
INFO: 2024/11/18 18:13:36.602197 Wrote backup with name base_000000070000015400000014 to storage default
```
こちらの問題に対処するため、バックアップ終了後はプライマリ側でWALスイッチ+チェックポイントを必ず実行するようにしました。
```
-- primary で wal switch => checkpoint した場合(違うディレクトリとなる)
INFO: 2024/11/18 18:06:03.915728 Wrote backup with name base_000000070000015400000012 to storage default
INFO: 2024/11/18 18:06:42.447829 Wrote backup with name base_000000070000015400000013 to storage default
INFO: 2024/11/18 18:07:33.365485 Wrote backup with name base_000000070000015400000014 to storage default
```

# まとめ
wal-gについて実際に利用する上で詰まったポイントを主軸にTipsを記載しました。

wal-gはパラレルリストアによる高速なリカバリなど多機能な点が魅力です。オンプレやVMでのPostgreSQLでクラウドストレージにバックアップを配置したい場合、選択肢として検討してはいかがでしょうか。