クラウド ストレージ
Windows Azure ストレージによるアプリケーション エンジンの強化
Kevin Hoffman および Nathan Dudek
開発者は、まるでお気に入りの毛布に固執するように、物理的に触れることができるインフラストラクチャに固執する傾向があります。物理的なインフラストラクチャであれば使い方や操作方法もわかるし、問題が起きればその場所を突き止めることができます。このことが、クラウド コンピューティングなどの新しいテクノロジの導入を遅らせる障害になることがよくあります。
懐疑的な開発者が抱く最大の疑問点の 1 つが、どうすればクラウドで引き続きバックグラウンド処理を実行できるのか、つまり、自分たちが開発した "エンジン" を引き続き機能させるにはどのようにすればよいかということです。今回の記事では、Windows Azure ストレージを使用してアプリケーション エンジンを構築し、非同期メッセージングと非同期処理の実装方法を示すことで、「クラウドにはバックグラウンド処理が欠けている」という定説を払拭します。
開発者が物理インフラストラクチャというお気に入りの毛布を脱ぎ捨て、アプリケーション エンジンをクラウドに配置できることを証明するために、Hollywood Hackers という電子商取引アプリケーションの小さなサブセットの実装を取り上げます。このアプリケーションでは、物理の法則や古くからの常識をまったく無視した、ハリウッドで使用されている不思議なテクノロジを購入できます。
ここでは、主に次の 2 つのシナリオについて説明します。
- アプリケーションのユーザーへの非同期テキスト メッセージ ("トースト") の送信。ショッピング カートが送信されたといった重要なイベントの通知や、従業員どうしのメッセージ送信を行います。このシナリオでは、Windows Azure キュー、Windows Azure テーブル、および Windows Azure Worker ロールを使用します。
- Windows Azure キューと Windows Azure Worker ロールを使用したフルフィルメント エンジンへのショッピング カートの送信。
キュー ストレージによるアプリケーション内メッセージング
具体的なシナリオについて説明する前に、Windows Azure キューの基礎についていくつか説明する必要があります。クラウドでのキューは、.NET アプリケーションでの標準的なキューのようには機能しません。AppDomain 内でデータを操作するときは、そのデータのコピーが 1 つしかなく、1 つのマネージ プロセス内に間違いなく存在していると考えることができます。
しかし、クラウドでは、データの一部がカリフォルニアにあり、別の一部がニューヨークにあるかもしれません。さらに、そのデータをテキサスで処理する Worker ロールが存在し、それとは別にノースダコタでデータを処理する Worker ロールが存在するかもしれません。
こうした分散コンピューティングや分散データを扱う場合、多くの開発者にはなじみのない問題が持ち上がります。たとえば、エラーにつながる可能性があるコードを記述したり、データのコミット時に複数回試行するという考え方、つまりアイデムポテントな (複数回実行しても有効なのは一度だけという) 考え方を組み込んだりしてしまいます。
インプロセスの標準的な CLR のキューのように扱わない限り、Windows Azure キューのしくみは非常に簡単です。まず、アプリケーションからキューにいくつかのメッセージを要求し (ただし、一度に 20 個を越えるメッセージを要求してはいけないので注意してください)、その際、タイムアウトを設定します。このタイムアウトの期間、そのキューを処理する他のクライアントからは要求中のメッセージが見えなくなります。アプリケーションでそのキュー メッセージに対して実行すべき処理を完了すれば、そのメッセージをキューから削除します。
アプリケーションが例外をスローしたり、キュー メッセージの処理に失敗したりすると、タイムアウト期間経過後にそのメッセージが他のクライアントから再度見えるようになります。その結果、ある Worker ロールが処理に失敗しても、別の Worker ロールが引き続き処理を実行することが可能です。キューにメッセージを送信するのは非常に簡単で、アプリケーションで適切な HTTP POST メッセージを (直接、またはクライアント ライブラリを使用して) 作成し、文字列かバイト配列のいずれかを送信します。キューは明確にアプリケーション内メッセージング向けにデザインされ、ストレージに保存されるわけではないため、メッセージはかなり短くしておく必要があります。
少し触れたように、同じメッセージを処理しようとする複数の Worker ロールが存在する可能性があります。現在処理中のメッセージを見えなくするタイムアウトも役には立ちますが、確実ではありません。競合を完全に回避するには、"アイデムポテント" になるようにエンジンの処理をデザインする必要があります。つまり、アプリケーションの一貫性を保ちつつ、1 つ以上の Worker ロールが同じキュー メッセージを複数回処理できるようにします。
特定のメッセージの処理が既に完了しているかどうかを Worker ロールが検出できるのが理想です。キュー メッセージを処理する Worker ロールを作成するときに、既に処理が完了したメッセージを処理しようとする可能性がありますが、その可能性は低いことを覚えておいてください。
図 1 のコード スニペットは、Windows Azure SDK で提供される StorageClient アセンブリを使用して、メッセージを作成し、Windows Azure キューにメッセージを送信する方法を示しています。StorageClient ライブラリは、実際には、Windows Azure ストレージの HTTP インターフェイスのラッパーです。
図 1 メッセージの作成と Windows Azure キューへのメッセージの送信
string accountName;
string accountSharedKey;
string queueBaseUri;
string StorageCredentialsAccountAndKey credentials;
if (RoleEnvironment.IsAvailable)
{
// We are running in a cloud - INCLUDING LOCAL!
accountName =
RoleEnvironment.GetConfigurationSettingValue("AccountName");
accountSharedKey =
RoleEnvironment.GetConfigurationSettingValue("AccountSharedKey");
queueBaseUri = RoleEnvironment.GetConfigurationSettingValue
("QueueStorageEndpoint");
}
else
{
accountName = ConfigurationManager.AppSettings["AccountName"];
accountSharedKey =
ConfigurationManager.AppSettings["AccountSharedKey"];
queueBaseUri =
ConfigurationManager.AppSettings["QueueStorageEndpoint"];
}
credentials =
new StorageCredentialsAccountAndKey(accountName, accountSharedKey);
CloudQueueClient client =
new CloudQueueClient(queueBaseUri, credentials);
CloudQueue queue = client.GetQueueReference(queueName);
CloudQueueMessage m = new CloudQueueMessage(
/* string or byte[] representing message to enqueue */);
Queue.AddMessage(m);
今回の記事の他のサンプルでは、この処理を簡素化するいくつかのラッパー クラスを使用しています。これらのラッパー クラスは CodePlex サイトの「Hollywood Hackers」(hollywoodhackers.codeplex.com/SourceControl/ListDownloadableCommits.aspx、英語) で入手できます。
非同期メッセージング (トースト)
対話型 Web サイトは、単なる最近の流行ではなく、必要とされています。ユーザーは完全に対話可能な Web サイトに慣れてきたため、静的で対話できないページに遭遇すると違和感を感じるでしょう。このことを念頭に置けば、ユーザーがサイトを使用しているときに通知を送信できるようにすることになります。
そのためには、Windows Azure キューと Windows Azure テーブルのストレージ メカニズムを利用して、メッセージ配信フレームワークを構築します。クライアント側では、jQuery を jQuery Gritter プラグインと組み合わせて使用し、ユーザーのブラウザーで通知をトーストとして表示します。トーストとは、新しい Outlook 電子メール、インスタント メッセージ、ツイッターなどを受信したときに Windows システム トレイ上に表示されるメッセージと同じものです。
ユーザーに通知を送信する必要があるときは、通知がキューに挿入されます。Worker ロールがキュー内の各項目を処理する際に、各項目の処理方法が動的に決定されます。今回の例では、エンジンが実行する処理は 1 つしかありませんが、複雑な CRM Web サイトやヘルプデスク サイトでの可能性は無限大です。
Worker ロールがキュー内にユーザー通知があることを検出すると、通知をテーブル ストレージに格納し、キューから削除します。その結果、通知先のユーザーがログインするまで、メッセージを長期間保持できます。キュー ストレージ内のメッセージの最大有効期間は短く、わずか数日です。ユーザーが Web サイトにアクセスすると、jQuery スクリプトによってテーブルからすべてのメッセージが非同期に取り出され、既知の形式で JavaScript Object Notation (JSON) を返すコントローラーのメソッドが呼び出されて、ブラウザーにそのメッセージが表示されます。
キューでは文字列かバイト配列しか処理されませんが、データが構造化される型でも、バイナリにシリアル化し、必要になったときにシリアル化を解除すれば、どのようなデータ構造の型でもキューに格納することができます。これは、厳密に型指定されるオブジェクトをキューに渡す際に強力な技法になります。この技法を、キュー メッセージの基本クラスに組み込みます。その後、システム メッセージのクラスにデータを含めて、オブジェクト全体をキューに送信し、必要に応じて利用することができます (図 2 参照)。
図 2 キューへの構造化されたデータの格納
namespace HollywoodHackers.Storage.Queue
{
[Serializable]
public class QueueMessageBase
{
public byte[] ToBinary()
{
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
ms.Position = 0;
bf.Serialize(ms, this);
byte[] output = ms.GetBuffer();
ms.Close();
return output;
}
public static T FromMessage<T>(CloudQueueMessage m)
{
byte[] buffer = m.AsBytes();
MemoryStream ms = new MemoryStream(buffer);
ms.Position = 0;
BinaryFormatter bf = new BinaryFormatter();
return (T)bf.Deserialize(ms);
}
}
[Serializable]
public class ToastQueueMessage : QueueMessageBase
{
public ToastQueueMessage()
: base()
{
}
public string TargetUserName { get; set; }
public string MessageText { get; set; }
public string Title { get; set; }
public DateTime CreatedOn { get; set; }
}
BinaryFormatter クラスを使用するには、Windows Azure Worker ロールが完全信頼モードで実行されている必要があるので注意してください (これは、サービス構成ファイルを使用して有効にできます)。
ここで、キューを操作するための単純なラッパーが必要になります。根本的には、メッセージをキューに挿入し、保留中の任意のメッセージを取得し、キューをクリアする機能が必要です (図 3 参照)。
図 3 キューを操作するためのラッパー
namespace HollywoodHackers.Storage.Queue
{
public class StdQueue<T> :
StorageBase where T : QueueMessageBase, new()
{
protected CloudQueue queue;
protected CloudQueueClient client;
public StdQueue(string queueName)
{
client = new CloudQueueClient
(StorageBase.QueueBaseUri, StorageBase.Credentials);
queue = client.GetQueueReference(queueName);
queue.CreateIfNotExist();
}
public void AddMessage(T message)
{
CloudQueueMessage msg =
new CloudQueueMessage(message.ToBinary());
queue.AddMessage(msg);
}
public void DeleteMessage(CloudQueueMessage msg)
{
queue.DeleteMessage(msg);
}
public CloudQueueMessage GetMessage()
{
return queue.GetMessage(TimeSpan.FromSeconds(60));
}
}
public class ToastQueue : StdQueue<ToastQueueMessage>
{
public ToastQueue()
: base("toasts")
{
}
}
}
テーブル ストレージ用にラッパーを設定して、ユーザーがサイトにログインするまでユーザー通知を格納できるようにすることも必要です。テーブル データは、PartitionKey (行の "コレクション" の識別子) と、RowKey (特定のパーティション内の個々の行を一意に識別) を使用して構成されます。PartitionKey と RowKey に使用するデータの選択は、テーブル ストレージを使用する際の、デザイン上の最も重要な決定事項の 1 つとなる可能性があります。
これらの機能により、ストレージ ノード間で負荷分散を行い、アプリケーションでスケーラビリティに関する組み込みのオプションを提供することが可能になります。データに関連するデータ センターのアフィニティにかかわらず、同じパーティション キーを持つテーブル ストレージ内の行は、同じ物理データ ストア内に保存されます。メッセージはユーザーごとに格納されるため、パーティション キーは UserName に、RowKey は各行を識別する GUID になります (図 4 参照)。
図 4 テーブル ストレージのラッパー
namespace HollywoodHackers.Storage.Repositories
{
public class UserTextNotificationRepository : StorageBase
{
public const string EntitySetName =
"UserTextNotifications";
CloudTableClient tableClient;
UserTextNotificationContext notificationContext;
public UserTextNotificationRepository()
: base()
{
tableClient = new CloudTableClient
(StorageBase.TableBaseUri, StorageBase.Credentials);
notificationContext = new UserTextNotificationContext
(StorageBase.TableBaseUri,StorageBase.Credentials);
tableClient.CreateTableIfNotExist(EntitySetName);
}
public UserTextNotification[]
GetNotificationsForUser(string userName)
{
var q = from notification in
notificationContext.UserNotifications
where notification.TargetUserName ==
userName select notification;
return q.ToArray();
}
public void AddNotification
(UserTextNotification notification)
{
notification.RowKey = Guid.NewGuid().ToString();
notificationContext.AddObject
(EntitySetName, notification);
notificationContext.SaveChanges();
}
}
}
これでストレージ メカニズムを実装したので、次に、電子商取引サイトのバックグラウンドでメッセージを処理するエンジンとして機能する Worker ロールが必要です。これを行うために、Microsoft.ServiceHosting.ServiceRuntime.RoleEntryPoint クラスから継承するクラスを定義し、そのクラスをクラウド サービス プロジェクトの Worker ロールに関連付けます (図 5 参照)。
図 5 エンジンとして機能する Worker ロール
public class WorkerRole : RoleEntryPoint
{
ShoppingCartQueue cartQueue;
ToastQueue toastQueue;
UserTextNotificationRepository toastRepository;
public override void Run()
{
// This is a sample worker implementation.
//Replace with your logic.
Trace.WriteLine("WorkerRole1 entry point called",
"Information");
toastRepository = new UserTextNotificationRepository();
InitQueue();
while (true)
{
Thread.Sleep(10000);
Trace.WriteLine("Working", "Information");
ProcessNewTextNotifications();
ProcessShoppingCarts();
}
}
private void InitQueue()
{
cartQueue = new ShoppingCartQueue();
toastQueue = new ToastQueue();
}
private void ProcessNewTextNotifications()
{
CloudQueueMessage cqm = toastQueue.GetMessage();
while (cqm != null)
{
ToastQueueMessage message =
QueueMessageBase.FromMessage<ToastQueueMessage>(cqm);
toastRepository.AddNotification(new
UserTextNotification()
{
MessageText = message.MessageText,
MessageDate = DateTime.Now,
TargetUserName = message.TargetUserName,
Title = message.Title
});
toastQueue.DeleteMessage(cqm);
cqm = toastQueue.GetMessage();
}
}
private void ProcessShoppingCarts()
{
// We will add this later in the article!
}
public override bool OnStart()
{
// Set the maximum number of concurrent connections
ServicePointManager.DefaultConnectionLimit = 12;
DiagnosticMonitor.Start("DiagnosticsConnectionString");
// For information on handling configuration changes
// see the MSDN topic at
//http://go.microsoft.com/fwlink/?LinkId=166357.
RoleEnvironment.Changing += RoleEnvironmentChanging;
return base.OnStart();
}
private void RoleEnvironmentChanging(object sender, RoleEnvironmentChangingEventArgs e)
{
// If a configuration setting is changing
if (e.Changes.Any(change => change is RoleEnvironmentConfigurationSettingChange))
{
// Set e.Cancel to true to restart this role instance
e.Cancel = true;
}
}
}
Worker ロールのコードについて説明していきましょう。必要なキューとテーブル ストレージを初期化して設定したら、コードはループに入ります。10 秒ごとに、キュー内のメッセージを処理します。処理ループを実行するたびに、(キューが空になったことを示す) null が最終的に返されるまで、キューからメッセージを取得します。
繰り返しになりますが、キューには 20 個を越えるメッセージを格納することはできません。キュー メッセージがタイムアウトしたと見なされてキュー内のそのメッセージが他の Worker から見えるようになり、処理できるようになる前に、キューで処理実行するあらゆる操作が、各キュー メッセージで重要な処理を実行する時間は限られています。各メッセージは、ユーザー通知としてテーブル ストレージに追加されます。Worker ロールに関して覚えておく必要がある重要な点は、エントリ ポイント メソッドが完了すると、その Worker ロールが終了することです。ロジックをループ内で実行する必要があるのはこのためです。
クライアント側では、JSON メッセージを返せるようにするため、jQuery から非同期にポーリングを行い、新しいユーザー通知を表示できるようにする必要があります。これを行うために、メッセージ コントローラーにいくつかコードを追加して、通知にアクセスできるようにします (図 6 参照)。
図 6 JSON メッセージを返す
public JsonResult GetMessages()
{
if (User.Identity.IsAuthenticated)
{
UserTextNotification[] userToasts =
toastRepository.GetNotifications(User.Identity.Name);
object[] data =
(from UserTextNotification toast in userToasts
select new { title = toast.Title ?? "Notification",
text = toast.MessageText }).ToArray();
return Json(data, JsonRequestBehavior.AllowGet);
}
else
return Json(null);
}
Visual Studio 2010 ベータ 2 (この記事の執筆時の環境) の ASP.NET MVC 2.0 では、JsonRequestBehavior.AllowGet オプションを使用しないで JSON データを jQuery やその他のクライアントに返すことができません。ASP.NET MVC 1.0 では、このオプションが不要です。ここで、15 秒ごとに GetMessages メソッドを呼び出し、通知をトースト形式のメッセージとして表示する JavaScript を作成します (図 7 参照)。
図 7 トースト形式のメッセージとして表示される通知
$(document).ready(function() {
setInterval(function() {
$.ajax({
contentType: "application/json; charset=utf-8",
dataType: "json",
url: "/SystemMessage/GetMessages",
success: function(data) {
for (msg in data) {
$.gritter.add({
title: data[msg].title,
text: data[msg].text,
sticky: false
});
}
}
})
}, 15000)
});
ショッピング カートの送信と処理
このサンプル アプリケーションでは、キュー ストレージを使用して有効にするもう 1 つの主要なシナリオとして、ショッピング カートの送信がありました。エンジンがショッピング カートでいくつか処理を実行する必要があるため、Hollywood Hackers には、サード パーティ製のフルフィルメント システムが搭載されています (小さな倉庫にすべての道具を保管することはできません)。処理が完了すると、エンジンはユーザーの通知キューにメッセージを送信し、ショッピング カートが処理されたこと (または問題が発生したこと) をそのユーザーに通知します。ショッピング カートが処理されたときにユーザーがオンライン状態であれば、システムからポップアップ形式のトースト メッセージを受信します。オフライン状態であれば、そのユーザーがサイトに次回ログオンするときに、ポップアップ メッセージを受信します (図 8 参照)。
図 8 ユーザー通知のサンプル
まず、ショッピング カートのキューを操作できるように、いくつかラッパー クラスが必要です。こうしたラッパーは実に単純です。このラッパーのソース コードは、CodePlex サイトから入手できます。
標準の CRUD (作成、読み取り、更新、削除) のリポジトリとは異なり、キューでの読み取り操作は単純ではありません。キューからメッセージを取得するときは、常に、そのメッセージを処理し、操作が失敗するか、処理を完了してメッセージを削除するまでの期間が限られていることを思い出してください。こうしたパターンはリポジトリのパターンには適切に変換されないため、ラッパー クラスに抽象化することはしませんでした。
これで、ショッピング カートのキューを操作するコードができたので、ショッピング カートのコントローラーにいくつかのコードを追加して、カートの中身をキューに送信します (図 9 参照)。
図 9 キューへのショッピング カートの送信
public ActionResult Submit()
{
ShoppingCartMessage cart = new ShoppingCartMessage();
cart.UserName = User.Identity.Name;
cart.Discounts = 12.50f;
cart.CartID = Guid.NewGuid().ToString();
List<ShoppingCartItem> items = new List<ShoppingCartItem>();
items.Add(new ShoppingCartItem()
{ Quantity = 12, SKU = "10000101010",
UnitPrice = 15.75f });
items.Add(new ShoppingCartItem()
{ Quantity = 27, SKU = "12390123j213",
UnitPrice = 99.92f });
cart.CartItems = items.ToArray();
cartQueue.AddMessage(cart);
return View();
}
実際のシナリオでは、セッション ストア、キャッシュのようなプロセス外の状態や、フォーム ポストからショッピング カートを取得することになります。今回の記事ではコードを単純にするため、カートの中身を模擬的に作成します。
最後に、キューに配置したショッピング カートの中身を使って、キューを定期的にチェックし保留中のカートを検出するよう、Worker ロールを変更できます。これにより、各カートをキューから 1 つずつ取り出して、それを 1 分間かけて処理し、ショッピング カートが処理されたことをユーザーに通知するメッセージをユーザーの通知キューに送信します (図 10 参照)。
図 10 キューをチェックして保留中のショッピング カートを検出
private void ProcessShoppingCarts()
{
CloudQueueMessage cqm = cartQueue.GetMessage();
while (cqm != null)
{
ShoppingCartMessage cart =
QueueMessageBase.FromMessage<ShoppingCartMessage>(cqm);
toastRepository.AddNotification(new UserTextNotification()
{
MessageText = String.Format
("Your shopping cart containing {0} items has been processed.",
cart.CartItems.Length),
MessageDate = DateTime.Now,
TargetUserName = cart.UserName
});
cartQueue.DeleteMessage(cqm);
cqm = cartQueue.GetMessage();
}
}
キュー メッセージが適切に処理されてユーザーの通知テーブルに追加されると、マスター ページに配置されている jQuery Gritter コードは、次の 15 秒間のポーリング サイクルで新しいメッセージを検出し、ショッピング カートに関するトースト形式の通知をユーザーに表示します。
まとめと今後のステップ
今回の記事の目的は、開発者に物理的なデータセンターというお気に入りの毛布を脱ぎ捨ててもらい、Windows Azure を使用して単純な "Hello World" Web サイトの作成よりも複雑な処理を実行できることを認識してもらうことです。Windows Azure キューと Windows Azure テーブル ストレージの機能があれば、アプリケーションと (複数の) Worker ロール間での非同期メッセージングにこれらの機能を活用することにより、Windows Azure によりアプリケーション エンジンを適切に強化することができます。
わかりやすく読みやすい記事にするために、かなりのコードをリファクタリングせずにそのまま残しました。新しい Windows Azure の機能を試す演習として、この記事のコードの一部をリファクタリングすることで、キューをより汎用的に使用できるようにするだけでなく、あらゆる ASP.NET MVC Web サイトで非同期のメッセージングと通知を実行するのに必要なすべてのコードを含む、スタンドアロンのアセンブリを作成してみてください。
重要なのは、演習に本気で取り組み、いくつかサイトを作成して、実行可能な処理を確認することです。今回の記事のコードは、CodePlex サイトの「Hollywood Hackers」(hollywoodhackers.codeplex.com、英語) で確認できます。
新しいテクノロジに関する著者のブログや熱弁については、exclaimcomputing.com (英語) を参照してください。Kevin Hoffman と Nate Dudek は、クラウド向けソリューションの開発を専門とする企業 Exclaim Computing の共同創設者です。