###### tags: `TransactionalInformationSystems` # 17章-1 ステートレスなアプリケーションの回復 ## 17章概要 ここではサーバ側でなく、クライアント側のアプリケーションプログラムに着目する。 したがって、サーバ側のデータの整合性は保証されており、トランザクションをアトミックに実行してくれるものと仮定する。 このような状況で、サーバのエラー、ソフトウェアエラー、伝送エラーなどにより失敗したプロセスやメッセージの回復を考える。 17.2では、特別ケースとして、トランザクションがクライアント-サーバ間のリクエスト-リプライのペア1つからなる場合を考える。この場合は、メッセージを永続&復旧可能なメッセージキューで管理するのが解決策になる。 17.3では、複数のリクエスト-リプライペアからなる、ユーザとのやり取りを含んだステートフルなプログラムの回復を考える。クライアントプログラム側でローカルにセッション情報を管理し、サーバで実行されるトランザクションは実質的に複数に分割される。メッセージキューをこの場合にも拡張することができる。 17.4では、長大なワークフローを考える。この場合ACID特性のI(分離性)はもはや満たせないので諦めるが、原子性や、ただ一度だけ実行されることは保証したい。トランザクションをキューで管理することが鍵になる。 17.5では、さらに一般化して、多くの状態を持つ長大なプロセスを複数生成する巨大なサービスでの回復を考える。 ## 17.2 ステートレスなアプリケーション まずステートレス(サーバとのやり取りに関する情報を記憶する必要がない)なクライアントを考える。その実行ステップは以下のようになる。 1. ユーザがコンピュータになんらかの入力を与える。クライアントはその入力に基づいてプログラムを実行する。 2. プログラムがデータサーバにリクエストを送る。 3. データサーバがリクエストをトランザクションとして処理し、リプライをクライアントに送り返す。 4. クライアントがリプライを受け取り、出力をユーザに提示する。 リクエスト/リプライが一度しか行われないので、この一連の手順が終わった時点で、アプリケーションは履歴をすべて忘れてもよい(=ステートレス)。次にユーザが入力を与えたときは、新しくプログラムを実行することになる。 さらに議論をシンプルにするため、ここでは非同期リクエストを禁止することにする。つまり、クライアントはリクエストを送った時点でプログラムを一時停止し、リプライが返ってくるまで待つこととする。 このようなアプリケーションでの最大の問題は、クライアント-サーバ間のメッセージがどちらかのクラッシュにより失われても、最終的に正しい実行結果を出力できるかということである。これを、persistent, recoverable message queues(永続かつ回復可能なメッセージキュー)を用いて、送信者はキューにメッセージを入れ、受信者はキューから取り出す形で実現する。 ### メッセージキューの実装 キュー自体は障害回復や同時実行制御を備えたサーバのストレージ上に格納され、複数のクライアントからアクセスされる。このサーバをキューマネージャと呼ぶ。同時実行制御はnon-FIFOキューのセマンティクスを利用して緩和できる(7章参照)。障害回復は13-16章の方法を用いればよい。 キューマネージャをデータサーバ自体に組み込む場合と、独立したサーバで運用する場合が考えうる。前者ではメッセージキューに関する操作もトランザクション内に組み込んでACID特性を保証できる。後者の場合はトランザクションを第4部で導入する分散トランザクションとみなす必要がある。 ### リクエストが失われないことの保証 クライアントがユーザからの入力を得たら、リクエストを即座にenqueueしてトランザクションをcommitする。これにより、キューマネージャでenqueueに関するログがforceされるのでユーザ入力はストレージに記憶される。 あとは、このキューに記録されたリクエストが一度だけ実行され、リプライがクライアントに確実に送り返されることを保証すればよい。 ### リクエストが1度だけ実行されることの保証 データサーバでは、トランザクションの内部にリクエストのdequeueとリクエストの実行、生成したリプライのenqueueを組み込む。こうすることで、リクエストの実行が失敗した時点でキューの状態が元に戻ってリクエストに再挑戦することができる。実行が成功してcommitされたら、リクエストはキューから消え、リプライがキューに入るので、1度だけの実行が保証できる。 ### リプライが失われないことの保証 クライアントのリプライ受け取りも、トランザクションで処理する。リプライをdequeueしてトランザクションをcommitする。失敗したらキューにリプライが戻されるし、成功したらリプライの正しい受け取りができている。 --- 総括すると、このプロセスは3つのトランザクションからなる。 1. クライアントによるリクエストのenqueue 2. サーバによるリクエストのdequeueとリプライのenqueue 3. クライアントによるリプライのdequeue クライアントから送られるのがストアドプロシージャでなくSQL文の列だったりした場合、メッセージキューを使わず、アプリケーション・サーバ双方でトランザクションを走らせ、その内部で一連のやり取りを行う。 ここまで考えてきた手法は3層アーキテクチャ(クライアント-アプリケーションサーバ-データサーバ)においても同様で、クライアント-アプリケーション間、アプリケーション-データ間の双方についてキューを用いたやり取り、もしくは単一トランザクション内での一連のやり取りを行う。キューに関する操作は全てアプリケーションサーバで、永続データに関する操作はすべてデータサーバ上で実行される。ただしこのとき、トランザクションは第4部で導入される分散トランザクションになる。 --- キューによる管理は大きなオーバヘッドを引き起こす一方で、**ACIDよりもさらに良い特性**(**確実に一度実行される**)を持たせることができるのが美点である。ただしこれはユーザから見た観点では必ずしも徹底されていない。例えば、ユーザにリプライの内容が出力された直後にトランザクションが失敗し、リプライがキューに戻されて再実行された場合、ユーザは出力内容を2回見ることになる。それほど頻度の多い現象でもないし、クリティカルでもないのであまり問題にはならないが。 ### 出力の検証 もしこの出力の重複表示がクリティカルな問題であるならば(ATMにおける現金出力など)、ハードウェア実装したカウンタを利用して既に出力されたか検証する方法を取ることはできる。出力を行うたびに現在のカウンタの値をインクリメントし、ストレージのある領域に保存する。出力前に現在のカウンタとストレージに保存された値を比較し、異なっていたなら出力をやめて単にリプライのdequeueだけを行う。 このような検証が必要になるのは、**一般に「出力」は実世界に影響を及ぼす操作**(画面表示や現金の引き出しなど)であり、undoができないためである。無かったことにはできないが、既に一度行ったかどうか検証することはできる。 ### 定理17.1 ステートレスなアプリケーションについてのqueued transaction protocolは以下を保証する。 1. いちどユーザ入力トランザクションがcommitされたら、リクエストはサーバによってただ一度実行される。 2. いちどユーザ入力トランザクションがcommitされたら、ユーザ出力は少なくとも一度出力される。 3. ユーザ出力が検証可能ならば、いちどユーザ入力トランザクションがcommitされたら、ユーザ出力はただ一度出力される。 #### 証明 障害が起こるケースを場合分けする。 まず、ユーザ入力トランザクションがcommitされたが、その後にクライアントが落ちた場合を考える。サーバは影響を受けないので、単にリクエストを実行してリプライをenqueueする。回復したクライアントは、既にリプライがキューにあればdequeueするし、なければ待つ。どちらにせよ少なくとも一度リプライを受け取って出力する。出力が検証可能ならばただ一度だけ出力する。 次に、サーバがトランザクション中に落ちた場合を考える。それがリクエストをdequeueする前なら、回復に際して何も行う必要がない。リクエストのdequeue後かつcommit前に起きた場合は、サーバを回復する過程でデータを復元しつつリクエストがキューに戻され、リプライがキューにあったらそれは除かれる。したがって、改めてリクエストを受け取ってただ一度だけ実行でき、リプライは前者と同じく正しく出力される。$\square$ 2層アーキテクチャにおける通常動作中のクライアント・サーバの疑似コードを置いておく。 ```verilog // クライアントによるリクエストのenqueue user-input-processing-by-client(): begin transaction; enqueue(request); commit transaction; // クライアントによるリプライのdequeue user-output-processing-by-client(): wait until reply queue is not empty; begin transaction; dequeue(reply); // 既に出力したかの最低限の検証(もっと厳密に検証:演習17.1) while user has not acknowledged the reply or sent the next request do present reply to user; end commit transaction; // サーバによるリクエストの実行 request-reply-processing-by-data-server(): begin transaction; dequeue(request); perform data operations and generate reply; enqueue(reply); commit transaction; // クライアントの回復プロセス restart-client(): check reply queue; if not empty then process reply like during normal operation; end // サーバの回復プロセス restart-server(): check request queue; if not empty then initiate processing of requests like during normal operation end ```