ちょっと硬派なコンピュータフリークのBlogです。

カスタム検索

2009-02-23

もしもデータベースサーバがクラッシュしたら

人の作ったものは完璧ではない。完璧でないものはクラッシュする。故にデータベースはクラッシュする。サーバハードウェアの故障、OSのクラッシュ、データベースそのもののバグなど原因は様々であるが、永遠に停止することなく動き続けるRDBMSというものは存在しない。もちろん数年間無停止で問題が出ない場合もあるが、クラッシュするときはするので対策が必要である。もしもの時に備えて抜かりないのがスマートなオトコのスタイルである。データベースのご利用は計画的に。

トランザクションのことなら全部データベースに任せればいいじゃん!!

と思ったそこのアナタ!!人生そんなに楽ではありません。

もちろんRDBMSはトランザクションのDurabilityを保証しているので99%の場合はそれでOKだけど、それはCOMMITが成功した場合の話。COMMITは大抵の場合高速な処理であるが、それでも処理にかかる時間はゼロではない。アプリケーションがデータベースサーバにCOMMITを送信してから、その応答が返るまでの間にデータベースがクラッシュしてしまった場合、COMMITが成功したかどうかはアプリケーションからは分からない。もう少し具体的に言うと、例えばJavaなどの場合にはデータベースサーバがクラッシュした場合にはSQLExceptionがスローされるが、SQLExceptionの内容を見ただけではテーブルが正常に更新されたのかどうかの判断がつかないのである。それはなぜか?次の絵を見てもらいたい。
この絵はMySQLにおけるCOMMIT時の一連の処理の流れを簡略的に表したものである。1の処理中に失敗すると、アプリケーションは「COMMITリクエストをサーバに送る事ができなかった」ことが分かるので、処理が失敗したことを認識することができる。また、5まで来れば処理が成功したと判断できる。しかし、2〜4の間でデータベースサーバがクラッシュしているとどうだろうか。データがファイルに書き込まれる前にクラッシュした(2の場合)かも知れないし、ファイルへの更新が行われた後(4の場合)だったかも知れない。はたまたファイルの更新が中途半端な状態でクラッシュした(3の場合)かも知れない。しかしいずれの場合もMySQLのエラーメッセージは「MySQL server has gone away」である。

つまり、データベースがCOMMIT処理中にクラッシュした場合、アプリケーションにおいてデータが正常に更新されたのかそうでないのかが不明であるという前提に立って、エラーハンドリングをしなければならないのである。ではエラーハンドリングはどのように行えばいいのだろうか。

1. XAトランザクションを利用する

XAトランザクションとは、分散トランザクション(複数のRDBMSにおいてアトミックな処理を行う)のための規格であり、二相コミットを利用したロジックを実装しているのが特徴的である。二相コミットでは、COMMITの処理を分割して、PREPAREフェーズとCOMMITフェーズに分けている。PREPAREフェーズのトランザクションは、データベースサーバがクラッシュしても残っており、エラーハンドリング時には必要に応じてCOMMITまたはROLLBACKをすればいいわけである。

XAトランザクションは単体でも利用することができるが、分散トランザクションとしてトランザクションマネージャを通じて利用するのが一般的である。(二相コミットを自分で実装するのは極めて面倒である。)J2EEを利用した開発であれば、JTA(Java Transaction API)を利用することができる。この辺の話は次のページに詳しく書かれているので参照して欲しい。
MySQLでは、MySQL Server 5.0以降、Connector/J 5.0以降、かつInnoDBおよびバイナリログ利用時にXAトランザクションをサポートしている。MySQLリファレンスマニュアルのXAトランザクションのページはこちら

XAトランザクションを利用する場合の問題点は、適切なトランザクションマネージャミドルウェアを利用出来ない場合があることである。J2EEなどの適切なフレームワークを利用していない場合や、ミドルウェアを買うための予算がない場合などの理由などが考えられる。

また、J2EEを利用している場合でもXAトランザクションを利用することのデメリットが存在する。MySQLの場合、
  • XAトランザクション処理中は通常の(非XAの)トランザクションを処理することができない。
  • 二相コミットのためのオーバーヘッドが発生する。
  • 分離レベルはREPEATABLE-READまたはSERIALIZABLEでなければならない。
といったデメリットが挙げられる。

2. 同じ更新をもう一度適用する

COMMITが成功したかどうかが分からないならば、もう一度同じ内容を書き込めばいいじゃないか?!

と思うかも知れない。しかし事はそんなに単純ではない。例えばお金を扱うアプリケーションにおいて、「Aさんの口座の金額を100万円増やす」という処理を盲目的にやり直したらどうだろう。Aさんはデータベースがクラッシュしたことによって100万円余分にゲットしてほくほく顔になるが、サイトの運営者には出所不明の100万円が計上されてしまう。

このようなことにならないようにするには、処理をidempotentにしておく必要がある。idempotentとは、何度実行しても一回だけ実行したのと同じ結果になる性質のことである。例えば、多くのウェブサイトでは「ブラウザ上で誤ってクリックを2回押してしまった」という事態に対処するロジックが含まれていることだろう。ブラウザには1回だけクリックした時と同じ結果を表示するべきであるが、この場合そのウェブサイトはidempotentであるといえる。

idempotentな性質を保証するための仕組みとしては、Version Numberパターンが挙げられる。このデザインパターンは、versionという名前のINT型カラムを用意することで実装する。UPDATEを行うときにWHERE句でversionを条件として指定し、それと同時にversionのインクリメントを行う。そうすることで、2回目以降はversionの値が異なるので同じSQL文を実行してもテーブルは更新されないという寸法である。具体的なUPDATE文は次のようになる。

mysql> UPDATE tbl1 SET col2=10000, col3='abc', version=1
    -> WHERE col1=123 AND version=0;

このSQL文を一度実行したあとは、col1=123 AND version=0の条件に適合するカラムは既に存在しないので、2回目以降の更新は行われない。

つまり、更新処理をidempotentにしておけばCOMMIT中にデータベースサーバがクラッシュした場合には「データベースが再起動した際に何も考えずに同じ処理をもう一度実行」すればいいというわけである。これならエラーハンドリングの処置は簡単である。

Version NumberパターンはEJBで一般的に用いられている手法であり、書籍「Javaデータアクセス実践講座 (DB Magazine SELECTION)」などで詳しく取り上げられている。

なお、idempotentとXAトランザクションは別の概念であり、多くの場合ウェブサイト構築時などには前者は必須であったりする。エラーハンドリングという観点からするとidempotentの特性を利用した「何も考えずにもう一度更新!」という処理が簡単便利であるため、XAトランザクションの使いどころを狭めているのかもしれない。とはいえ、ウェブページのidempotentな特性と、データアクセスにおけるidempotentな特性は必ずしも同時に実装する必要はなく、ウェブページはidempotentであるがデータアクセスのエラーハンドリングはXAトランザクションで行うという設計も可能である。

3. ログ用のテーブルを利用する

XAトランザクションやidempotentによるキレイなエラーハンドリングを行うのが理想的であるが、理想通りに物事が進まないのが世の常というものである。理想の年収、理想の住宅、理想の体型、理想の恋人、理想のデータベース設計。どれも手に入れるのは非常に難しい。現場ではデータベース設計やアプリケーションの実装などはカオスになっていることも多いだろう。(過去の遺産もといソフトウェア資産のメンテナンスも続けて行かなければならない。)そのような場合、「設計を見直してXAトランザクションを使う」とか「idempotentなデータアクセスにする」などということは困難ではないだろうか。

そんなとき、さらにカオス度を増すようで乗り気はしないが、新たにログ用のテーブルを導入する方法が考えられる。現在進行中の処理を特定できる情報として、セッションの番号やトランザクションの番号を記録しておくわけである。例えば次のようにログ用のテーブルを定義する。
mysql> CREATE TABLE lg_tbl1
    -> tx_id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    -> session_id VARCHAR(128))
    -> ENGINE InnoDB;

セッションIDはオマケである。別になくてもいい。

そして次のように更新処理を行う。

トランザクションの開始
mysql> BEGIN;

ログテーブルを更新
mysql> INSERT INTO lg_tbl1 (session_id) VALUES('ABCDEFG');

挿入したtx_idの取得
mysql> SELECT LAST_INSERT_ID();

本番テーブルの更新
mysql> UPDATE tbl ...;

トランザクションのコミット
mysql> COMMIT;

こうしておくことで、データベースがクラッシュした場合にはログテーブルを参照すればトランザクションが成功したかどうかが一目瞭然というわけである。トランザクションが失敗していれば同じ更新をやり直せばいいし、成功していればそのまま処理を続行すればいい。

もちろんテーブルそのものに更新を特定できる情報(上記のtx_idのような)を含めてしまってもいい。しかし、エラーハンドリングの目的であれば古い内容は消してしまっても構わないので、ログとしてテーブルを分けておくことで要らなくなったデータを消去できるためディスク領域を節約できるというメリットがある。

4. 対処しない

おそらく、現存する中規模以下のウェブサイトの中でこのパターンが最も多いのではないだろうか。対処しない。つまりノーガード戦法。

オトコらしいといえばオトコらしい方法であるが、当たったときのダメージが大きすぎるのでノーガード戦法は漫画の中だけの話にして頂きたい。ITにおいてはガードは何重にも万全なのが望ましい。

COMMIT中にデータベースがコケた時に、COMMITが完了したものと見なして一切の対処をしないというのではクレームの元になりかねない。(注文したつもりなのにいつまで経っても商品が届かないよ!!ということが起こり得るからである)従ってノーガード戦法は避けたいが、かといって複雑なエラーハンドリングを実装するのは時間もコストもかかってしまう。ならば、COMMIT中にデータベースがコケた場合には次のようなメッセージを示す画面にユーザを誘導するといいだろう。

「更新処理中にデータベースのエラーが発生しました。担当者に連絡して、ただいまのお取引が正常に行われたことを確認してください。」

スマートではないが、例外的な例外処理実装ということで。

0 コメント:

コメントを投稿