Marlock Homes Diary

備忘録。忘れないように書きます。

PostgreSQLのトランザクションID周回問題を強制的に発生させる

こんにちは!
PostgreSQLトランザクションID(XID)の周回により、
以前は見ていた古いデータが見えなくなる事象(XID周回問題)を強制的に発生させたいと思います。

通常は自動Vacuumが古いXIDの整理を自動実行している、かつ
XID周回問題が発生する前に以下のようなメッセージが出力され、
XID周回問題(データが見えなくなる)は発生しません。

XID周回問題の発生まで、残り1,000万トランザクションで以下の警告メッセージが出力されます。

WARNING:  database "mydb" must be vacuumed within 177009986 transactions
HINT:  To avoid a database shutdown, execute a database-wide VACUUM in "mydb".

残り100万トランザクションでエラーになります。(XID周回問題の防止)
エラーが発生した場合には、メッセージのHINTに出力されている通り、
シングルユーザモードでVACUUM(FREEZE)を実行し、復旧させる必要があます。

ERROR:  database is not accepting commands to avoid wraparound data loss in database "mydb"
HINT:  Stop the postmaster and vacuum that database in single-user mode.

参考にした情報
https://www.postgresql.jp/document/9.6/html/routine-vacuuming.html
https://www.slideshare.net/iakio/jpug-ezo20110809

今回はXID周回が発生した場合の動作を確認するために、XID周回を強制的に発生させました。
※ データが見えなくなりますので、壊れてよいデータベースクラスタを用意して実行しています。

0. 環境情報

# cat /etc/redhat-release
Red Hat Enterprise Linux Server release 7.3 (Maipo)
$ psql -c "SELECT version()"
                                                 version
---------------------------------------------------------------------------------------------------------
 PostgreSQL 10.0 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-11), 64-bit
(1 row)

1. テスト用テーブル作成

$ psql
=# CREATE TABLE test(id int);
=# INSERT INTO test VALUES (1);
=# INSERT INTO test VALUES (2);
=# INSERT INTO test VALUES (3);
=# SELECT xmin,xmax,id FROM test;
 xmin | xmax | id
------+------+----
  556 |    0 |  1
  557 |    0 |  2
  558 |    0 |  3
(3 rows)

2. データベース停止

$ pg_ctl stop
waiting for server to shut down.... done
server stopped

3. xidを2の31乗進める

$ pg_resetwal -x 0x80000000 data
Write-ahead log reset

4. シングルユーザモードで起動

$ postgres --single postgres
PostgreSQL stand-alone backend 10.0

5. frozenxidを書き換える

=# UPDATE pg_database SET datfrozenxid = '2147483648' -- 2の31乗
=# UPDATE pg_class SET relfrozenxid= '2147483648' -- 2の31乗
=# SELECT datname,datfrozenxid from pg_database; -- datfrozenxidが"2147483648"
=# SELECT relname,relfrozenxid from pg_class; -- relfrozenxidが"2147483648"
Ctrl + D

6. データベース起動

$ pg_ctl start

7. XIDを消費

for i in `seq 0 545`
do
  psql -c "SELECT txid_current();"
done

7. 確認

$ psql
=# SELECT txid_current();
 txid_current
--------------
   2147484201
(1 row)


=# SELECT xmin,xmax,id FROM test;
 xmin | xmax | id
------+------+----
  556 |    0 |  1
  557 |    0 |  2
  558 |    0 |  3
(3 rows)

※ 上記の2SQLを繰り返すると、xmin=556のデータから見えなくなる
 xmin | xmax | id
------+------+----
(0 rows)

6. VACUUM FREEZEの実行

=# VACUUM FREEZE;
=# SELECT xmin,xmax,id FROM test;
ERROR:  catalog is missing 1 attribute(s) for relid 16384
LINE 1: SELECT xmin,xmax,id FROM test;
=# SELECT 
    * 
  FROM 
    pg_attribute
  WHERE 
    attrelid = ( SELECT oid FROM pg_class WHERE relname = 'test' );
(0 rows)

システムカタログpg_attributeのデータが見えなくなってしまったようです。
何らかの特殊要因(システムカタログを書き換えたなど(ないと思いますが))で、周回問題が発生しうるか否かを確認する際には、
frozenidと該当テーブルのxminの値を比較してみるとよいかもしれません。

以上です。