MVC Movie アプリと Azure Redis Cache を 15 分で接続
このポストは、6 月 5 日に投稿した MVC movie app with Azure Redis Cache in 15 minutes の翻訳です。
新たに提供が開始されたプレビュー版の Azure Redis Cache (英語) は、お客様の Azure Web アプリと非常に簡単に接続できます。今回は、私が作成に参加している MVC Movie サンプル アプリ (英語) に Azure Redis Cache を接続し、Azure にデプロイして実行してみました。所要時間は 17 分以下 (そのうち 15 分は接続およびローカルでのテストの時間) でした。
キャッシュから読み込む際の速度は、データベースからの場合と比較して約 100 倍です。アクセス頻度の高いホット データをキャッシュからフェッチすることで、アプリの実行速度が改善されるだけでなく、データベースの負荷を軽減して他のクエリに対する応答速度を向上させることができます。
完成済みのサンプルは、こちらからダウンロードしていただけます。
ここからは、Azure Redis Cache を MVC Movie サンプル アプリに接続するために私が行った手順について説明します。
- Azure 管理ポータル (プレビュー) にログインして、新しい Cache の作成を選択します。
この処理に最大で 15 分ほどを要しますが、これは今回の所要時間には含めていません。手順の詳細については、「Azure Redis Cache の使用方法 (英語)」の記事を参照してください。ここで重要なのは、自分が Web Sites を作成した場所 (データ センター) と同じ場所に Cache を作成することです。試しに Web Sites を異なる場所に移動してみたところ、キャッシュの待機時間 (レイテンシ) は 25 倍に増加しました。詳細については、Azure Redis Cache の作成に関する記事をご覧ください。手順を開始するサンプル アプリとして、MVC Movie アプリ (英語) をダウンロードしてお使いいただけます。代わりに、完成済みのサンプルをダウンロードしてキャッシュのエンドポイント (URL) および認証情報を更新してもよいでしょう。その場合は以下の手順を理解してください。 - キャッシュの名前 (<ユーザー名>.redis.cache.windows.net) と、パスワードをコピーします (ポータルのプロパティ ブレードで鍵のボタンをクリックすると、キャッシュの名前とパスワードを表示できます)。
- NuGet パッケージの StackExchange.Redis を追加します。ここで、サンプル アプリをお使いの場合は、NuGet パッケージを復元する必要があります。
- パッケージ マネージャー コンソールで Update-Database というコマンドを実行します。NuGet パッケージを復元した後に、Visual Studio を再起動しないと Update-Database コマンドが表示されない場合があります。
- コントローラーに接続情報を埋め込みます。
public class MoviesController : Controller
{
private MovieDBContext db = newMovieDBContext();
private static ConnectionMultiplexer connection;
private static ConnectionMultiplexer Connection
{
get
{
if (connection == null || !connection.IsConnected)
{
connection = ConnectionMultiplexer.Connect(
"<キャッシュ名>.redis.cache.windows.net,ssl=true," +
"password=<パスワード>");
}
return connection;
}
}
警告: この例では説明を簡単にするためにソース コードに認証情報を記述していますが、認証情報をソース コードとして保存することは絶対にしないでください。認証情報を保存する方法については、アプリケーション文字列と接続文字列の機能性に関する記事を参照してください。
この接続は静的な値として保存されるため、要求ごとに接続を新規作成する必要はありません。Get メソッドを使用すると、接続が有効であるかどうかを確認できます。また、接続が切断された場合は再接続が実行されます。
SampleStackExchangeRedisExtensions クラスを含む新しいクラスを作成します。
public static class SampleStackExchangeRedisExtensions
{
public static T Get<T>(this IDatabase cache, string key)
{
return Deserialize<T>(cache.StringGet(key));
}
public static object Get(this IDatabase cache, string key)
{
return Deserialize<object>(cache.StringGet(key));
}
public static void Set(this IDatabase cache, string key, object value)
{
cache.StringSet(key, Serialize(value));
}
static byte[] Serialize(object o)
{
if (o == null)
{
return null;
}
BinaryFormatter binaryFormatter = new BinaryFormatter();
using (MemoryStream memoryStream = new MemoryStream())
{
binaryFormatter.Serialize(memoryStream, o);
byte[] objectDataAsStream = memoryStream.ToArray();
return objectDataAsStream;
}
}
static T Deserialize<T>(byte[] stream)
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
if (stream == null)
return default(T);
using (MemoryStream memoryStream = new MemoryStream(stream))
{
T result = (T)binaryFormatter.Deserialize(memoryStream);
return result;
}
}
}
SampleStackExchangeRedisExtensions クラスを使用すると、あらゆる型を簡単にシリアル化できます。次のように、[Serializable] 属性をモデルに追加する必要があります。
[Serializable]
public class Movie
Movie movie = db.Movies.Find(id); となっているインスタンスをすべて検索し、
次のように変更します。
//Movie movie = db.Movies.Find(id);
Movie movie = getMovie((int)id);
POST Edit および POST Delete の各メソッドで、下記の呼び出しによりキャッシュを削除することができます。
ClearMovieCache(movie.ID);
次のコードを Movie コントローラーに追加します。getMovie メソッドでは、標準のオンデマンドのキャッシュ アサイド手法を使用します。
Movie getMovie(int id)
{
Stopwatch sw = Stopwatch.StartNew();
IDatabase cache = Connection.GetDatabase();
Movie m = (Movie)cache.Get(id.ToString());
if (m == null)
{
Movie movie = db.Movies.Find(id);
cache.Set(id.ToString(), movie);
StopWatchMiss(sw);
return movie;
}
StopWatchHit(sw);
return m;
}
private void ClearMovieCache(int p)
{
IDatabase cache = connection.GetDatabase();
if (cache.KeyExists(p.ToString()))
cache.KeyDelete(p.ToString());
}
void StopWatchEnd(Stopwatch sw, string msg)
{
sw.Stop();
double ms = sw.ElapsedTicks / (Stopwatch.Frequency / (1000.0));
ViewBag.cacheMsg = msg + ms.ToString() +
” PID: ” + Process.GetCurrentProcess().Id.ToString();
}
void StopWatchMiss(Stopwatch sw)
{
StopWatchEnd(sw, “Miss – MS:”);
}
void StopWatchHit(Stopwatch sw)
{
StopWatchEnd(sw, “Hit – MS:”);
}
ViewBag.cacheMsg のコードを Views\Shared\_Layout.cshtml というファイルに追加し、各ページのタイミング情報を取得します。
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<h2>@ViewBag.cacheMsg</h2>
</footer>
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
</html>
これで、ローカルでのテストおよびタイミング情報の取得ができるようになりました。いったんデータがキャッシュに保存されると、そのままキャッシュに存在し続けます (私のデータベースは、メモリの負荷の問題によりすべてのデータを削除するだけの容量はありません)。デスクトップからクラウドへの方向でのパフォーマンスは、それほど高くはありません。サンプルをダウンロードして使用している場合は、 [ClearCache] ボタンをクリックして各キャッシュ エントリを削除できます。
ポータルからキャッシュを監視する
ポータルでは、キャッシュのヒットとミスの統計を取得できます。
[Monitoring] ボックスを右クリックしてダッシュボードにピン留めすると、ポータルにログインしているときにはいつでも確認できます。他に、削除されたキー、期限切れのキー、メモリの使用率、CPU などの指標も追加できます。
最新のデータを取得し監視対象を選択するには、 [monitoring] ボックスを右クリックして [Edit Query] を選択します。ここで監視対象のデータを選択できます。
アラートの設定は、この手順でチェックしたアイテムに対してのみ可能です。 [Monitoring] ボックスをダブル クリックすると、監視対象のアイテムの詳細な値が表示されます。 [Add Alert] ボタンをクリックすると、監視対象のアイテムでアラートを設定できます。次の図では、キーの削除について 15 分間の間隔で監視しています。削除量が大きいと、より大きなキャッシュが利用された可能性があることを示しています。
Visual Studio を使用すると、Azure への発行が簡単にできます。Web アプリを右クリックして [publish] を選択します。ここで、Web Sites のリージョンは、Cache を作成した場所と同じリージョンを選択することが重要です。Web Sites を Cache と異なるリージョンに作成して影響を試してみたところ、待機時間 (レイテンシ) は予想よりはるかに大きく増加しました。また、キャッシュのクライアント (この例では Web アプリ) と Cache のリージョンが異なると、データ転送の料金が発生する場合があります。[settings] タブで、必ず [Execute Code First Migrations] のチェックをオンにします。
これで、クラウドでアプリをテストする準備が完了しました。キャッシュの待機時間 (レイテンシ) が大幅に短縮されたことをご確認ください (ただし、Cache と Web Sites が同一のデータ センターである場合)。
キャッシュのストレス テスト
既定では、キャッシュ操作がタイムアウトするまでの時間は 1,000 ミリ秒 (1 秒) に設定されています。次のコードはタイムアウトまでの時間を増減するもので、キャッシュのタイムアウト例外処理を強制的に発生させ、コードがタイムアウト例外を適切に処理するかどうかを検証する際に使用します。 #define NotTestingTimeOut がコメント アウトされている場合、タイムアウトまでの時間は 150 ミリ秒に短縮され、高負荷時のタイムアウト例外が発生しやすくなります。
#else
#region StressTest
private static ConnectionMultiplexer Connection
{
get
{
if (connection != null && connection.IsConnected)
{
return connection;
}
var config = new ConfigurationOptions();
config.EndPoints.Add(Keys.URL);
config.Password = Keys.passwd;
config.Ssl = true;
config.SyncTimeout = 150;
connection = ConnectionMultiplexer.Connect(config);
return connection;
}
}
#endregion
#endif
ストレス テストの際には、セッションのキャッシュを無効化することを推奨します。次のコードを web.config ファイルに含めると、アプリ全体でセッションのキャッシュが無効化されます。
<sessionState mode="Off" />
また、コントローラーで [SessionState(SessionStateBehavior.Disabled)] を使用する方法もあります。次の更新された getMovie メソッドでは堅牢性が向上しいて、タイムアウト例外が発生した場合には最大 3 回再試行します。
Movie getMovie(int id, int retryAttempts = 0)
{
IDatabase cache = Connection.GetDatabase();
if (retryAttempts > 3)
{
string error = "getMovie timeout with " + retryAttempts.ToString()
+ " retry attempts. Movie ID = " + id.ToString();
Logger(error);
ViewBag.cacheMsg = error + " Fetch from DB";
// Cache unavailable, get data from DB
return db.Movies.Find(id);
}
Stopwatch sw = Stopwatch.StartNew();
Movie m;
try
{
m = (Movie)cache.Get(id.ToString());
}
catch (TimeoutException tx)
{
Logger("getMovie fail, ID = " + id.ToString(), tx);
return getMovie(id, ++retryAttempts);
}
if (m == null)
{
Movie movie = db.Movies.Find(id);
cache.Set(id.ToString(), movie);
StopWatchMiss(sw);
return movie;
}
StopWatchHit(sw);
return m;
}
このサンプル アプリには、キャッシュの負荷テストの際に呼び出すことができるメソッドが複数用意されています。
WriteCache および ReadCache の各メソッドでは、それぞれ 1 KB のサイズのアイテムの書き込みおよび読み込みが実行されます。オプションで、URL に "/n" を追加すると、読み書きするアイテムのサイズが n KB になります。たとえば、https://<サイト>.azurewebsites.net/Movies/ReadCache/3 という URL の場合、キャッシュ内のアイテムが 3 KB 読み込まれます。
タイムアウトを 150 ミリ秒に設定してキャッシュに頻繁にヒットさせると、キャッシュへの要求のほぼ半数を 3 回タイムアウトさせ、完全にデータベースにヒットするようにフォールバックさせることができました。私が設定した getMovie メソッドは、キャッシュへの要求の失敗を正常に処理し、データベースからデータを返して、ログに "getMovie timeout with 4 retry attempts. Movie ID = 3 Fetch from DB" という警告メッセージを記録しました。
本番用のアプリは、キャッシュへの要求の失敗を処理できるようにしておく必要があります。Basic (基本) レベルのキャッシュ (スレーブまたはフェールオーバー機能がない) をご利用の場合、1 か月に 1 回、ホストしている VM に修正プログラムが適用される際に数分間キャッシュが使用できなくなります。Standard (標準) レベルのキャッシュでは、マスターとスレーブ (フェールオーバー キャッシュ) が存在し、非常に高速なノンブロッキングでの初回同期および自動再接続機能をご利用いただけます。このため、キャッシュへの要求の失敗を適切に処理できるようにコードを作成する必要があります。
ASP.NET セッション状態プロバイダーの Azure Redis Cache (プレビュー)
セッション状態の使用を回避することが理想的ですが、アプリケーションによっては、セッション データの使用によりパフォーマンスと複雑さのバランスを調整したり、明確にセッション状態を要求したりする場合があります。インメモリのセッション状態プロバイダーでは、既定でスケール アウト (複数のインスタンスを持つ Web Sites を実行すること) は許可されていません。ASP.NET SQL Server のセッション状態プロバイダーでは、複数の Web Sites でセッション状態を使用することが許可されていますが、インメモリのプロバイダーと比較すると待機時間 (レイテンシ) が長くなります。しかし、代替策として Azure Redis Cache のセッション状態プロバイダーを使用すると、構成やセットアップが非常に簡単な上に、待機時間 (レイテンシ) も短くできます。アプリが使用するセッション状態の量が少ない場合は、キャッシュの容量のほとんどはデータのキャッシュに使用され、セッション状態に使用される容量は多くありません。
Azure Redis Cache のセッション状態プロバイダーを使用するには、NuGet パッケージの RedisSessionStateProvider を Web アプリに追加します (このとき、プレリリースを指定します。詳細についてはこちらの記事を参照してください)。ルートの Web.config ファイルに追加されたマークアップのホスト URL とキーを編集します。このとき、必ず ssl を true に設定します。
<system.web>
<customErrors mode="Off" />
<!--<sessionState mode="Off" />-->
<authentication mode="None" />
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
<sessionState mode="Custom" customProvider="RedisSessionProvider">
<add name="RedisSessionProvider"
type="Microsoft.Web.Redis.RedisSessionStateProvider"
port="6380"
host="movie2.redis.cache.windows.net"
accessKey="m7PNV60CrvKpLqMUxosC3dSe6kx9nQ6jP5del8TmADk="
ssl="true" />
<!--<add name="MySessionStateStore" type="Microsoft.Web.Redis.RedisSessionStateProvider" host="127.0.0.1" accessKey="" ssl="false" />-->
</providers>
</sessionState>
</system.web>
<system.webServer>
これで、Web アプリでセッション状態を使用する準備が完了しました。このサンプルでは、WriteCache アクションと ReadCache アクションのメニューのセッション状態 (および UI) が提供されます。WriteCache では、文字列形式の経路データを指定するオプションを使用できます。たとえば、https://<サイト>.azurewebsites.net/SessionTest/WriteSession/Hello_joe を指定した場合、セッション状態に "Hello_joe" が書き込まれます。アプリの全インスタンスで同一の Redis セッション キャッシュが使用されるため、固定セッションを使用する必要はありません。
この記事のご感想、および Azure Redis Cache に関する次回の記事へのご要望をお聞かせください。
また、私の Twitter アカウント (@RickAndMSFT) でも情報をご提供しますので、フォローしていただけますと幸いです。
関連情報
- Azure Redis Cache でのデータのキャッシュ
- Microsoft Azure Cache 拡張ライブラリ (英語): このライブラリでは、ユーザーのサービスおよびアプリケーションで共有可能な、高レベルのデータ構造の実装を提供しています。
- Azure Redis Cache に関する MSDN の記事