レプリケーション構成のスタンバイ側で、以前の記事では、「バッファ・ピンによるリカバリ競合」と「テーブルロック」によりデッドロックを発生させました。
今回は、「テーブルスペース削除によるリカバリ競合」と「テーブルロック」によりデッドロックを発生させます。このデッドロックは、以下2つの待ちが重なることで発生します。
- バックエンドプロセスが利用中のテーブルスペースを、startupプロセスが削除しようとして待ち状態になる
- startupプロセスが取得済のテーブルロックを、バックエンドプロセスが取得しようとして待ち状態になる
デッドロックを発生させる手順
レプリケーション構成のプライマリ側をセットアップする。
$ initdb -D data --no-locale --encoding=UTF8
$ pg_ctl -D data start
テーブルスペースを作成する。
$ mkdir /tmp/hoge1
$ psql -c "CREATE TABLESPACE hoge LOCATION '/tmp/hoge1'"
レプリケーション構成のスタンバイ側をセットアップする。
$ pg_basebackup -D sby1 -R -T /tmp/hoge1=/tmp/hoge2 -X fetch
$ echo "port = 5433" >> sby1/postgresql.conf
$ echo "temp_tablespaces = 'hoge'" >> sby1/postgresql.conf
$ echo "max_standby_streaming_delay = -1" >> sby1/postgresql.conf
$ pg_ctl -D sby1 start
- 今回は同一ホスト上でプライマリとスタンバイを動かすため、pg_basebackup実行時に-Tオプションでテーブルスペースのディレクトリの対応を指定する。また、プライマリと異なるポート番号をスタンバイ側で設定する。
- テーブルスペース削除によるリカバリ競合を引き起こすために、
で作成したテーブルスペースをtemp_tablespacesに設定する。
- リカバリと競合するリードオンリー・トランザクションがすぐにキャンセルされないように(すぐにキャンセルされるとデッドロックを観察できないため)、max_standby_streaming_delayを-1に設定する。
プライマリ側で、テーブルを作成する。また、そのテーブルに対してACCESS EXCLUSIVEロックを取得して、ロック取得のWALがすぐにスタンバイに転送されるようにpg_switch_wal()を実行する。
CREATE TABLE t();
BEGIN;
LOCK TABLE t IN ACCESS EXCLUSIVE MODE;
SELECT pg_switch_wal();
- この手順により、スタンバイ側では、startupプロセスはテーブルに対するACCESS EXCLUSIVEロックを取得する。
スタンバイ側で、ORDER BYのディスクソートにより、temp_tablespacesに設定したテーブルスペース上で一時ファイルを発生させる。また、テーブルをSELECTする。
BEGIN;
SET work_mem TO 64;
DECLARE mycur CURSOR FOR SELECT * FROM generate_series(1, 1000000) n ORDER BY n;
FETCH mycur;
SELECT * FROM t;
- ディスクソートが発生しやすいように、ORDER BY検索の実行前にwork_memを小さい値に設定しておく。
- 一時ファイルが残ったままになるように、カーソルを使ってORDER BY検索が途中状態のままになるようにする。
- テーブルのSELECTにより、バックエンドプロセスはテーブルに対するACCESS SHAREロックを取得しようとして待ち状態になる。
プライマリ側で、
とは別のセッションを開始して、テーブルスペースを削除する。
DROP TABLESPACE hoge;
- この手順により、startupプロセスはスタンバイ側でテーブルスペースを削除しようとするが、バックエンドプロセスがまだそのテーブルスペースを使っている(一時ファイルが残っている)ため、テーブルスペース削除によるリカバリ競合となり、待ち状態になる。
バックエンドプロセスもstartupプロセスも待ち状態のため、デッドロックとなる。
2024年1月28日現在のPostgreSQLのコードでは、このデッドロックは検出されず自動的な解決もされない。(ように見えるが、嘘だったら申し訳ない。。)