スキップしてメイン コンテンツに移動

COMMITについて少し考えてみた(3)

前回、Oracleのトランザクションガードについて見てみました。
せっかくなので、というか単純に興味があったのでPostgreSQLのCOMMITの動きやOracleと同じく障害発生のタイミングによりトランザクションが行方不明になった時の対処方法を見てみたいと思います。

PostgreSQLのCOMMITの様子

とてもざっくりですが、PostgreSQLのCOMMITの様子を見てみます。

PostgreSQL 11のcommitの動き
  1. psqlなどクライアントは"COMMIT"という文字列をsendto(2)でbackendのソケットに書き込みます
  2. クライアントからのI/Oイベントにより起床したbackendは以下の処理を行います
    1. recvfrom(2)で"COMMIT"を受け取る
    2. write(2)でWALファイルにlog bufferの内容を書き出す
  3. write(2)でWALに書き出した後backendは以下の処理を行います
    1. fdatasync(2)でファイルシステムのキャッシュにあるデータを物理デバイスと同期させる
    2. sendto(2)によりクライアントにCOMMIT完了のメッセージを送信
  4. recvfrom(2)でメッセージを受け取ったクライアントは最終的にCOMMIT完了の処理を実施(psqlの場合はコンソールに"COMMIT"を書き出す)

余談

ちなみに、PostgreSQLにwalwriterというプロセスがいますが"COMMIT"に限定すると通常のwalwriterはその名前から想像できるようなWALファイルへの書き込みを実行しません。walwriterは5秒おき(*)にlog bufferの内容をコミットの有無に関係なくWALファイルに書き込みを実施しますがCOMMIT時の同期書き込みはbackendが実施しています。しかし、トランザクションが非同期コミットとして設定される場合(synchronous_commit=offの場合)では、backendに送られたCOMMITをトリガーにCOMMIT自体のWAL recordがwalwriterにより非同期、バッチ的にWALファイルに書き込まれます。
  • 5秒(wal_writer_delay(200ms) x #HIBERNATE_FACTOR(25))もしくは状況によっては200ms(wal_writer_delay)おき
非同期コミット(synchronous_commit=off)の場合の動作は以下のようになります。

PostgreSQL 11の非同期commitの動き
  1. psqlなどクライアントは"COMMIT"という文字列をsendto(2)でbackendのソケットに書き込みます
  2. クライアントからのI/Oイベントにより起床したbackendは以下の処理を行います
    1. recvfrom(2)で"COMMIT"を受け取る
    2. kill(2)でwal writerに対してシグナルを送信
    3. sendto(2)によりクライアントにCOMMIT完了のメッセージを送信
  3. recvfrom(2)でメッセージを受け取ったクライアントは最終的にCOMMIT完了の処理を実施(psqlの場合はコンソールに"COMMIT"を書き出す)
実際のWALファイルへの書き込みは上記のbackendの動作とは非同期に行われることになります。
  1. 上記の2-1でシグナルを受けて起床したwal writerはwrite(2)でWALファイルにlog bufferの内容を書き出す
  2. fdatasync(2)でファイルシステムのキャッシュにあるデータを物理デバイスと同期させる
さらにポイントは本当にWALファイルに書き込まれたかどうか通常の同期commitと違いクライアントに通知されません。そういう意味でトランザクションがCOMMITにより本当に永続化されたかどうかを待機するよりパフォーマンスを優先する場合に使える設定となることが理解できます。

COMMITの途中のどこで障害が発生するかにより結果は異なる

それでは話を戻して、以前のOracleの時と同様に障害でbackendが異常終了する場合、異常が発生したタイミングがWALファイルへの書き込みの前(①)と後(④)でトランザクションの完了か否かが変わります。WALファイル書き込み完了前後の障害でクライアント(psql)はどのようなエラーとなるか確認してみましょう。

WALファイルへの書き込み完了前で障害が発生した場合

1) INSERTを実行(AUTOCOMMITはoffに設定)
 postgres=# select * from sample_table;
2) backendのrecvfrom(2)をブロック(上記シーケンス図の①の部分)
 postgres=# select pg_backend_pid();
  pg_backend_pid
 ----------------
           15795
 (1 行)
 $ gdb -p 15795
 ...
 (gdb) catch syscall recvfrom
 Catchpoint 1 (syscall 'recvfrom' [45])
 (gdb) c
 Continuing.
3) COMMITを実行
 postgres=# commit;
上記の状態でCOMMITが完了することはありません
4) この状態でbackendの障害を発生(kill)させてみます
 $ kill -9 15795
また、2)のgdbのセッションをキャンセルします
5) psqlにエラーが返ります
 postgres=# commit;
 サーバとの接続が想定外にクローズされました
         おそらく要求の処理前または処理中にサーバが異常終了
         したことを意味しています。
 サーバへの接続が失われました。リセットしています: 成功。
6) 再接続してデータが存在しないことを確認します
 postgres=# select * from sample_table;
  id
 ----
 (0 行)

今度はWALファイルへの書き込み完了後で障害が発生した場合

1) INSERTを実行(AUTOCOMMITはoffに設定)
 postgres=# insert into sample_table values (1);
2) backendのsendto(2)をブロック(上記シーケンス図の④の直前部分)
 postgres=# select pg_backend_pid();
  pg_backend_pid
 ----------------
           16293
 (1 行)
 $ gdb -p 16293
 ...
 (gdb) catch syscall sendto
 Catchpoint 1 (syscall 'sendto' [44])
 (gdb) c
 Continuing.
3) COMMITを実行
 postgres=# commit;
上記の状態でCOMMITが完了することはありません
4) この状態でbackendの障害を発生(kill)させてみます
 $ kill -9 16293
また、2)のgdbのセッションをキャンセルします
5) psqlにエラーが返ります
 postgres=# commit;
 サーバとの接続が想定外にクローズされました
         おそらく要求の処理前または処理中にサーバが異常終了
         したことを意味しています。
 サーバへの接続が失われました。リセットしています: 成功。
6) 再接続後にデータが存在することを確認します
 postgres=# select * from sample_table;
  id
 ----
   1
 (1 行)

COMMITの結果を確実に確認するには

今回のシミュレーションでは、Oracleの時と同様にクライアントはCOMMITを実行後、COMMITが失敗しますが本当にトランザクションが永続化されているかどうかを知ることができていません。単純にトランザクションの再実行ではデータの二重登録(論理破壊)が発生する可能性があります。Oracleでこのような状況に対処するにはトランザクションガードが利用できると前回書きましたが、PostgreSQL 10以降であればtxid_status()が利用できます。txid_status()に関しては次回見てみることにします。
また、今回もbackendの障害という極端な例となっていますが、backendからクライアントへの通信が途切れた場合など(こちらのケースの方が圧倒的に頻度は高いと思いますが)コミットの完了メッセージが行方不明になりトランザクションの成否が不明な状況になります。

コメント