PostgreSQLの障害対策 - Point In Time Recovery編 - | A Day In The Boy's Life

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

前回の「PostgreSQLの障害対策 - WAL編 - 」にて、WALでインスタンス障害への対応ができていることがわかりましたが、DB管理者から見ればより深刻なのはメディア障害の方です。


データファイルが壊れた、ディスク障害が発生してデータファイルの取出しが不可能になった、といった場合に先ほどのWALだけでは対応できません。

前回のバックアップファイルが残っていて、それ以降のトランザクション処理が全てWALセグメントファイルに含まれているのであれば復旧できるかもしれませんが、多くの場合はそのようなことはまれだと思います。


PostgreSQLのPITRは、Oracleのアーカイブログ運用と似ています。
バックアップ以降のWALセグメントファイルが残っていれば、それを適用させることで障害前の状態に復旧させることができます。


※ 障害前のどの状態まで戻せるかは、WALセグメントファイルに残っているトランザクション情報によります。


PITR機能を有効にするには、postgresql.confファイルに定義されている、archive_commandディレクティブを有効にし、適切なコマンドを指定する必要があります。


例えば、このように書きます。


archive_command = 'cp %p /var/log/arch/%f'

%pがWALセグメントファイルのフルパスが、%fはファイル名となり、上記のように書くと作成されたWALトランザクションファイルが/var/log/arch以下にも保存されるようになります。
Oracleのアーカイブログと違って、PostgreSQLの場合はWALトランザクションファイルをそのまま利用します。
設定ファイルを編集後は、PostgreSQLを再起動しておきます。


この設定が何を意味するのか確かめてみます。
わかりやすくするために、postgresql.confのarchive_timeoutディレクティブを下記のように編集し、PostgreSQLを再起動しておきます。


archive_timeout = 5

これは、指定の時間(秒)でWALトランザクションファイルを強制的に出力するためのオプションです。
(Oracleのように、ログスイッチを実行するためのSQLは存在しないようです)

まず、pg_xlogの中身を確認しておきます。


$ ls -o pg_xlog/
合計 16416
-rw-------  1 postgres 16777216  2月 17 13:30 000000010000000000000000
drwx------  2 postgres     4096  2月 17 13:30 archive_status

次に、適当にテーブルにデータを格納するなどのトランザクションを実行してみます。
すると、pg_xlogディレクトリとarchive_commandの中で指定したarchディレクトリの両方にログが出力されるようになっているのがわかります。


$ ls -o pg_xlog/
合計 65628
-rw-------  1 postgres 16777216  2月 17 13:35 000000010000000000000001
-rw-------  1 postgres 16777216  2月 17 13:35 000000010000000000000002
-rw-------  1 postgres 16777216  2月 17 13:35 000000010000000000000003
-rw-------  1 postgres 16777216  2月 17 13:36 000000010000000000000004
drwx------  2 postgres     4096  2月 17 13:36 archive_status

$ ls -o /var/log/arch/
合計 82032
-rw-------  1 postgres 16777216  2月 17 13:30 000000010000000000000000
-rw-------  1 postgres 16777216  2月 17 13:35 000000010000000000000001
-rw-------  1 postgres 16777216  2月 17 13:35 000000010000000000000002
-rw-------  1 postgres 16777216  2月 17 13:35 000000010000000000000003
-rw-------  1 postgres 16777216  2月 17 13:36 000000010000000000000004

そして、よく見ると「000000010000000000000000」というファイルは、archディレクトリにしか存在しません。
WALトランザクションファイルが指定の数を超えたので削除対象となり、削除を行う前にarchディレクトリに退避させていることがわかります。


つまり、このWALトランザクションファイルが、直近のトランザクションを記録したログ(Oracleで言うREDOログ)と、永続的に記録するためのログファイル(Oracleで言うアーカイブログ)の2つの意味を持たせていることになります。

ファイル名が同じ形式なため、どちらがどちらなのか区別が付かないのですが、archive_commandの中で指定した退避先がアーカイブログ、pg_xlogディレクトリにあるのがREDOログの役割となっています。


これで、インスタンス障害とメディア障害に対応できたログファイルを残すことができるようになりました。
次は、PITRを使ったオンラインバックアップの方法です。



Point In Time Recoveryを使うためのオンラインバックアップ


PITRでリカバリを行うためには、ある時点での物理的なバックアップファイルと、それを取得した以降のアーカイブログが必要となります。


物理的なバックアップとは、データファイルのコピーを指しますが、単にコピーしただけではリカバリ時に使えません。
データファイルは常に更新がかかっているので、何時の時点で取られたバックアップなのか明確にし、それとアーカイブログをつき合わせて、どの時点以降をリカバリする必要があるのかを記録させていなければなりません。


まずは、バックアップファイルを取得します。

データファイルをオンラインでバックアップするには先ほど述べたように、まずバックアップを取ることの宣言をする必要があります。


template1=# SELECT pg_start_backup('bakcup');
 pg_start_backup
-----------------
 0/458368
(1 row)

これで、バックアップの宣言が行われます。
pg_start_backup関数で指定しているbackupという文字は単なるラベルですので、何を指定してもかまいません。
この間に、データファイル一式のコピーを取得しておきます。


$ cp -R /usr/local/pgsql/data/ /home/postgres/pg_data_backup/

コピーが完了したら、バックアップの終了宣言を行います。


template1=# SELECT pg_stop_backup();
 pg_stop_backup
----------------
 0/4583CC
(1 row)

これで、データファイルのバックアップが完了しました。
Oracleでオンラインバックアップを取るときのBEGIN BACKUPとEND BACKUP宣言と同じようなイメージですね。

もう一度ログファイルを覗いてみると


$ ls -o pg_xlog/
合計 82036
-rw------- 1 postgres 16777216 2月 17 14:20 000000010000000000000009
-rw------- 1 postgres      239 2月 17 14:20 000000010000000000000009.00019D2C.backup
-rw------- 1 postgres 16777216 2月 17 13:40 00000001000000000000000A
-rw------- 1 postgres 16777216 2月 17 13:45 00000001000000000000000B
-rw------- 1 postgres 16777216 2月 17 13:50 00000001000000000000000C
-rw------- 1 postgres 16777216 2月 17 13:55 00000001000000000000000D

.backupというファイルができています。
この中身は、


START WAL LOCATION: 0/9019D2C (file 000000010000000000000009)
STOP WAL LOCATION: 0/9019D90 (file 000000010000000000000009)
CHECKPOINT LOCATION: 0/9019D2C
START TIME: 2009-02-17 14:20:08 JST
LABEL: bakcup
STOP TIME: 2009-02-17 14:20:51 JST

のように、バックアップの開始時間と終了時刻、WALやチェックポイントの情報が記載されいます。
バックアップの終了宣言を行うと、強制的にログスイッチが行われ、アーカイブログが出力されます。



Point In Time Recoveryを使ったリカバリ


いよいよ大詰めですが、PITRを使った簡単なリカバリのテストをしてみたいと思います。
PITRを使ったリカバリは、特定時刻のDBの状態に戻すこともできますし、WALトランザクションファイルに記録されている最新の状態まで戻すこともできます。


まず、最初のDBの状態です。


testdb=# select no, to_char(date, 'YYYY/MM/DD HH24:MI:SS') from test_tab;
 no |       to_char
----+---------------------
  1 | 2009/02/17 18:12:50

この時点で、先ほどやったようにオンラインバックアップを取得しておきます。

新たに1つレコードを追加してみます。


testdb=# select no, to_char(date, 'YYYY/MM/DD HH24:MI:SS') from test_tab;
 no |       to_char
----+---------------------
  1 | 2009/02/17 14:19:10
  2 | 2009/02/17 14:32:32

ここから障害を起こしますが、障害を発生させる直前のトランザクション(先ほど投入したnoが2のデータ)が含まれるWALトランザクションファイルもバックアップしておきます。
でないと、障害直前に戻すことができません。


$ ls -o pg_xlog/
合計 32824
-rw-------  1 postgres 16777216  2月 17 18:14 000000010000000000000000
-rw-------  1 postgres      236  2月 17 18:14 000000010000000000000000.0044C270.backup
-rw-------  1 postgres 16777216  2月 17 18:29 000000010000000000000001
drwx------  2 postgres     4096  2月 17 18:14 archive_status

バックアップ以降にできた、「000000010000000000000001」のトランザクションファイルを別ディレクトリにコピーしておきます。

ここではバックアップ以降のトランザクションファイルが明らかなために個別にバックアップしていますが、不明な場合はpg_xlog全体をバックアップしておくことで問題ありません。


$ cp 000000010000000000000001 /home/backup/pg_xlog/

ここで、擬似的な障害としてデータファイルを全て削除してみます。


$ rm -Rf data/

ついでにPostgreSQL関連のプロセスも強制終了しておきます。


ここからがリカバリです。

まず、データファイルのバックアップを元の位置に戻します。


$ cp -R ~/pg_data_backup/ ./data/

このバックアップしたデータファイルに含まれるWALトランザクションログは不要ですので削除し、先ほどバックアップしておいたWALトランザクションファイルをリストアします。
戻した後は、このようになっています。


$ ls -o pg_xlog/
合計 16416
-rw-------  1 postgres 16777216  2月 17 18:37 000000010000000000000001
drwx------  2 postgres     4096  2月 17 18:37 archive_status

次に、リストア用の設定ファイルを作成します。
テンプレートが、shareディレクトリ以下に含まれていますので、それをdataディレクトリコピーします。


$ cd /usr/local/pgsql/share/

$ cp recovery.conf.sample ../data/recovery.conf

適当なエディタでrecovery.confを開き、restore_commandディレクティブにアーカイブログのパスを指定して、pg_xlogへリストアするように指定ます。


restore_command = 'cp /var/log/arch/%f %p'

編集が完了した後は、PostgreSQLを起動します。

そして、DBの内容を確認してみると・・・


testdb=# select no, to_char(date, 'YYYY/MM/DD HH24:MI:SS') from test_tab;
 no |       to_char
----+---------------------
  1 | 2009/02/17 18:12:50
  2 | 2009/02/17 18:29:26

リストアされていることがわかります。


では、次にPITRの言葉が示すとおり、特定時間へのリストを試してみます。

先ほどのtest_tabテーブルへ、もう1レコード追加します。


testdb=# select no, to_char(date, 'YYYY/MM/DD HH24:MI:SS') from test_tab;
 no |       to_char
----+---------------------
  1 | 2009/02/17 18:12:50
  2 | 2009/02/17 18:29:26
  3 | 2009/02/17 18:52:37

先ほどと同様に擬似的な障害を発生させても良いですが、特定時間のDBの状態に戻す場合は、むしろ人為的なミスなどによる場合がおいでしょうから、今回は普通にDBを停止させて、18時30分の時点(noが2のデータ投入直後)に戻してみます。


まず、DBを正常に停止させ、先ほど同様にpg_xlog内にあるWALトランザクションファイルをバックアップしておきます。


$ pg_ctl stop

$ cp -R pg_xlog /home/backup/

次に、dataディレクトリを削除し、前回バックアップしておいたファイルをリストアします。


$ rm -Rf data/

$ cp -R ~/pg_data_backup/ ./data/

同様にrecovery.confをdataディレクトリに作成し、restore_commandディレクティブを編集しておきます。
ただ、前回のものに加え、recovery_target_timeディレクティブを下記のように指定し、どの時点まで戻すのかを明示しておきます。


recovery_target_time = '2009-02-17 18:30:00 JST'

上記では、カラムnoが2のデータを投入した直後の時間帯を指定しています。


これで準備が整いましたので、PostgreSQLを起動させます。
その後、テーブルを確認してみたところ・・・


testdb=# select no, to_char(date, 'YYYY/MM/DD HH24:MI:SS') from test_tab;
 no |       to_char
----+---------------------
  1 | 2009/02/17 18:12:50
  2 | 2009/02/17 18:29:26

意図どおりに18時30分時点のDBの状態にリストアされていることがわかります。
DB起動時のログからも、どの時点にリストアしたのかを確認することができます。


LOG:  database system was interrupted at 2009-02-17 18:13:37 JST
LOG:  starting archive recovery
LOG:  restore_command = "cp /var/log/arch/%f %p"
LOG:  recovery_target_time = 2009-02-17 18:30:00+09
LOG:  restored log file "000000010000000000000000.0044C270.backup" from archive
LOG:  restored log file "000000010000000000000000" from archive
LOG:  checkpoint record is at 0/44C270
LOG:  redo record is at 0/44C270; undo record is at 0/0; shutdown FALSE
LOG:  next transaction ID: 0/626; next OID: 24576
LOG:  next MultiXactId: 1; next MultiXactOffset: 0
LOG:  automatic recovery in progress


上記のように、18時30分時点まで戻そうとしているのがわかりますね。
また、その時点までに必要なアーカイブログを適用してリストアしているのもわかります。


このように、PostgreSQLでもWALとPITRの機能をうまく使えば、障害に強いDBを構築することができます。

WALトランザクションファイルやアーカイブログを冗長化することができない、アーカイブログの出力方法がシェルを使っており信頼性にかける(失敗したときの対処ができない)、などと幾つか弱点はあると感じましたが、そのような点をうまくカバーすれば大規模なDBにも利用できると思います。