スマート クライアント
NHibernate および Rhino Service Bus を使用した分散アプリケーションの構築
Oren Eini
私は長い経歴の大部分を Web アプリケーションに専念してきました。スマート クライアント アプリケーションの構築に初めて着手したとき、どうすればそのようなアプリケーションを構築できるのか途方に暮れてしまいました。データ アクセスの扱い方も、スマート クライアント アプリケーションとサーバーとの通信方法もわかりませんでした。
そのうえ、これまで使用してきた、開発時間と開発コストを大幅に削減できるツールセットに莫大な投資をしていたため、今後もなんとかしてそれらのツールを使用できるようにしたいと考えていました。自分が納得できるよう細部まで把握するのにしばらく時間がかかり、その間、Web アプリケーションの方がはるかに単純なのに、と何度も感じたものです。これは、Web アプリケーションの扱い方を既に知っているからに他ならないのですが。
スマート クライアント アプリケーションにはメリットとデメリットがあります。メリットとしては、スマート クライアントは応答が迅速でユーザーとの対話性に優れています。また、処理をクライアント コンピューターに移動することでサーバーの負荷が軽減され、ユーザーはバックエンド システムに接続していなくても作業を継続できます。
デメリットとしては、イントラネットやインターネット経由で行うデータ アクセスの速度、セキュリティ、帯域幅の制限など、スマート クライアント特有の課題があります。また、フロントエンド システムとバックエンド システム間でのデータの同期、分散時の変更の追跡、不定期接続環境での作業時に発生する問題への対処などを行わなければなりません。
今回説明するように、スマート クライアント アプリケーションは、Windows Presentation Foundation (WPF) または Silverlight を使用して構築できます。Silverlight は WPF 機能のサブセットを公開しているため、ここで簡単に説明するテクニックとアプローチは WPF にも Silverlight にも当てはまります。
今回の記事では、データ アクセスに NHibernate を、サーバーとの信頼性の高い通信に Rhino Service Bus を使用して、スマート クライアント アプリケーションの計画から構築までのプロセスに着手します。このアプリケーションはオンラインの貸出図書館のフロントエンドとして機能します。ここではこのアプリケーションを Alexandra と呼んでいます。アプリケーション自体は大きく 2 つの部分に分けられます。1 つは、ビジネス ロジックの大半が常駐する一連のサービスを実行するアプリケーション サーバーで、NHibernate を使用してデータベースにアクセスします。もう 1 つは、これらのサービスを容易にクライアントに公開できるスマート クライアント UI です。
NHibernate (英語) はオブジェクト リレーショナル マッピング (O/RM) フレームワークで、メモリ内データを扱うように簡単にリレーショナル データベースを操作できるよう設計されています。Rhino Service Bus (英語) は、Microsoft .NET Framework を基盤としたオープン ソースのサービス バス実装で、主に開発、展開、および使い易さに重点を置いています。
役割分担
貸出図書館を構築するための最初の作業は、フロントエンド システムとバックエンド システムの役割分担を適切に行うことです。1 つの方法は、アプリケーションでは主に UI に重点を置き、処理の大半をクライアント コンピューターで行います。この場合、バックエンドは主としてデータ リポジトリとして機能します。
つまり、バックエンドはデータ ストアの単なるプロキシとして機能する従来のクライアント/サーバー アプリケーションを再現するものです。バックエンド システムが単なるデータ リポジトリの場合は設計上有効な選択肢となります。たとえば、個人の書籍目録であれば、アプリケーションの動作は個人データの管理に限定され、サーバー側でデータを操作する必要はないため、このようなアーキテクチャを使用するメリットがあります。
この種のアプリケーションでは、WCF RIA Services または WCF Data Services を使用するのがお勧めです。バックエンド サーバーで外部に向けて CRUD インターフェイスを公開するのであれば、WCF RIA Services または WCF Data Services を活用することで、アプリケーションの構築に必要な時間を大幅に短縮できます。どちらのテクノロジを使用しても独自のビジネス ロジックを CRUD インターフェイスに追加することになりますが、このアプローチを使用してアプリケーションの主要動作の実装を試みると、メンテナンスが困難で不安定なものになりかねません。
ここではこのようなアプリケーションの構築については説明しませんが、Brad Adams がブログ (blogs.msdn.com/brada/archive/2009/08/06/business-apps-example-for-silverlight-3-rtm-and-net-ria-services-july-update-part-nhibernate.aspx、英語) で NHibernate と WCF RIA Services を使用してこのようなアプリケーションを構築するアプローチを順を追って示しています。
これとは正反対のアプローチとして、バックエンドにアプリケーションの動作の大半を実装し、フロントエンドには純粋にプレゼンテーションのみを担当させる方法があります。このアプローチは最初は妥当に思えますが、これは一般に Web ベースのアプリケーションを記述する方法であるため、クライアント側で実際のアプリケーションを実行する際に利用することはできません。状態管理がかなり難しくなります。そのため、このような複雑さをすべて抱えた状態で Web アプリケーションの記述に戻っていることになります。クライアント コンピューターに処理を移行することも、接続の中断を処理することもできないでしょう。
さらに悪いことに、ユーザーの立場からは、すべての操作でサーバーへのラウンドトリップが発生するため、UI の処理速度が遅くなります。
もうお分かりでしょうが、ここで使用するアプローチはこの 2 つの中間にあたります。クライアント コンピューターで実行することによって得られる可能性を活かしながら、アプリケーションの重要な部分をバックエンドのサービスとして実行する予定です (図 1 参照)。
図 1 アプリケーションのアーキテクチャ
サンプル ソリューションは 3 つのプロジェクトから構成されていて、github.com/ayende/alexandria (英語) からダウンロードできます。Alexandria.Backend はバックエンド コードをホストするコンソール アプリケーションです。Alexandria.Client はフロントエンド コードを保持し、Alexandria.Messages にはこの 2 つのコンポーネント間で共有されるメッセージ定義を保持します。サンプルを実行するには、Alexandria.Backend と Alexandria.Client の両方を実行しておく必要があります。
コンソール アプリケーションでバックエンドをホストするメリットの 1 つは、バックエンド コンソール アプリケーションをシャットダウンしてから再開するだけで、切断時のシナリオのシミュレーションを簡単に行えることです。
分散コンピューティングに関する誤った想定
アーキテクチャの基本は理解したので、スマート クライアント アプリケーションを記述する際の影響について見ていきましょう。バックエンドとの通信には、イントラネットまたはインターネットを使用します。多くの Web アプリケーションではリモート呼び出しの主なソースが同じデータセンター (多くの場合同じラック) にあるデータベースや別のアプリケーション サーバーであるという事実を踏まえると、このことは、複数の影響を受ける大きな変更点です。
イントラネットとインターネットの接続では、速度、帯域幅の制限、およびセキュリティの面での問題の影響を受けます。通信コストにおける非常に大きな違いにより、アプリケーションのすべての主要部分が同じデータセンター内に配置されている場合に採用していたものとは異なる通信構造になります。
分散アプリケーションで対処する必要のある最も大きな問題の中に、分散コンピューティングに関する誤った想定というのがあります。開発者が分散アプリケーションの構築時に行いがちで、最終的には間違いだと証明される一連の想定のことです。このような誤った想定に従うと、たいていの場合、機能が減少したり、システムの再設計と再構築に莫大なコストが必要になったりすることになります。誤った想定とは、次に示す 8 項目です。
- ネットワークは信頼できる
- 待ち時間はない
- 帯域幅は無限にある
- ネットワークのセキュリティは確保される
- トポロジは変わらない
- 管理者は 1 人存在する
- 転送にはコストがかからない
- ネットワークの種類は同じである
このような想定を誤っていると考えていない分散アプリケーションでは、サーバーで問題が発生することになります。こうした問題には、スマート クライアント アプリケーションで正面から対処する必要があります。このような状況では、キャッシュの使用が非常に重要なトピックになります。分散形式での作業に関心がない方にとっても、キャッシュはアプリケーションの応答性向上にほぼ常に役立ちます。
考慮する必要のあるもう 1 つの側面は、アプリケーションの通信モデルです。最もシンプルなモデルは、リモート プロシージャ コール (RPC) を実行できる標準のサービス プロキシだと思われるかもしれませんが、このようなプロキシを使用すると後で問題が発生することが多くなります。切断された状態を処理するコードがより複雑になり、UI スレッドでのブロックを回避する場合は、非同期呼び出しを明示的に処理することが必要になります。
バックエンドの基本
次に、アプリケーションのバックエンドを構成する方法が問題になります。優れたパフォーマンスを発揮し、UI の構成方法とはある程度分離した方法を使用します。
パフォーマンスと応答性の観点からは、バックエンドに 1 回だけ呼び出しを行い、表示画面に必要なデータをすべて取得するのが理想的です。この手段で生じる問題は、サービス インターフェイスがスマート クライアントの UI を正確に模倣したものになることです。これは多くの理由で悪影響を及ぼします。 たいていの場合、UI はアプリケーションの中で最も変わりやすい部分です。UI とのサービス インターフェイスをこの形式にすると、純粋な UI だけの変更によってサービスを頻繁に変更しなくてはいけなくなります。
つまり、アプリケーションの配置が非常に大変な作業になります。フロントエンドとバックエンドの両方を同時に配置する必要があるため、一度に複数のバージョンをサポートしようとすると、複雑さが増す可能性があります。さらに、サービス インターフェイスを、追加の UI を構築するためや、サードパーティのサービスや別のサービスの統合ポイントとして使用することができなくなります。
標準のきめ細かなインターフェイスを構築するという別の手段を試みても、誤った想定という壁に突き当たります。きめ細かなインターフェイスを使用すると、何度もリモート呼び出しが発生し、待ち時間、信頼性、および帯域幅に関する問題が発生します。
この課題に対処するには、一般的な RPC モデルとは決別することです。リモートで呼び出されるようにメソッドを公開するのではなく、ローカル キャッシュとメッセージ指向の通信モデルを使用してみましょう。
図 2 は、フロントエンドからバックエンドにいくつかの要求をまとめて送る方法を示しています。この方法では、リモート呼び出しを 1 回行いますが、サーバー側では UI のニーズに密接には結び付けられないプログラミング モデルを維持できます。
図 2 サーバーへの 1 回の要求に複数のメッセージを含める
応答性を向上させるために、いくつかのクエリに即座に応答できるローカル キャッシュを含めることができます。そうすると、応答性の高いアプリケーションになります。
このようなシナリオで考慮すべき事項の 1 つは、使用するデータの種類と、表示するデータの新鮮さに関する要件です。Alexandria アプリケーションでは、アプリケーションがバックエンド システムから新しいデータを要求するときに、キャッシュされたデータをユーザーに表示することが許容されるため、ローカル キャッシュに大きく依存します。株式取引などのアプリケーションでは、古いデータを表示するぐらいなら、何も表示すべきではありません。
切断状態での操作
次に直面する問題は、切断状態のシナリオへの対応です。多くのアプリケーションでは、接続が必須であることが仕様で決められているため、バックエンド サーバーが利用できなくなったときはユーザーにエラーを表示するだけです。しかし、スマート クライアント アプリケーションには切断状態でも操作できるというメリットがあり、Alexandria アプリケーョンではこのメリットを全面的に活用しています。
ただし、このことはキャッシュがさらに重要になることを意味します。キャッシュを使用することで通信速度を向上できるだけではなく、バックエンドと通信できない場合にキャッシュに保持されているデータを使って機能することもできます。
ここまでで、アプリケーションを構築する際に生じる課題については十分に理解できたでしょう。ここからは、こうした課題を解決する方法を見ていきましょう。
お気に入りの 1 つであるキュー
Alexandria では、フロントエンドとバックエンド間で RPC 通信は行われません。代わりに、図 3 に示すように、すべての通信がキューを経由する一方向メッセージを介して処理されます。
図 3 Alexandria の通信モデル
キューは、上記で特定した通信の問題を解決する非常に優れた方法を提供します。切断状態のシナリオをサポートするのは "困難" になるため、フロントエンドとバックエンドとの間で直接通信するのではなく、キュー サブシステムで通信のすべてを処理します。
キューの使用は非常に単純です。ローカルのキュー サブシステムに、メッセージを特定のキューに送信するよう依頼します。キュー サブシステムはメッセージの所有権を引き継ぎ、どこかの時点で送信先にメッセージが必ず到達するようにします。ただし、アプリケーションはメッセージが送信先に届くのを待機しないで、動作を続けることができます。
送信先キューが使用できない場合は、キュー サブシステムは、送信先キューが再び使用可能になるまで待機してからメッセージを配信します。通常、キュー サブシステムはメッセージが配信されるまでディスクにメッセージを保存しておくため、送信元のコンピューターが再起動された場合でも保留中のメッセージが送信先に届きます。
キューを使用するときは、メッセージと送信先という観点から考えると簡単になります。バックエンド システムに届くメッセージによって、なんらかの操作が呼び出されます。その結果、元の送信者に返信が送られることもあります。各システムは完全に独立しているため、どちらのシステムでもブロックは発生しません。
キュー サブシステムには、MSMQ、ActiveMQ、RabbitMQ などがあります。Alexandria アプリケーションでは、Rhino Queues (github.com/rhino-queues/rhino-queues、英語) という、オープン ソースで xcopy 配置のキュー サブシステムを使用します。インストールも管理も不要という単純な理由で Rhino Queues を採用しましたが、Rhino Queues は多くのコンピューターに配置することが求められるサンプルやアプリケーションに適しています。ついでにお知らせしておきますが、Rhino Queues は私が作成しました。お気に入りいただけるとさいわいです。
キューを使用する
ここでは、キューを使用してメイン画面のデータ取得を処理する方法を見ていきましょう。以下に、ApplicationModel の初期化ルーチンを示します。
protected override void OnInitialize() {
bus.Send(
new MyBooksQuery { UserId = userId },
new MyQueueQuery { UserId = userId },
new MyRecommendationsQuery { UserId = userId },
new SubscriptionDetailsQuery { UserId = userId });
}
メッセージのバッチをサーバーに送信し、いくつかの情報を要求します。ここには注目すべき点がたくさんあります。送信されるメッセージの粒度が高くなります。MainWindowQuery などの 1 つの汎用メッセージを送信するのではなく、非常に具体的な情報ごとに多くのメッセージ (MyBooksQuery、MyQueueQuery など) を送信します。既に説明したように、これにより、1 つのバッチで複数のメッセージを送信する (ネットワーク ラウンドトリップを軽減する) ことと、フロントエンドとバックエンド間の結合度を緩くすることの両方のメリットを得ることができます。
RPC は汝の敵である
分散アプリケーションを構築する際におかしやすい間違いの 1 つは、アプリケーションが分散されているという側面を無視してしまうことです。たとえば、WCF ではネットワーク経由でメソッドの呼び出しが行われているという事実を簡単に見過ごしてしまいます。これは非常にシンプルなプログラミング モデルですが、分散コンピューティングに関する誤った想定の 1 つに違反しないよう、細心の注意を払うことが必要です。
実際、WCF などのフレームワークによって提供されるプログラミング モデルは、ローカル コンピューター上でメソッドを呼び出すことに非常に似ていて、誤解しやすいのは事実です。
標準の RPC API では、ネットワーク経由の呼び出しを行うときにブロックが行われ、リモート メソッドの呼び出しが行われるたびに高いコストがかかり、バックエンド サーバーが使用できない場合はエラーが発生する可能性が高くなります。確かに、この API を基盤として優れた分散アプリケーションを構築することは可能ですが、細心の注意が必要です。
そこで、多くの SOAP ベースの RPC スタックによく見られる暗黙のメッセージ交換ではなく、明示的なメッセージ交換に基づくプログラミング モデルが新たなアプローチとして浮かび上がってきます。そのモデルは最初は奇妙に感じることがあり、考え方を少し変える必要がありますが、このモデルに移行すると考慮すべき複雑さが全体的に非常に軽減されます。
ここで例に使用している Alexandria アプリケーションは一方向メッセージング プラットフォーム上に構築されています。このアプリケーションでは、このプラットフォームをフル活用して、アプリケーションが分散型であるという事実を認識し、実際にその事実を活かしています。
どのメッセージも Query という用語で終わっていることに注意してください。これは単純な表記規則で、状態なしを変更し、なんらかの応答を求める純粋なクエリ メッセージを示すために使用します。
最後に、サーバーからなんらかの応答を取得しているように見えないことに注目してください。キューを使用しているため、"開始した後は忘れてよい (fire and forget)" という通信モードになっています。メッセージ (またはメッセージのバッチ) を送信したら、後は返信に対処します。
ここで、フロントエンドが応答を処理する方法を見ていく前に、先ほど送信したメッセージをバックエンドがどのように処理するのかを見てみましょう。図 4 は、バックエンド サーバーが書籍のクエリを使用する方法を示しています。ここで初めて、NHibernate と Rhino Service Bus の両方を使用する方法を確認できます。
図 4 バックエンド システムでのクエリの使用
public class MyBooksQueryConsumer :
ConsumerOf<MyBooksQuery> {
private readonly ISession session;
private readonly IServiceBus bus;
public MyBooksQueryConsumer(
ISession session, IServiceBus bus) {
this.session = session;
this.bus = bus;
}
public void Consume(MyBooksQuery message) {
var user = session.Get<User>(message.UserId);
Console.WriteLine("{0}'s has {1} books at home",
user.Name, user.CurrentlyReading.Count);
bus.Reply(new MyBooksResponse {
UserId = message.UserId,
Timestamp = DateTime.Now,
Books = user.CurrentlyReading.ToBookDtoArray()
});
}
}
このメッセージを処理する実際のコードを見ていく前に、このコードによって実行される構造について説明しましょう。
大事なのはメッセージ
Rhino Service Bus (hibernatingrhinos.com/open-source/rhino-service-bus、英語) は、言うまでもなくサービス バスの実装であり、NServiceBus (nservicebus.com、英語) から大きな影響を受けた、キューを使用する一方向メッセージ交換に基づく通信フレームワークです。
バス上を送信されるメッセージは、送信先キューに届き、そこでメッセージ コンシューマーが呼び出されます。図 4 のメッセージ コンシューマーは MyBooksQueryConsumer です。メッセージ コンシューマーは ConsumerOf<TMsg> を実装するクラスで、適切なメッセージ インスタンスを指定して Consume メソッドが呼び出され、メッセージを処理します。
メッセージ コンシューマーの依存関係を提供する制御の反転 (IoC: Inversion of Control) コンテナーを使用していることは、MyBooksQueryConsumer コンストラクターから推測できると思います。MyBooksQueryConsumer の場合、こうした依存関係はバス自体と NHibernate セッションです。
メッセージを使用する際の実際のコードは単純です。NHibernate セッションから適切なユーザーを取得して、メッセージの送信元に要求されたデータを含む返信を送ります。
また、フロントエンドにもメッセージ コンシューマーが存在します。このコンシューマーは、MyBooksResponse です。
public class MyBooksResponseConsumer :
ConsumerOf<MyBooksResponse> {
private readonly ApplicationModel applicationModel;
public MyBooksResponseConsumer(
ApplicationModel applicationModel) {
this.applicationModel = applicationModel;
}
public void Consume(MyBooksResponse message) {
applicationModel.MyBooks.UpdateFrom(message.Books);
}
}
このコンシューマーは単純にメッセージに含まれているデータでアプリケーション モデルを更新します。ただし、1 つだけ注意すべき点があります。consume メソッドは UI スレッドで呼び出されるのではなく、バックグラウンド スレッドで呼び出されます。にもかかわらず、アプリケーション モデルが UI にバインドされているため、更新は UI スレッドで行う必要があります。UpdateFrom メソッドはこのことを認識しており、適切なスレッドでアプリケーション モデルを更新するために UI スレッドに切り替えます。
他のメッセージを処理するコードは、バックエンドとフロントエンドのどちらにもあり、よく似ています。この通信は、純粋に非同期処理です。バックエンドからの応答を待機する場面はないため、.NET Framework の非同期 API は使用しません。代わりに、明示的なメッセージ交換を行います。このメッセージ交換は通常はほとんど即座に行われますが、切断モードで作業している場合には長時間かかることもあります。
バックエンドにクエリを送信することについて説明したときに、メッセージを送信するバスについては触れましたが、メッセージの送信先については説明しませんでした。図 4 では、Reply を呼び出しているだけで、メッセージの送信先は指定していません。では、バスはどのようにしてメッセージの送信先を認識するのでしょう。
メッセージをバックエンドに送信する場合、その答えは構成です。App.config に次の構成が含まれています。
<messages>
<add name="Alexandria.Messages"
endpoint="rhino.queues://localhost:51231/alexandria_backend"/>
</messages>
この構成では、Alexandria.Messages で始まる名前空間のすべてのメッセージを alexandria_backend エンドポイントに送信するよう、バスに指示しています。
バックエンド システムでメッセージを処理するときに Reply を呼び出すことは、単純に送信元にメッセージを返信することを意味します。
この構成では、メッセージがバスとサブスクリプション要求の送信先に配置されるときに、このメッセージの送信先にメッセージの所有権を指定します。そのため、この種のメッセージがパブリッシュされるときに、配布リストに含まれるようになります。Alexandria アプリケーションではメッセージ パブリケーションを使用していないため、ここでは説明しません。
セッション管理
これで通信メカニズムのしくみについては確認しましたが、先に進む前に、対処しておくべきインフラストラクチャの問題があります。NHibernate アプリケーションの場合、なんらかの方法でセッションの有効期限を管理し、トランザクションを適切に処理する必要があります。
Web アプリケーションの標準アプローチでは、1 要求につき 1 セッションを作成するため、各要求が独自のセッションを所有します。メッセージング アプリケーションの場合、動作はほとんど同じになります。1 要求につき 1 セッションを所有するのではなく、メッセージ バッチごとに 1 セッションを所有します。
これはほぼ完全にインフラストラクチャによって処理されます。図 5 は、バックエンド システムの初期化コードを示しています。
図 5 メッセージング セッションの初期化
public class AlexandriaBootStrapper :
AbstractBootStrapper {
public AlexandriaBootStrapper() {
NHibernateProfiler.Initialize();
}
protected override void ConfigureContainer() {
var cfg = new Configuration()
.Configure("nhibernate.config");
var sessionFactory = cfg.BuildSessionFactory();
container.Kernel.AddFacility(
"factory", new FactorySupportFacility());
container.Register(
Component.For<ISessionFactory>()
.Instance(sessionFactory),
Component.For<IMessageModule>()
.ImplementedBy<NHibernateMessageModule>(),
Component.For<ISession>()
.UsingFactoryMethod(() =>
NHibernateMessageModule.CurrentSession)
.LifeStyle.Is(LifestyleType.Transient));
base.ConfigureContainer();
}
}
ブートストラップは、AbstractBootStrapper から派生するクラスによって実装される Rhino Service Bus における明示的な概念です。ブートストラップは、一般的な Web アプリケーションの Global.asax と同じ働きをします。図 5 では、最初に NHibernate セッション ファクトリを構築してから、コンテナー (Castle Windsor) を設定して NHibenrateMessageModule から NHibernate セッションを提供します。
メッセージ モジュールには、Web アプリケーションの HTTP モジュールと同じ目的があります。つまり、すべての要求に横断的な問題を処理することです。ここでは NHibernateMessageModule を使用してセッションの有効期間を管理します (図 6 参照)。
図 6 セッションの有効期間の管理
public class NHibernateMessageModule : IMessageModule {
private readonly ISessionFactory sessionFactory;
[ThreadStatic]
private static ISession currentSession;
public static ISession CurrentSession {
get { return currentSession; }
}
public NHibernateMessageModule(
ISessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public void Init(ITransport transport,
IServiceBus serviceBus) {
transport.MessageArrived += TransportOnMessageArrived;
transport.MessageProcessingCompleted
+= TransportOnMessageProcessingCompleted;
}
private static void
TransportOnMessageProcessingCompleted(
CurrentMessageInformation currentMessageInformation,
Exception exception) {
if (currentSession != null)
currentSession.Dispose();
currentSession = null;
}
private bool TransportOnMessageArrived(
CurrentMessageInformation currentMessageInformation) {
if (currentSession == null)
currentSession = sessionFactory.OpenSession();
return false;
}
}
コードはかなりシンプルです。適切なイベントを登録し、適切な場所でセッションの作成と破棄を行えば完了です。
このアプローチの興味深い影響の 1 つは、バッチ内のすべてのメッセージが同じセッションを共有するため、多くの場合に NHibernate の最初のレベルのキャッシュを活用できることです。
トランザクション管理
セッション管理はこれで終わりですが、トランザクションについてはどうでしょう。
NHibernate のベスト プラクティスでは、データベースとのすべてのやり取りをトランザクションで処理すべきとされています。ただし、ここでは NHibernate のトランザクションを使用していません。なぜでしょう。
それはトランザクションが Rhino Service Bus によって処理されるためです。各コンシューマーが独自にトランザクションを管理するのではなく、Rhino Service Bus では別のアプローチを採用しています。それは、System.Transactions.TransactionScope を利用して、バッチ内のメッセージに対応するすべてのコンシューマーを網羅する 1 つのトランザクションを作成するアプローチです。
つまり、(1 つのメッセージではなく) "メッセージのバッチ" に対する応答で実行されるすべての操作が同じトランザクションに含まれます。NHibernate は自動的にアンビエント トランザクションに 1 つのセッションを参加させるため、Rhino Service Bus を使用している場合は、トランザクションを明示的に扱う必要がありません。
1 つのセッションと 1 つのトランザクションを組み合わせることで、簡単に複数の操作を組み合わせて 1 つのトランザクション単位にすることができます。また、NHibernate の最初のレベルのキャッシュから直接メリットを得ることもできます。たとえば、以下に MyQueueQuery を処理する関連コードを示します。
public void Consume(MyQueueQuery message) {
var user = session.Get<User>(message.UserId);
Console.WriteLine("{0}'s has {1} books queued for reading",
user.Name, user.Queue.Count);
bus.Reply(new MyQueueResponse {
UserId = message.UserId,
Timestamp = DateTime.Now,
Queue = user.Queue.ToBookDtoArray()
});
}
MyQueueQuery と MyBooksQuery を処理する実際のコードはほぼ同じです。ここで、次のコードで 1 セッションにつき 1 トランザクションを処理する場合のパフォーマンスの影響について調べてみましょう。
bus.Send(
new MyBooksQuery {
UserId = userId
},
new MyQueueQuery {
UserId = userId
});
一見すると、4 つのクエリを使用して、必要な情報をすべて収集しているように思えます。MyBookQuery では、あるクエリを使用して適切なユーザーを取得し、別のクエリを使用してユーザーの書籍を読み込みます。MyQueueQuery でも同じことが言えます。あるクエリを使用してユーザーを取得し、別のクエリを使用してユーザーのキューを読み込みます。
ただし、図 7 の NHibernate Profiler (nhprof.com、英語) の出力からわかるように、バッチ全体に 1 つのセッションを使用することで、実際には最初のレベルのキャッシュを使用して不要なクエリを回避しています。
図 7 処理要求の NHibnerate Profiler ビュー
不定期接続のシナリオのサポート
現状では、アプリケーションはバックエンド サーバーに接続できなくてもエラーをスローしませんが、これだけではあまり役に立ちません。
このアプリケーションを進化させる次の手順は、バックエンド サーバーが応答しない場合でもアプリケーションが操作を継続できるように、キャッシュを導入して実際の不定期接続型クライアントにすることです。ただし、アプリケーション コードからキャッシュを明示的に呼び出す従来のキャッシュ アーキテクチャは使用しません。代わりに、インフラストラクチャ レベルでキャッシュを適用します。
図 8 は、キャッシュがメッセージング インフラストラクチャの一部として実装されている場合に、ユーザーの書籍に関するデータを要求する 1 つのメッセージを送信したときの一連の処理を示しています。
図 8 同時メッセージング操作でのキャッシュの使用
クライアントから MyBooksQuery メッセージを送信します。メッセージはバス上に送信されますが、同時に、この要求の "応答" があるかどうかを確認するためにキャッシュに照会されます。前の要求に対する応答がキャッシュに含まれている場合、メッセージがバスに到着したばかりであるかのように、キャッシュは即座にキャッシュされたメッセージを返します。
バックエンド システムから応答が届きます。メッセージは通常どおり使用され、キャッシュに配置されます。表面上このアプローチは複雑に見えますが、効率的なキャッシュ動作が行われるため、アプリケーション コードではキャッシュの問題をほぼ完全に意識する必要がなくなります。永続キャッシュ (アプリケーションを再起動しても消去されないキャッシュ) を使用すると、データをバックエンド サーバーから要求する必要なく、完全に独立してアプリケーションを操作できます。
では、この機能を実装してみましょう。永続キャッシュを想定して (サンプル コードではバイナリ シリアル化を使用して値をディスクに保存する単純な実装を提供しています)、次の規則を定義します。
- メッセージが要求/応答のメッセージ交換に含まれている場合、そのメッセージをキャッシュできます。
- 要求メッセージと応答メッセージはどちらもメッセージ交換用のキャッシュ キーを送信します。
メッセージ交換は、1 つの Key プロパティを備えた ICacheableQuery インターフェイスと、Key プロパティと Timestamp プロパティを備えた ICacheableResponse インターフェイスによって定義されます。
この規則を実装するため、フロントエンドで実行する CachingMessageModule を作成し、送受信メッセージをインターセプトします。図 9 は受信メッセージがどのように処理されるかを示しています。
図 9 受信接続のキャッシュ
private bool TransportOnMessageArrived(
CurrentMessageInformation
currentMessageInformation) {
var cachableResponse =
currentMessageInformation.Message as
ICacheableResponse;
if (cachableResponse == null)
return false;
var alreadyInCache = cache.Get(cachableResponse.Key);
if (alreadyInCache == null ||
alreadyInCache.Timestamp <
cachableResponse.Timestamp) {
cache.Put(cachableResponse.Key,
cachableResponse.Timestamp, cachableResponse);
}
return false;
}
ここではそれほど多くのことは行っていません。メッセージがキャッシュ可能な応答であれば、そのメッセージをキャッシュに配置します。ただし、注意すべきことが 1 つあります。あるタイムスタンプのメッセージが届いた後で、それより前のタイムスタンプのメッセージが届くといった具合に、メッセージの順序が間違っている場合にも対応しています。これにより、最新の情報だけがキャッシュに保存されます。
図 10 からわかるように、送信メッセージの処理とキャッシュからのメッセージのディスパッチはかなり興味深いものです。
図 10 メッセージのディスパッチ
private void TransportOnMessageSent(
CurrentMessageInformation
currentMessageInformation) {
var cacheableQuerys =
currentMessageInformation.AllMessages.OfType<
ICacheableQuery>();
var responses =
from msg in cacheableQuerys
let response = cache.Get(msg.Key)
where response != null
select response.Value;
var array = responses.ToArray();
if (array.Length == 0)
return;
bus.ConsumeMessages(array);
}
キャッシュからキャッシュされた応答を収集して、それらの応答で ConsumeMessages を呼び出します。これにより、バスが通常のメッセージ呼び出しロジックを呼び出すため、メッセージが再び届いたように見えます。
ただし、キャッシュされた応答がある場合でも、メッセージを送信することに注意してください。これにより、ユーザーにはすばやく (キャッシュされた) 応答を提供し、バックエンドが新しいメッセージに応答したときに、ユーザーに表示される情報を最新状態に更新しています。
次のステップ
スマート クライアント アプリケーションの基本的な構成要素として、バックエンドの構成方法、およびスマート クライアント アプリケーションとバックエンド間の通信モードについては説明しました。間違った通信モードを選択すると、分散コンピューティングに関する誤った想定が行われることがあるため、通信モードは重要です。また、スマート クライアント アプリケーションのパフォーマンスを向上させるために非常に重要な 2 つのアプローチ、つまり、バッチ処理とキャッシュについても触れました。
バックエンドでトランザクションと NHibernate セッションを管理する方法、クライアントからのメッセージを使用してそのメッセージに返信する方法、およびブートストラップですべてをまとめる方法について確認しました。
ここでは、主にインフラストラクチャの問題に重点を置きました。次回は、バックエンドとスマート クライアント アプリケーション間でデータを送信する際のベスト プラクティス、および分散型の変更管理のパターンについて説明します。
Oren Eini (Ayende Rahien というハンドルネームでも活躍しています) は、いくつかのオープン ソース プロジェクト (NHibernate、Castle など) の現役メンバーであり、その他多数のプロジェクト (Rhino Mocks、NHibernate Query Analyzer、Rhino Commons など) の発起人でもあります。また、NHibernate 用のビジュアル デバッガーである NHibernate Profiler (nhprof.com、英語) の責任者です。仕事の状況については、ayende.com/blog (英語) を参照してください。