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

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

この記事は、JPOUG Advent Calendar 2019 の22日目の記事です。
21日目はryota_hnkさんの11gしか知らないオッサンが19cを触るでした。そして、なんと5年ぶりにJPOUG Advent Calendarで書くのですが(そもそも、このブログで何か書くのも5年ぶり...)、最近ちょっと考えてみた誰もが知っていて日常的に使っている「COMMIT」について書いてみたいと思います。
COMMITにおけるREDOログへの同期I/Oがパフォーマンス上問題になっている際によく観測される待機イベントは皆さんがご存知の"log file sync"になるのですが、ここでは深く触れません。"log file sync"について知りたい方は、以下、かなり昔にCOMMITにまつわるLog Writerの仕組みを書いたので参考にしてもらえると良いと思います。(ブラウザで見ると文字化けする部分があるのですが、ダウンロードするときれいに見えるとフィードバックをもらっています)

COMMITの仕組みのおさらい

今回は、普段あまり気にしない(と思われる)COMMITの「失敗」や「成功」、さらにCOMMITの結果が「不明」といった状況について考えてみたいと思います。知っている人も多いと思いますが、念の為COMMITを実行した時の動き(若干デフォルメされ複数存在するlog writer workerはlg00と記載してます)を確認しておきます。


18c(12c以降)でのコミット時のシーケンス図

  1. sqlplusなどクライアントは"COMMIT"という文字列をwrite(2)でServer Processのソケットに書き込みます
  2. "COMMIT"を受け取ったServer Processはsemctl(2)でlog writer(lgwr)を起床させます
  3. 起床したlog writer worker(lg00)はpwrite64(2)でオンラインREDOログにlog bufferの内容およびCOMMIT自体のchange vectorを書き出します
  4. 書き込みが完了したlog writer workerはsemctl(2)でlog writerとServer Processを起床させます
  5. 起床したServer Processはwrite(2)でクライアントのソケットにCOMMIT完了のメッセージを書き込みます(最終的にはクライアント(sqlplus)はユーザー(コンソール)にCOMMIT完了のメッセージを書き出します)
  • この文脈では、semctl(2)はセマフォで別プロセスを起床させます
  • この文脈では、semtimedop(2)は最大で指定したtimeoutの時間セマフォで待機します

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

今回のポイントは仮にlog writer(やその他)の障害でlog writerが異常終了する場合、異常が発生したタイミングがオンラインREDOログへの書き込みの前(①〜③)と後(④〜⑤)で意味合いが大きく異なるということです。
異常終了が①〜③の間の場合、当然トランザクションは完了しておらずDBがクラッシュリカバリして復旧した際にデータはROLLBACKされますが、同じ異常終了でも④〜⑤の間の場合、COMMITは完了していることになります。つまり、COMMITの異常終了でもタイミングによりトランザクションが確定したり、していなかったりするわけです。まずはREDOログ書き込み完了前後の障害でクライアント(sqlplus)はどのようなエラーとなるか確認してみましょう。

REDOログへの書き込み完了前で障害が発生した場合

1) INSERTを実行
 SQL> insert into sample_table values (1);
2) log writerのsemtimedop(2)をブロック(上記シーケンス図の②の部分)
 $ ps -elf|grep lgwr
 0 t oracle    4006     1  0  80   0 - 511492 ptrace 10:43 ?       00:00:00 xe_lgwr_XE

 $ gdb -p 4006
 ...
 (gdb) catch syscall semtimedop
 Catchpoint 1 (syscall 'semtimedop' [220])
 (gdb) c
 Continuing.
3) COMMITを実行
 SQL> commit;
上記の状態でCOMMITが完了することはありません
4) この状態でlog writerの障害を発生(kill)させてみます
 $ kill -9 4006
また、2)のgdbのセッションをキャンセルします
 (gdb) q
 A debugging session is active.

  Inferior 1 [process 4006] will be detached.

 Quit anyway? (y or n) y
 Detaching from program: /opt/oracle/product/18c/dbhomeXE/bin/oracle, process 4006
 Quitting: ptrace: No such process.
5) sqlplusにエラーが返ります
 commit
      *
 ERROR at line 1:
 ORA-03113: end-of-file on communication channel
 Process ID: 11787
 Session ID: 1347 Serial number: 37730
6) クラッシュリカバリ後にデータが存在しないことを確認します
 SQL> select * from sample_table;

 no rows selected

今度はREDOログへの書き込み完了後で障害が発生した場合

1) INSERTを実行
 SQL> insert into sample_table values (1);
2) log writer workerのsemctl(2)をブロック(上記シーケンス図の④の直前部分)
 $ ps -elf|grep lg00
 0 S oracle   16262     1  0  80   0 - 507681 SYSC_s 13:40 ?       00:00:00 xe_lg00_XE

 $ gdb -p 16262
 ...
 (gdb) catch syscall semctl
 Catchpoint 1 (syscall 'semctl' [66])
 (gdb) c
 Continuing.
3) COMMITを実行
 SQL> commit;
上記の状態でCOMMITが完了することはありません
4) この状態でlog writer workerの障害を発生(kill)させてみます
 $ kill -9 16262
また、2)のgdbのセッションをキャンセルします
 (gdb) q
 A debugging session is active.

  Inferior 1 [process 16262] will be detached.

 Quit anyway? (y or n) y
 Detaching from program: /opt/oracle/product/18c/dbhomeXE/bin/oracle, process 16262
 Quitting: ptrace: No such process.
5) sqlplusにエラーが返ります
 commit
      *
 ERROR at line 1:
 ORA-03113: end-of-file on communication channel
 Process ID: 16646
 Session ID: 1346 Serial number: 18983
6) クラッシュリカバリ後にデータが存在することを確認します
 SQL> select * from sample_table;

  ID
 ----------
   1

COMMIT失敗時の動きのまとめ

今回のシミュレーションでは、クライアントはCOMMIT実行後にエラーを受け取りますが、タイミングによってCOMMITが成功していたり、本当に失敗していたりすることが分かります。(今回はlog writerの障害(=インスタンス停止)という極端な例となっていますが、Server Processからクライアントへの通信が途切れた場合など(こちらのケースの方が圧倒的に頻度は高いと思いますが)コミットの完了メッセージが行方不明になりトランザクションの成否が不明な状況になります)

仮にCOMMITがエラーになった場合、トランザクションを再実行すれば良いと考えて実際に再実行するとタイミングによっては簡単にデータの論理破壊(今回の場合だと二重登録)が発生することが理解できると思います。このようにアプリケーションから見てCOMMITが失敗した時の本当の意味でのトランザクションの成否はかなり不確実なので、論理破壊を防ぐためにPKやUKなどを設定することも重要です。しかし、アプリケーションによっては、COMMIT失敗時にトランザクションの再実行など必要な処理をより柔軟に制御したいという目的で、COMMIT失敗によるトランザクションの成否を簡単かつ正確に知ることが必要な場合があります。
このような場合にはOracle 12c R1で導入されたトランザクションガードを利用すると良いということで、トランザクションガードの話を次回することにします。

コメント