コンサルティングビジネスしていると、非同期通信パターンがもたらす固有のスケーラビリティを認めた後でさえ、「非同期できないものがある」と言う人々によく出くわす。よく引き合いに出される例はユーザ認証、つまりユーザ名とパスワードの組み合わせを取得しバックエンドのストアに対してそれが正しいかどうか確認する処理だ。ここでの我々の目的を考慮して、バックエンドにはデータベースを想定してみることにする。
セットアップ
この実例をセキュアなものにするために、パスワードは保管される前に不可逆なハッシュ化が行われているものと想定する。同様に、適切なネットワークインフラを想定すると、私たちの Web サーバは DMZ(サイト・英語) で隔離されるだろうし、次から次へと DB と通信するいくつかのアプリケーションサーバにアクセスする必要があるだろう。Web サーバ間のラウンドロビン・ロードバランシングや、とりわけユーザログインのようなものにも十分な可能性がある。
要点に入る前に、少し前置きさせてもらいたい。人々が非同期なアプローチを却下するときに私が気づいた共通点の一つは、彼らは実際のデプロイ環境、または複数のサーバ、ファーム、データセンターへのソリューションのスケールアップを軽視しているということである。
同期ソリューション
同期ソリューションでは、各サーバは、それぞれのユーザログインリクエストごとにアプリケーションサーバに接続することになるだろう。すなわち、アプリケーションサーバと、ひいてはデータベースサーバ上でのロードがログイン数に比例することになる。このロードの一つの特性は自身のデータがある場所、と言うよりむしろ、その場所の不足だ。仮にユーザ U がログインしたとする。DB は、ユーザ U と同じページのためにメモリに全てのユーザ名とパスワードデータをロードしても必ずしもパフォーマンス上の利益を得ないだろう。もう一つの特性は揮発性がないことである。そんなに頻繁に変化しないのだ。
同期ソリューションは今までに数多く分析されてきた(source)ので深くは踏み込まないことにする。結論を言うと、データベースがネックなのだ。シャードに分割するというソリューションを使う選択肢もある。大手サイトの多くが更新用のマスタと共にこの種類のデータ用の読み出し専用データベースを持っている。読み出し専用の複製にデータをコピーしているのである。もしあなたが( LAMP の) MySQL みたいな安いデータベースを使っているなら言うことない(source)が、もし Oracle や MS の SQLServer を運用しているのならあまり良いことではない。
あなたがデータ層でどんな処理をしていようが、データ層を使っていることにはかわりない。もし処理が Web サーバ内で完結するなら、すばらしいことではないだろうか? たとえあなたが Apache を使っていても、マシンや電力を削減し、全体の構成がクールになる。それが非同期ソリューションというものだ。他の面で節約するために低価格のメモリをフル活用するのだ。
非同期ソリューション
非同期ソリューションでは、Web サーバ上のメモリ内でユーザ名、およびハッシュ化されたパスワードをキャッシュし、それを使って認証する。どれくらいのメモリを必要とするのか分析してみよう。
ユーザ名はたいてい12文字ないしそれ以下だが、念のため、ここでは32文字としておこう。Unicode を使うとユーザ名は64バイトになる。ハッシュ化されたパスワードはアルゴリズムに応じて256から512ビットで実行され、8で割られて64バイトとなり、全部でおよそ128バイトになる。そのため我々は Web サーバごとに 1GB を使って800万ものユーザ名とパスワードを支障なくキャッシュすることができる。もしあなたが100万人のユーザを抱えてるなら、まず、それは素晴らしいことだ。しかし、100万人分はたった 128MB のメモリにしか相当しないから、安物の 2GB の Web サーバにとってもたいした問題ではない。
また、新しいユーザを登録するときに、そのユーザ名がすでに使われているかどうかを Web サーバレベルでチェックすることが可能だという事実を考えてみよう。これは、並行処理の問題(source)に対処するために DB での再チェックが不要になるわけではないが、DB 上でのデータのロードは更に減少するということだ。読み出し専用の複製と、複製処理がないことにも注目してほしい。とても単純だ。このソリューションでは Web サーバが「複製」なのだ。
認証サービス
すべてを円滑に動かすのはアプリケーションサーバ上の認証サービスだ。認証サービスは以前いつでも同期ソリューション内にあって、Web サーバからのログイン要求をさばいた。そして言うまでもなくサーバが新しいユーザを登録したり、他の一般的な作業をするのを可能にしていた。相違点といえば、現在は新しいユーザが登録されたとき(または有効化されたとき。ともに長期的なワークフローの一環)に、メッセージを発行することだ。受取側がユーザ名とハッシュ化されたパスワードの全ペアを含むリストを受け取ることも可能になった。メモリに同じデータを保存する可能性もかなり高いだろう。
オープンソース通信フレームワークの nServiceBus を使ってこのソリューションの実装を説明しようと思うが、同じ原理がどのメッセージングソリューションや ESB ソリューションにも見られるだろう。ひとつの物理メッセージで複数の論理メッセージを送るという nServiceBus(source) の機能を使うことによって、単一の更新の通知と、同じ論理メッセージを使った完全なリストの返却をモデル化することが可能になる。そのメッセージを定義してみよう。
[Serializable]
public class UsernameInUseMessage : IMessage
{
private string username;
public string Username
{
get { return username; }
set { username = value; }
}
private byte[] hashedPassword;
public byte[] HashedPassword
{
get { return hashedPassword; }
set { hashedPassword = value; }
}
}
完全なリストが必要なときにWebサーバが送るメッセージは次のとおり。:
[Serializable]
public class GetAllUsernamesMessage : IMessage
{
}
起動時に Web サーバが実行するコードはこのような感じになる(コンストラクタインジェクションを想定)。
public class UserAuthenticationServiceAgent
{
public UserAuthenticationServiceAgent(IBus bus)
{
this.bus = bus;
bus.Subscribe(typeof(UsernameInUseMessage)); // subscribe for updates
bus.Send(new GetAllUsernamesMessages()); // request the full list
}
}
認証サービスが GetAllUsernamesMessage を受け取るとそのメッセージハンドラは自らのユーザ名とパスワードのキャッシュにアクセスし、次のように呼出元に返却されるメッセージを構築する。
public class GetAllUsernamesMessageHandler : BaseMessageHandler
{
public override void Handle(GetAllUsernamesMessage message)
{
this.Bus.Reply(Cache.GetAll());
}
}
UsernameInUseMessage が届いたときに処理をする Web サーバ上のクラスは次のとおり。
public class UsernameInUseMessageHandler : BaseMessageHandler
{
public override void Handle(UsernameInUseMessage message)
{
WebCache.SaveOrUpdate(message.Username, message.HashedPassword);
}
}
アプリケーションサーバが完全なリストを送ると、UsernameInUseMessage 型の複数のオブジェクトが一つの物理メッセージとして Web サーバに送られる。しかし、Web サーバ上で動作する bus オブジェクトはこれらの各論理メッセージを上記のメッセージハンドラに一つずつ送り出す。
それゆえ、実際にユーザを認証する段階になるとこの Web ページ( MVC を利用しているならコントローラ)は以下のメソッドを呼び出す。
public class UserAuthenticationServiceAgent
{
public bool Authenticate(string username, string password)
{
byte[] existingHashedPassword = WebCache[username];
if (existingHashedPassword != null)
return existingHashedPassword == this.Hash(password);
return false;
}
}
新しいユーザを登録するとき、Web サーバはもちろん最初にキャッシュをチェックして、それからユーザ名とハッシュ化されたパスワードを含む RegisterUserMessage を送る。
[Serializable]
[StartsWorkflow]
public class RegisterUserMessage : IMessage
{
private string username;
public string Username
{
get { return username; }
set { username = value; }
}
private string email;
public string Email
{
get { return email; }
set { email = value; }
}
private byte[] hashedPassword;
public byte[] HashedPassword
{
get { return hashedPassword; }
set { hashedPassword = value; }
}
}
アプリケーションサーバに RegisterUserMessage が届くと、プロセスを処理するために長期のワークフローが新しく開始される。
public class RegisterUserWorkflow :
BaseWorkflow<RegisterUserMessage>, IMessageHandler<UserValidatedMessage>
{
public void Handle(RegisterUserMessage message)
{
//send validation request to message.Email containing this.Id (a guid)
// as a part of the URL
}
/// <summary>
/// When a user clicks the validation link in the email, the web server
/// sends a UserValidatedMessage containing the workflow Id
/// </summary>
public void Handle(UserValidatedMessage message)
{
// write user to the DB
this.Bus.Publish(new UsernameInUseMessage(
message.Username, message.HashedPassword));
}
}
UsernameInUseMessage は最終的にサブスクライブしている全ての Web サーバに届く。
パフォーマンスとセキュリティのトレードオフ
このワークフローを詳しく検証するとワークフローをふたつの別々のメッセージハンドラとして実装し、ワークフロー ID をメールアドレスで代用するという選択肢があることに気付く。このより性能の良い代替ソリューションの問題点はセキュリティと関係している。ワークフロー ID への依存を取り除くことによって、私たちはすでに受け取った RegisterUserMessage を持たずに UserValidatedMessage を受け取る用意があることを本質的に提示した
UserValidatedMessage の処理には、DB への書き込みと全ての Web サーバへのメッセージの送信など、比較的コストがかかるので、悪意あるユーザは多数の検出装置網をかいくぐってそれほど多くのメッセージを使わずに DOS(サイト・英語) 攻撃を行うかもしれない。有効なワークフローインスタンスを得ることができるだろう GUID の成りすましはさらにやっかいだ。また、ワークフローインスタンスは恐らくメモリ内の複製されたいくつかのデータグリッドで保管されるため、ルックアップの相対的なコストはかなり抑えられるだろう。検知器が見つけるまで DOS を回避するには十分だ。
改善されたバンド幅とレイテンシ
結論を言うと、データ層に文句をつけて早い段階でスケールアウトしなくてはならなくなるより、むしろ上記のような方法で Web 層から更に多くの物を得るほうが良い。また、この方法だとネットワークトラフィックがより少ないことに注目して欲しい。ユーザ名とパスワード程度なら大した影響はないが、同じ方法で構築された他のシナリオではもっと多くのデータを必要とするかもしれない。そして言うまでもなく、Web サーバ( DMZ 内の)からアプリケーションサーバや DB サーバに行ったり来たりしなくて済むので、ユーザをログインさせるのにかかる時間も同様にずっと少なくなる。
このソリューションで覚えておかないといけないのは、パブリッシュ/サブスクライブをすることである。nServiceBus はパブリッシュ/サブスクライブ周辺のシステムを設計するためにシンプルな API を提供しているに過ぎない。パブリッシングは本格的なスケーラビリティを得る手段だ。あなたが更に多くのユーザを獲得するにつれて、当然もっと Web サーバが必要になる。要するに、ログインを扱うためだけにデータベースサーバを追加する必要はないだろう、ということだ。この場合、成されるべき全ての作業はリクエストを受け取るサーバ上で処理できるので、リクエスト当たりのレイテンシは一層小さくなる(source)。
ETag で更なる改善を
上級者のために ETag(サイト・英語) を説明して終わりにしよう。Web サーバがダウンすると、キャッシュが失われる。私たちにできるのは、ディスクにキャッシュを書き込み(恐らくバックグラウンドスレッドで)、最後に受け取った UsernameInUseMessage と一緒にサーバから送られてきた ETag を使ってそのキャッシュにタグ付けすることだ。そうすると、Web サーバは、バックアップをするときに、アプリケーションサーバが変更されたものだけを送信できるように GetAllUsernamesMessage と一緒に ETag を送ることができる。HTTP の GET を If-Modified-Since ヘッダと一緒に使うことによって REST(サイト・英語) スタイルも実現できる。これらのことは、Web サーバ上のディスクスペースをわずかばかり犠牲にしてネットワーク利用率を更に引き下げることにつながる。
締めくくりに
たとえ今あなたが一つの物理サーバしか持っていなくて、それが Web サーバとデータベースサーバ両方の役割を果たしているとしても、このソリューションが処理速度を鈍らせることはない。どちらかと言えば、速度を向上させる。あなたが以前よりスケールアウトする用意ができているとしても、800万もの Facebook ユーザがあなたのサーバを訪れたときにアーキテクチャ全体を置き換える必要はない。
詳しい情報は
http://www.nServiceBus.com (英語)
nServiceBus は企業向けの .NET システムの構築を容易にするオープンソース通信フレームワークだ。nServiceBus は、スケーラビリティや重要な機能でもあるパブリッシュ/サブスクライブのサポート、そして統合された長期のワークフローや幅のある拡張性を提供することによって、分散システムの確固たる基盤をもたらすのである。
Podcast on Autonomous Servers and Publish/Subscribe (英語)
このポッドキャストでは、自律サービス、パブリッシュ/サブスクライブ通信、例外処理、データ重複、再利用、ガバナンスの問題を取り上げている。
著者について
ソフトウェアシンプリストである Udi Dahan 氏は広く認められた .NET のエキスパートであり、Microsoft Architects と Technologists Councils のメンバーでもある。
Udi 氏は、SOA やスケーラブルでセキュアな .NET アーキテクチャデザインや Web サービスに特化して、世界中のクライアントにトレーニングやアドバイスを提供するほか、ハイエンドなアーキテクチャコンサルティングサービスを行っている。
彼は INETA ( International Speakers Bureau of the International .NET Association )のメンバーで、IASA ( International Association of Software Architects )の協会会員、カンファレンスの常連プレゼンター、Dr.Dobb's 社によるスポンサーを受けている Web サービス、SOA 、 XML のエキスパートでもあり、定期的に著書も出している。
Udi 氏には彼のブログ http://www.UdiDahan.com (英語) から連絡が可能だ。
原文はこちらです:http://www.infoq.com/articles/async-high-perf-login-web-farms