Azure Web Sites

Azure Web Sites による Web アプリケーションのスケール変換

Yochay Kiriaty

コード サンプルのダウンロード

意外にも、Web アプリケーション開発では、アプリケーションのスケールが見過ごされることがよくあります。通常、Web アプリケーションのスケールを問題にするのは、物事がうまくいかなくなり始め、プレゼンテーション層での遅延やタイムアウトによりユーザー エクスペリエンスに影響が出始めた場合のみです。Web アプリケーションでこのようなパフォーマンスの低下が現れたら、スケーラビリティを考える時期がきています。この時点では、コードの論理的なバグではなく、CPU、メモリ、帯域幅などのリソースの不足により、アプリケーションの機能に影響が出ています。

この時点で、Web アプリケーションのスケーラビリティを考え、コンピューティング リソースやストレージを追加するか、データベース バックエンドを強化します。クラウドで最もよく利用されるスケール変換の形式は水平方向のスケール変換です。つまり、コンピューティング インスタンスを新たに追加する形式です。インスタンスを追加すると、Web アプリケーションは複数の Web サーバー (インスタンス) 上で同時に実行できるようになります。Microsoft Azure などのクラウド プラットフォームでは、Web アプリケーションのサポート基盤となるインフラストラクチャを非常に簡単にスケール変換できます。クラウド プラットフォームでは、多数の Web サーバーを仮想マシン (VM) の形式で簡単に追加できます。とは言え、Web アプリケーションが複数のインスタンスにスケール変換して実行するように設計されていなければ、追加されたリソースを利用することはできないので、期待する結果は得られません。

今回は、Web アプリケーションをスケール変換するための主な設計概念と設計パターンを取り上げます。ここで示す実装の詳細と例は、Microsoft Azure Web Sites で実行される Web アプリケーションに重点を置いています。

説明に入る前に、Web アプリケーションのスケール変換は、コンテキストと、アプリケーションの設計方法に大きく依存することに注意が必要です。今回使用する Web アプリケーションはシンプルですが、Web アプリケーションのスケール変換の基本的な部分を扱っていて、特に Azure Web Sites での実行時のスケールに対処します。

さまざまなビジネス ニーズに応じて、スケールのレベルもさまざまです。ここでは、複数のインスタンスで実行できない Web アプリケーションから、複数のインスタンス (地理上の複数の地域やデータセンター) にスケール変換できる Web アプリケーションまで、4 つのレベルのスケール変換機能を見ていきます。

ステップ 1: アプリケーションの紹介

まず、サンプル Web アプリケーションの制限事項の確認から始めます。このステップでは、アプリケーションのスケーラビリティを強化するために必要な変更を行う基準を設けます。まったく新しいアプリケーションを作成したりゼロから設計するのではなく、実際の現場でよく行われる既存のアプリケーションの変更というシナリオで進めます。

今回使用するアプリケーションは、ASP.NET Web ページ向けの WebMatrix フォト ギャラリー テンプレート (bit.ly/1llAJdQ、英語) です。このテンプレートは、ASP.NET Web ページを使用して実際の Web アプリケーションを作成する方法を習得するのに優れた手段です。これは完全に機能する Web アプリケーションで、ユーザーがフォト アルバムを作成して画像をアップロードできるようにします。画像の表示はだれでも可能で、ログインすればコメントを残すこともできます。フォト ギャラリー Web アプリケーションは、WebMatrix から Azure Web Sites に配置することも、Azure Web Sites ギャラリーを介して Azure ポータルから直接配置することもできます。

この Web アプリケーションのコードをよく見ると、アプリケーションのスケーラビリティを制限するアーキテクチャ上の重大な問題が少なくとも 3 つ明らかになります。まず、データベースとしての ローカル SQL Server Express を使用しています。次に、インプロセス (Web サーバーのローカル メモリ) のセッション状態を使用しています。さらに、ローカル ファイル システムを使用して写真を保存しています。

ここからは、これらの制限事項をそれぞれ詳しく確認していきます。

App_Data フォルダーにある PhotoGallery.sdf ファイルは、アプリケーションと共に配布される既定の SQL Server Express データベースです。SQL Server Express により、アプリケーションの開発に着手しやすくなり、最高の学習機会を得られますが、アプリケーションのスケール変換能力は大きく制限されます。SQL Server Express データベースは、実際にはファイル システムの 1 つのファイルです。フォト ギャラリー アプリケーションは、このままの状態では複数のインスタンスに安全にスケール変換することはできません。複数のインスタンスにスケール変換を試みると、SQL Server Express データベース ファイルが各インスタンスにローカル ファイルとして存在し、それぞれが同期されていない状態で複数のファイルとして残る可能性があります。Web サーバーのすべてのインスタンスが同じファイル システムを共有するとしても、SQL Server Express ファイルはそれぞれ別のタイミングでいずれかのインスタンスによってロックされることになり、他のインスタンスはアクセスに失敗することになります。

フォト ギャラリー アプリケーションは、ユーザーのセッション状態を管理する方法によっても制限を受けます。セッションは、一定期間内に同一ユーザーから発行される一連の要求と定義され、セッション ID を一意ユーザーと関連付けることにより管理されます。この ID は、以降の各 HTTP 要求に使用され、Cookie、または要求 URL の特殊なフラグメントの形式でクライアントから提供されます。サーバー側では、このセッション データがインプロセス メモリ、SQL Server データベース、ASP.NET State Server など、サポートされているセッション状態ストアの 1 つに保存されます。

フォト ギャラリー アプリケーションは WebMatrix WebSecurity クラスを使用してユーザーのログインと状態を管理します。WebSecurity は、既定の ASP.NET メンバーシップ プロバイダーのセッション状態を使用します。既定では、ASP.NET メンバーシップ プロバイダーのセッション状態モードはインプロセス (InProc) です。このモードでは、セッション状態の値と変数は、ローカル Web サーバー インスタンス (VM) のメモリに格納されます。ユーザーのセッション状態を Web サーバーごとにローカルに保存している場合は、アプリケーションが複数のインスタンスで動作する能力が制限されます。これは、1 人のユーザーからの後続の HTTP 要求が Web サーバーの別のインスタンスに到達する可能性があるためです。Web サーバーの各インスタンスは状態のコピーをそのインスタンス自体のローカル メモリに保持するため、同じユーザーであっても、インスタンスが異れば InProc セッション状態も異なることになります。その結果、予期しないユーザー エクスペリエンスや一貫性のないユーザー エクスペリエンスが生じる場合があります。以下に、ユーザーの状態管理に使用されている WebSecurity クラスを示します。

_AppStart.cshtml

@{
  WebSecurity.InitializeDatabaseConnection
    ("PhotoGallery", "UserProfiles", "UserId", "Email", true);
}

Upload.cshtml

@{
  WebSecurity.RequireAuthenticatedUser();
    ...
...
}

WebSecurity クラスはヘルパー、つまり ASP.NET Web ページでのプログラミングを簡単にするコンポーネントです。WebSecurity クラスは背後で ASP.NET メンバーシップ プロバイダーと対話し、セキュリティ タスクの実行に必要な下位レベルの作業を実行します。ASP.NET Web ページの既定のメンバーシップ プロバイダーは SimpleMembershipProvider クラスで、既定のセッション状態モードはインプロセスです。

最後に、フォト ギャラリー Web アプリケーションの最新バージョンでは、写真をバイトの配列としてデータベースに保存しています。基本的に、アプリケーションは SQL Server Express を使用しているため、写真はローカル ディスクに保存されます。フォト ギャラリー アプリケーションの主要シナリオの 1 つが写真の表示です。そのため、写真に関する多くの要求の処理と表示が必要になります。したがって、データベースからの写真を読み取るのは感心しません。SQL Server や Azure SQL Database など、データベースを高度にしても変わりません。写真を取得する処理は大半は負荷のかかる操作です。

要するに、このバージョンのフォト ギャラリーはステートフルなアプリケーションです。ステートフルなアプリケーションは、複数のインスタンスへのスケール変換が適切に行われません。

ステップ 2: フォト ギャラリーをステートレス Web アプリケーションに変更する

フォト ギャラリー アプリケーションのスケール変換に関する制限事項を説明したので、これを 1 つずつ解決し、アプリケーションのスケーラビリティを強化します。ステップ 2 では、フォト ギャラリーをステートフルからステートレスに変換するのに必要な変更を加えます。このステップの終了時には、更新したフォト ギャラリー アプリケーションが、複数の Web サーバー インスタンス (VM) に安全にスケール変換されて実行されるようになります。

最初に、SQL Server Express をより強力なデータベース サーバーの Azure SQL Database に置き換えます。Azure SQL Database はマイクロソフトのクラウド ベースのサービスで、Azure Services Platform の一部としてデータ ストレージ機能を提供します。Azure SQL Database Standard SKU および Premium SKU は、ステップ 4. で使用するビジネス継続性に関する高度な機能を提供します。手始めに、データベースを SQL Server Express から Azure SQL Database へ単純に移行します。移行は簡単で、WebMatrix データベース移行ツールなどのツールを使用して SDF ファイルを Azure SQL Database 形式に変換するだけです。

データベースの移行時にスキーマも変更することをお勧めします。スキーマの変更は、少しであっても、アプリケーションのスケール変換能力に大きく影響するものがあります。

スキーマの変更は、テーブルの一部 (Galleries、Photos、UserProfiles など) の ID 列の型を INT から GUID に変換することから始めます。この変更は、ステップ 4. で、アプリケーションを複数のリージョンで実行するように更新し、データベースと写真のコンテンツの同期を確保するときに役立ちます。型を変更しても、アプリケーションのコードを変更する必要はありません。アプリケーションのすべての SQL クエリはそのままです。

次に、現在写真をバイト配列でデータベースに保存していますが、これを止めます。この変更では、スキーマとコードの両方に変更を加える必要があります。Photos テーブルから FileContents 列と FileSize 列を削除し、写真をディスクに直接保存し、保存した写真を区別する手段として写真 ID (ここでは GUID) を使用します。

以下のコードは、変更前の INSERT ステートメントを示しています (fileBytes と fileBytes.Length の両方が直接データベースに保存されます)。

db.Execute(@"INSERT INTO Photos
  (Id, GalleryId, UserName, Description, FileTitle, FileExtension,
  ContentType, FileSize, UploadDate, FileContents, Likes)
  VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10)",
  guid.ToString(), galleryId, Request.GetCurrentUser(Response), "",
  fileTitle, fileExtension, fileUpload.ImageFormat, fileBytes.Length,
  DateTime.Now, fileBytes, 0);

データベースを変更した後、上記のコードを以下のように変更します。

using (var db = Database.Open("PhotoGallery"))
{
  db.Execute(@"INSERT INTO Photos
  (Id, GalleryId, UserName, Description, FileTitle, FileExtension,
  UploadDate, Likes)
  VALUES (@0, @1, @2, @3, @4, @5, @6, @7)", imageId, galleryId,
  userName, "", imageId, extension,
  DateTime.UtcNow, 0);
}

ステップ 3. では、アプリケーションをさらに細かく変更し、Web サーバーのすべてのインスタンスがアクセスできる 1 つの場所 (共有ディスクなど) に写真を保存します。

ステップ 2. の最後に、インプロセスのセッション状態を使用しないように変更します。既に説明したように、WebSecurity は、ASP.NET メンバーシップ プロバイダーと対話するヘルパー クラスです。既定では、ASP.NET SimpleMembership のセッション状態モードはインプロセスです。SQL Server や ASP.NET State Server サービスなど、SimpleMembership と併用できるアウトプロセスのオプションはいくつかあります。この 2 つのオプションでは、セッション状態を Web サーバーの複数のインスタンスが共有し、サーバー アフィニティを回避できます。つまり、セッションを特定の Web サーバーに結び付ける必要がなくなります。

今回は、特にデータベースと Cookie を使用して、プロセス外で状態を管理します。ただし、基本的に複雑にならないように、ASP.NET ではなく独自の実装を使用します。実装では Cookie を使用し、セッション ID とその状態をデータベースに保存します。ユーザーがログインしたら、新しい GUID をセッション ID として割り当て、データベースに保存します。この GUID は、Cookie の形式でユーザーにも返されます。以下のコードは、ユーザーがログインするたびに呼び出される CreateNewUser メソッドを示しています。

private static string CreateNewUser()
{
  var newUser = Guid.NewGuid();
  var db = Database.Open("PhotoGallery");
db.Execute(@"INSERT INTO GuidUsers (UserName, TotalLikes) VALUES (@0, @1)",
  newUser.ToString(), 0);
  return newUser.ToString();
}

HTTP 要求に応答するときに、GUID を Cookie として HTTP 応答に埋め込みます。AddUser メソッドに渡すユーザー名は、上記の CreateNewUser 関数の結果を受けて、次のようになります。

public static class ResponseExtensions
{
  public static void AddUser(this HttpResponseBase response, 
    string userName)
  {
    var userCookie = new HttpCookie("GuidUser")
    {
      Value = userName,
      Expires = DateTime.UtcNow.AddYears(1)
    };
    response.Cookies.Add(userCookie);
  }
}

着信 HTTP 要求を処理するときは、まず、GUID で表されるユーザー ID を GuidUser Cookie から抽出します。次に、データベース内でそのユーザー ID (GUID) を探し、ユーザー固有の情報を抽出します。図 1 は、GetCurrentUser の実装の一部を示します。

図 1 GetCurrentUser

public static string GetCurrentUser(this HttpRequestBase request,
   HttpResponseBase response = null)
  {
    string userName;
    try
    {
      if (request.Cookies["GuidUser"] != null)
        {
          userName = request.Cookies["GuidUser"].Value;
          var db = Database.Open("PhotoGallery");
          var guidUser = db.QuerySingle(
            "SELECT * FROM GuidUsers WHERE UserName = @0", userName);
          if (guidUser == null || guidUser.TotalLikes > 5)
            {
              userName = CreateNewUser();
            }
        }
      ...
      ...
}

CreateNewUser と GetCurrentUser はどちらも RequestExtensions クラスに含まれています。同様に、AddUser は ResponseExtensions クラスに含まれています。どちらのクラスも ASP.NET 要求処理パイプラインに接続され、それぞれ要求と応答を処理します。

セッション状態を管理するこの方法は、安全ではなく認証を強制しないため、どちらかといえば未熟な方法です。ただし、この方法は、プロセス外でセッションを管理する利点を示し、スケール変換できます。独自のセッション状態管理を実装する際は、ASP.NET をベースにするかどうかに関係なく、認証を含む安全なソリューションと、返却する Cookie を暗号化する安全な方法を使用するようにしてください。

この時点で、更新したフォト ギャラリー アプリケーションは間違いなくステートレスな Web アプリケーションになりました。ローカル SQL Server Express データベースの実装を Azure SQL Database に置き換え、セッション状態の実装をインプロセスからアウトプロセスに変更して、Cookie とデータベースを使用することで、アプリケーションはステートフルからステートレスに正常に変換されました (図 2 参照)。

Logical Representation of the Modified Photo Gallery Application
図 2 変更後のフォト ギャラリー アプリケーションの論理表現

Web アプリケーションを確実にステートレスにするステップの実行は、おそらく、Web アプリケーションの開発中に最も重要な作業です。ユーザーの状態、データの破損、または機能の正確性に関する懸念なく、複数の Web サーバー インスタンスで安全に実行できることは、Web アプリケーションのスケール変換において最も重要な要素の 1 つです。

スタップ 3: 大きな効果があるその他の改善点

ステップ 2. でフォト ギャラリー Web アプリケーションに加えた変更によって、アプリケーションはステートレスになり、Web サーバーの複数のインスタンスに安全にスケール変換できるようになります。ここからは、さらに機能強化を行い、アプリケーションのスケーラビリティ特性をさらに向上させて、より少ないリソースでより多くの負荷を処理できるようにします。このステップでは、ストレージ方針を見直し、UX のパフォーマンスを向上する非同期設計パターンに対処します。

ステップ 2. で説明した変更の 1 つは、データベースではなく、Web サーバーのすべてのインスタンスからアクセスできる共有ディスクなど、集中管理される中央の場所に写真を保存することでした。Azure Web Sites のアーキテクチャでは、複数の Web サーバーで実行されている Web アプリケーションのすべてのインスタンスが同じディスクを共有します (図 3 参照)。

With Microsoft Azure Web Sites, All Instances of a Web Application See the Same Shared Disk
図 3 Microsoft Azure Web Sites では Web アプリケーションのすべてのインスタンスが同じ共有ディスクにアクセスする

フォト ギャラリー Web アプリケーションの観点から見た "共有ディスク"とは、ユーザーが写真をアップロードするときに、ローカル フォルダーのように見え、写真の保存先となる .../uploaded フォルダーのことです。ただし、画像がディスクに書き込まれるときは、HTTP 要求を処理する特定の Web サーバーの "ローカル" に保存されるのではなく、すべての Web サーバーがアクセスできる中央の場所に保存されます。したがって、すべてのサーバーが写真を共有ディスクに書き込むことができ、他のすべての Web サーバーがその写真を読み取ることができます。写真のメタデータはデータベースに保存され、アプリケーションはこのメタデータを使用して写真 ID (GUID) を読み取り、HTML 応答の一環として画像の URL を返します。以下のコードは view.cshtml の一部で、画像の表示を可能にするために使用するページです。

<img class="large-photo" src="@ImagePathHelper.GetFullImageUrl(photoId,
  photo.FileExtension.ToString())" alt="@Html.AttributeEncode(photo.FileTitle)" />

画像の HTML 要素のソースには、GetFullImageUrl ヘルパー関数の戻り値が設定されます。このヘルパー関数は、写真 ID とファイル拡張子 (.jpg、.png など) を受け取り、画像の URL を表す文字列を返します。

写真を中央の場所に保存すると、Web アプリケーションがステートレスになります。ただし、現在の実装では、Web アプリケーションを実行している Web サーバーの 1 つから直接提供される画像もあります。具体的には、各画像のソース URL は Web アプリケーションの URL を指します。その結果、画像自体は、Web アプリケーションを実行している Web サーバーの 1 つから提供されます。つまり、画像の実際のバイト列が、HTTP 応答として Web サーバーから送信されます。これは、Web サーバーが動的な Web ページの処理に加え、静的コンテンツ (画像など) も処理することを意味します。Web サーバーは、大きなスケールでは多くの静的コンテンツを処理できますが、CPU、IO、メモリなどのリソースに負担がかかります。写真などの静的コンテンツだけが Web アプリケーションを実行している Web サーバーから直接提供されるのではなく、どこか他の場所から提供されるようにすると、Web サーバーに到達する HTTP 要求の数を減らすことができます。その結果、Web サーバー上のリソースが解放され、さらに多くの動的 HTTP 要求を処理できるようになります。

まず、Azure BLOB ストレージ (bit.ly/TOK3yb、英語) を使用してユーザーの写真を保存および管理するように変更します。ユーザーが画像の表示を求めると、新しい GetFullImageUrl から返される URL は Azure BLOB を指すようになります。最終結果は以下の HTML のようになります (画像の URL は BLOB ストレージを指しています)。

<img class="large-photo" alt="764beb6b-1988-42d7-9900-03ee8a60749b"
  src="http://photogalcontentwestus.blob.core.windows.net/
  full/764beb6b-1988-42d7-9900-03ee8a60749b.jpg">

つまり、画像は、Web アプリケーションを実行している Web サーバーではなく、BLOB ストレージから直接提供されます。

これに対し、以下の HTML は、Azure Web Sites 共有ディスクに保存されている写真を示します。

<img class="large-photo" alt="764beb6b-1988-42d7-9900-03ee8a60749b"
  src="http:// builddemophotogal2014.websites.net/
  full/764beb6b-1988-42d7-9900-03ee8a60749b.jpg">

フォト ギャラリー Web アプリケーションは、完全な写真とサムネイルという 2 つのコンテナーを使用します。完全な写真のコンテナーは写真を元のサイズで保存しますが、サムネイル コンテナーはギャラリーのビューに表示する小さい画像を保存します。

public static string GetFullImageUrl(string imageId, 
  string imageExtension)
{
  return String.Format("{0}/full/{1}{2}",
    Environment.ExpandEnvironmentVariables("%AZURE_STORAGE_BASE_URL%"),
    imageId, imageExtension);
}

AZURE_STORAGE_BASE_URL は、Azure BLOB の基本 URL (この場合は http://photogalcontentwestus.blob.core.windows.net) を格納する環境変数です。この環境変数は、Azure ポータルの [Site Config] (サイト構成) タブで設定することも、アプリケーションの web.config に含めることもできます。ただし、Azure ポータルから環境変数を設定すると、再配置の必要なく簡単に変更できるため、柔軟性が向上します。

Azure Storage の使い方は、コンテンツ配信ネットワーク (CDN) とほぼ同じです。たいていの場合、画像の HTTP 要求はアプリケーションの Web サーバーからではなく、Azure Storage コンテナーから直接提供されるためです。この結果、常に Web サーバーに到達する静的 HTTP 要求トラフィックの量が大幅に減少するため、Web サーバーはより多くの動的要求を処理できるようになります。Azure Storage は平均的な Web サーバーよりもかなり多くのトラフィックを処理できます。1 つのコンテナーをスケール変換して、1 秒間に何万もの要求を処理するように調整できます。

静的コンテンツに BLOB ストレージを使用するだけでなく、Microsoft Azure CDN を追加することもできます。CDN はすべての静的コンテンツに対応するため、Web アプリケーションの上位に CDN を追加すると、さらにパフォーマンスが向上します。CDN に既にキャッシュされた写真に対する要求は BLOB ストレージには到達しません。さらに、CDN は、一般的にエンド ユーザーのより近くにあるエッジ サーバーを備えているため、感覚的なパフォーマンスも向上します。サンプル アプリケーションへの CDN の追加の詳細については今回説明しません。必要な変更点のほとんどは、DNS の登録と構成に関するものです。ただし、スケールに応じた運用に取り組んでいて、顧客が迅速かつ応答性に優れた UI を求めている場合は、CDN の使用を検討してください。

ユーザーがアップロードした画像を処理するコードは見直していませんが、この部分は、Web アプリケーションのパフォーマンスと UX の両方を強化する、基本非同期パターンに取り組むチャンスです。ステップ 4. で説明するように、これは異なる 2 つのリージョン間でのデータ同期にも役立ちます。

フォト ギャラリー Web アプリケーションに対して次に行う変更では、アプリケーション (Web サイト) のフロントエンドをバックエンドのビジネス ロジック (Web ジョブ + データベース) から分離する手段として、Azure Storage キューを追加します。アップロードのコードがフルサイズの画像をストレージに保存し、サムネイルを作成してストレージに保存し、SQL Server データベースを更新するときに、キューがなければフォト ギャラリーのコードは、フロントエンドとバックエンドの両方を処理し、その間、ユーザーは応答を待機することになります。しかし、Azure Storage キューを導入すれば、フロントエンドはメッセージをキューに書き込むだけですぐにユーザーに応答を返すようになります。バックグラウンド処理の Web ジョブ (bit.ly/1mw0A3w、英語) がキューからメッセージを取得し、バックエンドの必要なビジネス ロジックを実行します。フォト ギャラリーの場合、必要なビジネス ロジックには、画像の操作、適切な場所への画像の保存、データベースの更新などがあります。図 4 は、Azure Storage の使用とキューの追加を含め、ステップ 3. で行った変更を示しています。

Logical Representation of Photo Gallery Post Step Three
図 4 ステップ 3. 実行後のフォト ギャラリーの論理表現

キューを利用することにしたので、upload.cshtml のコードを変更する必要があります。以下のコードでは、画像を操作する複雑なビジネス ロジックを実行する代わりに、StorageHelper を使用してメッセージ (写真 ID、写真ファイルの拡張子、およびギャラリー ID を含むメッセージ) をキューに登録しています。

var file = Request.Files[i];
var fileExtension = Path.GetExtension(file.FileName).Trim();
guid = Guid.NewGuid();
using
var fileStream = new FileStream(
 Path.Combine( HostingEnvironment.MapPath("~/App_Data/Upload/"),
 guid + fileExtension), FileMode.Create))
{
  file.InputStream.CopyTo(fileStream);
  StorageHelper.EnqueueUploadAsync(
    Request.GetCurrentUser(Response),
     galleryId, guid.ToString(), fileExtension);
}

StorageHelper.EnqueueUploadAsync は、CloudQueueMessage を作成し、Azure Storage キューに非同期にアップロードしているだけです。

public static Task EnqueueUploadAsync
  (string userName, string galleryId, string imageId, 
    string imageExtension)
{
  return UploadQueue.AddMessageAsync(
    new CloudQueueMessage(String.Format("{0}, {1}, {2}, {3}",
    userName, galleryId, imageId,
    imageExtension)));
}

これで Web ジョブがバックエンドのビジネス ロジックを実行するようになります。Azure Web Sites の新しい Web ジョブ機能により、Web サイト上でサービスやバックグラウンド タスクなどのプログラムを簡単に実行できます。Web ジョブは、キューで変更をリッスンし、新しいメッセージを取得します。図 5 に示す ProcessUploadQueueMessages メソッドは、キューに少なくとも 1 つのメッセージがあるときに必ず呼び出されます。QueueInput 属性は、Azure Web Sites にバックグラウンド処理を追加するタスクを簡略化するフレームワーク、Microsoft Azure WebJobs SDK (bit.ly/1cN9eCx、英語) に含まれています。WebJobs SDK については今回説明しませんが、実際に知っておく必要があるのは、WebJobs SDK を使用すると、簡単にキュー (この例では uploadqueue) にバインドして着信メッセージをリッスンできることだけです。

図 5 キューからメッセージを読み取ってデータベースを更新する

public static void ProcessUploadQueueMessages
  ([QueueInput(“uploadqueue”)] string queueMessage, IBinder binder)
{
  var splited = queueMessage
    .Split(‘,’).Select(m => m.Trim()).ToArray();
  var userName = splited[0];
  var galleryId = splited[1];
  var imageId = splited[2];
  var extension = splited[3];
  var filePath = Path.Combine(ImageFolderPath, 
    imageId + extension);
  UploadFullImage(filePath, imageId + extension, binder);
  UploadThumbnail(filePath, imageId + extension, binder);
  SafeGuard(() => File.Delete(filePath));
  using (var db = Database.Open(“PhotoGallery”))
  {
    db.Execute(@”INSERT INTO Photos Id, GalleryId, UserName,
      Description, FileTitle, FileExtension, UploadDate, Likes)
      VALUES @0, @1, @2, @3, @4, @5, @6, @7)”, imageId,
      galleryId, userName, “”, imageId, extension, DateTime.UtcNow, 0);
  }
}

各メッセージをデコードし、入力文字列の各部分を切り出します。次に、メソッドは 2 つのヘルパー関数を呼び出し、画像を操作して BLOB コンテナーにアップロードします。最後に、データベースを更新します。

この時点で、更新したフォト ギャラリー Web アプリケーションは、1 日に何百万もの HTTP 要求を処理できるようになります。

ステップ 4: 世界規模に拡大する

ここまでフォト ギャラリー Web アプリケーションのスケール変換能力を大幅に強化してきました。アプリケーションは、Azure Web Sites でわずかな大型サーバーを使用して、何百万もの HTTP 要求を処理できるようになりました。現時点では、これらサーバーはすべて 1 つの Azure データセンターに存在しています。1 つのデータセンターで実行してもスケールに制限はありません。少なくとも標準的なスケールの定義では制限を受けていません。しかし、世界中の顧客をあまり待たせないようにするには、複数のデータセンターで Web アプリケーションを実行する必要があります。その結果、Web アプリケーションの持続性とビジネス継続性の能力も向上します。まれに 1 つのデータセンターでサービスの停止が発生することがありますが、Web アプリケーションは引き続き 2 番目の場所でトラフィックを処理するようになります。

このステップでは、複数のデータセンターで実行できるようにアプリケーションを変更します。ここでは、2 つの場所でアクティブ/アクティブ モードで実行することに注目します。どちらの場所で実行されているアプリケーションでも、ユーザーは写真を表示し、写真やコメントをアップロードできます。

フォト ギャラリー Web アプリケーションのコンテキストを考えると、ユーザー操作の大半が読み取り操作 (写真の表示) であることがわかります。新しい写真のアップロードやコメントの更新が含まれるのは、ごく少数のユーザー要求だけです。フォト ギャラリー Web アプリケーションの場合、読み取りと書き込みの比率は 95% 以上が読み取りであると言っても過言ではありません。このことから、いくつか仮説を立てることができます。たとえば、書き込み操作の応答が遅くても、最終的なシステム全体の応答性は許容されると考えられます。

重要なのは、このような仮説が状況に応じて変化すること、特定のアプリケーション固有の特性に依存していること、およびアプリケーションによって変わる可能性が高いことを理解しておくことです。

面倒な作業のほとんどはステップ 2. と 3. で完了しているため、2 つの場所からフォト ギャラリーを実行するために必要な作業の量は意外にもそれほど多くはありません。図 6 は、異なる 2 つのデータセンターから実行されるアプリケーション トポロジの大まかなブロック図を示しています。米国西部のアプリケーションが "メイン" のアプリケーションで、基本的にはステップ 3. 変更を加えたものです。米国東部のアプリケーションは "セカンダリ" サイトで、Azure Traffic Manager が両方の上位に配置されます。Azure Traffic Manager には複数の構成オプションがあります。[パフォーマンス] オプションを使用すると、Traffic Manager は、それぞれのリージョンの待機時間を両方のサイトで監視し、待機時間が短い方のサイトにトラフィックをルーティングするようになります。この場合、ニューヨーク (東海岸) の顧客は米国東部サイトにリダイレクトされ、サンフランシスコ (西海岸) の顧客は米国西部サイトにリダイレクトされます。どちらのサイトも同時にアクティブになり、トラフィックを処理します。一方のリージョンにあるアプリケーションでパフォーマンス上の問題が発生すると、理由の如何に関わらず、Traffic Manager は他のアプリケーションにトラフィックをルーティングします。データは同期されるため、失われることはありません。

Logical Representation of Photo Gallery Post Step Four
図 6 ステップ 4. 実行後のフォト ギャラリーの論理表現

米国西部のアプリケーションの変更を見てみます。コードの唯一の変更点は、キュー内のメッセージをリッスンする Web ジョブに対する変更です。Web ジョブは、写真を BLOB に保存するのではなく、ローカルと "リモート" の BLOB ストアに保存します。図 5 の UploadFullImage は、写真を BLOB ストレージに保存するヘルパー メソッドです。写真をリモート BLOB だけでなくローカル BLOB にもコピーするように、UploadFullImage の最後に ReplicateBlob ヘルパー関数を追加します。

private static void UploadFullImage(
  string imagePath, string blobName, IBinder binder)
{
  using (var fileStream = new FileStream(imagePath, FileMode.Open))
  {
    using (var outputStream =
      binder.Bind<Stream>(new BlobOutputAttribute(
      String.Format("full/{0}", blobName))))
    {
      fileStream.CopyTo(outputStream);
    }
  }
  RemoteStorageManager.ReplicateBlob("full", blobName);
}

以下のコードの ReplicateBlob メソッドには重要な 1 行があります。それは、StartCopyFromBlob メソッドを呼び出す最後の行です。この行は、BLOB のコンテンツ、プロパティ、およびメタデータすべてを新しい BLOB にコピーするようサービスに求めています (残りの部分は Azure SDK とストレージ サービスに処理させています)。

public static void ReplicateBlob(string container, string blob)
{
  if (sourceBlobClient == null || targetBlobClient == null)
    return;
  var sourceContainer = 
    sourceBlobClient.GetContainerReference(container);
  var targetContainer = 
    targetBlobClient.GetContainerReference(container);
  if (targetContainer.CreateIfNotExists())
  {
    targetContainer.SetPermissions(sourceContainer.GetPermissions());
  }
  var targetBlob = targetContainer.GetBlockBlobReference(blob);
  targetBlob.StartCopyFromBlob(sourceContainer.GetBlockBlobReference(blob));
}

米国東部では、ProcessLikeQueueMessages メソッドは何も処理しません。このメソッドは、メッセージを米国西部のキューにプッシュするだけです。メッセージは米国西部で処理され、画像はレプリケートされ、データベースは同期されます。この同期について少し説明します。

最後に残ったのは、データベースの同期についての説明です。データベースを同期するには、Azure SQL Database のアクティブ geo レプリケーション (連続コピー) プレビュー機能を使用します。この機能では、マスター データベースの読み取り専用のセカンダリ レプリカが保持されます。マスター データベースに書き込まれたデータは、自動的にこのセカンダリ データベースにコピーされます。マスターは読み取り/書き込みデータベースとして構成しますが、セカンダリ データベースはすべて読み取り専用で構成します。そのため、このシナリオでは、メッセージが米国東部のキューから米国西部にプッシュされます。(ポータルで) アクティブ geo レプリケーションを構成したら、データベースは同期された状態になります。ここまでに説明した点以外に、コードの変更は必要ありません。

まとめ

Microsoft Azure を使用すると、ほとんど手間をかけずにスケール変換できる Web アプリケーションをビルドできます。今回は、複数のインスタンスでは動作しないまったくスケール変換されない Web アプリケーションを、複数のインスタンスだけでなく、複数のリージョンでも実行でき、多数 (数千万) の HTTP 要求を処理するアプリケーションに、わずかなステップで変更する方法を説明しました。ここで紹介した例は、特定のアプリケーション固有のものですが、考え方は有効なので、任意の Web アプリケーションに実装できます。

Yochay Kiriaty は、Microsoft Azure チームの主任プログラム マネージャーで、Azure Web Sites に携わっています。連絡先は yochay@microsoft.com (英語のみ) です。Twitter は、twitter.com/yochayk (英語) でフォローできます。

この記事のレビューに協力してくれた Mohamed Ameen Ibrahim に心より感謝いたします。