前回にも書いたとおりRDBMSの重要な部分にACIDのサポートがあるが、このうちAID、つまり原子性(トランザクションのロールバック・コミットは不可分)、分離性(他のトランザクションの影響を受けない)、耐久性(一旦commitしたトランザクションは意地でも消えない)の3つをサポートするためには重要な、ログの話をしよう。
まず、この3つをサポートすると言うことは以下の3つの機能を持つことに等しいと言える。
ただし、「ディスク書き込み途中にクラッシュしても」というのを満たすのは結構難しい。そこで、実現のためには実行前と実行後のデータをログとして残しておく、と言うのが有効になる。この手法はWrite Ahead Loggingと呼ばれ、強力なデータ保護を行うためのシステムでは不可欠な仕組みである。保管しているログは、クラッシュ時の復旧に、以下のように使われる。
ログがトランザクションの耐久性を保証するため、ログは常に完全に書き込めていなければならない。しかし、ログが書き込めていることで、実際に全体の再構成が可能になる。一般にはディスクの同期書き込みを使う。
ここでポイントになるのは「同期」というところで、いわゆるHDDの同期書き込みはすこぶる遅い。そのために、大量のデータを書き込むと遅くなると言うのはつねにRDBMSの泣き所である。とはいえ、常にあらゆるデータを同期書き込みするよりコストは低いため、WALを使うことは性能上有利である。
まあここまでは教科書に書いてある話であるが、ここからがL.star的解釈の始まりである。ポイントは、「実行前のデータ(UNDO用)」「実行後のデータ(REDO用)」を一体どこに保存するのか?というのが、その後のRDBMSの実装を理解するためのポイントだと思っている。
最後にいくつかあったものを個人的な視点から項目にまとめて表にしてみた。
まず、この3つをサポートすると言うことは以下の3つの機能を持つことに等しいと言える。
- トランザクションを完全にロールバックするために、以前のバージョンに戻すことが出来ること。
- トランザクション実行中のデータが見えないように工夫できること。
- トランザクションのコミット時には、完全にデータが書き込みできるていること。
ただし、「ディスク書き込み途中にクラッシュしても」というのを満たすのは結構難しい。そこで、実現のためには実行前と実行後のデータをログとして残しておく、と言うのが有効になる。この手法はWrite Ahead Loggingと呼ばれ、強力なデータ保護を行うためのシステムでは不可欠な仕組みである。保管しているログは、クラッシュ時の復旧に、以下のように使われる。
- ログをトランザクションの始めから終わりまで読む
- 全部そろっていて、そのトランザクションがコミットされていれば、ログの「実行後」のデータを書き込む
- 全部そろっていないか、トランザクションがロールバックしていれば、ログの「実行前」のデータを書き込んで戻す
ログがトランザクションの耐久性を保証するため、ログは常に完全に書き込めていなければならない。しかし、ログが書き込めていることで、実際に全体の再構成が可能になる。一般にはディスクの同期書き込みを使う。
ここでポイントになるのは「同期」というところで、いわゆるHDDの同期書き込みはすこぶる遅い。そのために、大量のデータを書き込むと遅くなると言うのはつねにRDBMSの泣き所である。とはいえ、常にあらゆるデータを同期書き込みするよりコストは低いため、WALを使うことは性能上有利である。
まあここまでは教科書に書いてある話であるが、ここからがL.star的解釈の始まりである。ポイントは、「実行前のデータ(UNDO用)」「実行後のデータ(REDO用)」を一体どこに保存するのか?というのが、その後のRDBMSの実装を理解するためのポイントだと思っている。
- 教科書通りの実装では、REDO/UNDOともログファイル上にある。新しいデータは、常に古いデータを上書きする形で記述する。トランザクションの整合性を持つためにはただしくロックを取得して、書き込み中のデータや新しすぎるデータを読まない必要がある。が、データファイルには常に最新のデータのみが格納されるため、データファイル量が一定になり、大変扱いやすいものである。しかし、多数のロックが発生しうることは、マルチプロセッサ環境ではあまりよろしくない。
- PostgreSQLを初めとする追記型ストレージは、つねに実行前のバージョンを削除「しない」ため、そのままこれを実行前データ格納用のログとして使うことが出来る。また、MVCCと相性が非常に良く、ロックの削減に役立つ。持っているトランザクションログは、REDOログだけを記録する。
きわめてシンプルでスペックも素晴らしい。しかし、旧バージョンの削除という大問題がある。 しかし、ロールバックについては最も簡単である。 - ロールバックセグメント方式というのは追記型に工夫を加え、古いバージョンを全てロールバックセグメントに移動させる。ロールバックセグメントにはUNDO用のログが用意され、通常MVCCとからめて利用できる。つまり検索可能なUNDOログである。ログは分けられているため旧バージョンの削除が容易であるが、旧バージョンと新バージョンの移動という、ロールバックセグメントの管理自体が面倒である。
- シャドウページングと呼ばれるアルゴリズムは、WALを使わずにデータ保護を行おうというものである。いや、むしろデータファイルが無くてログだけが存在すると考えた方が良い。常にデータは追記書き込みされ、commit時点で最新バージョンを参照するように変更される。内部的にはログなので、例えばバックアップを残すことも容易である。しかし、完全な追記でもないかぎりファイルにおける論理的な位置と物理的な位置が一切一致しない。
スペック的にはいたれりつくせりのように見えるが、今の所まだまだ確立途中であり、ファイルシステムなどで上手に使われている一方でRDBMS内部でこのようなストレージは使われていないのでは無かろうか。
最後にいくつかあったものを個人的な視点から項目にまとめて表にしてみた。
項目 | 上書き型 | 追記型 | ロールバックセグメント型 | シャドウページング |
UNDOログ | ログ | データファイル | ロールバックセグメント | データファイル |
REDOログ | ログ | ログ | ログ | データファイル |
読み<->書きロック競合処理 | 行ロック | なし(MVCC) | なし(MVCC) | なし(MVCC) |
書き-書き競合 | 行ロック | 行ロック | 行ロック | 行ロック |
コミット時処理 | ほぼ無し | ほぼ無し | ほぼ無し | ほぼ無し |
ロールバック処理 | UNDO | ほぼ無し | UNDO | ほぼ無し |
1KB上書き時 データ書き込み量 | 2KB同期 1KB非同期 | 1KB同期 1KB非同期 | 1KB同期 1KB同期? 1KB非同期 | 1KB同期 |
上記ロールバック時書き戻し量 | 1KB非同期 | 無し | 1KB非同期 | 無し |
上記上書き時データ増加量 | 0 | 1KBデータファイルに | 1KBロールバックセグメントに | 1KB |
増加データ管理 | 必要なし | データファイルのパージ | ロールバックセグメントによる | データファイルのパージ |