Azure App ServiceとRedisキャッシュによる負荷分散環境での効率的なセッション管理の実現方法
従来の負荷分散環境におけるセッションの課題
一般的に、Webアプリケーションではインメモリセッション(データをRAM上に保存する方式)が広く使われています。専用VMや共有ホスティングプランでアプリケーションをホストする従来型の環境では、この方式でほとんど問題なく動作します。
しかし、トラフィックが増加すると、複数のWebサーバーを用意し、ロードバランサーでトラフィックを振り分けるスケールアウト構成を検討することになります。この場合、同一セッションに関連するリクエストが複数のサーバーに振り分けられる可能性があるため、インメモリセッションは正しく機能しません(同じサーバーが1つのセッションのすべてのリクエストを処理することも可能ですが、推奨されません)。そこでの解決策として、負荷分散環境内のすべてのWebサーバーからアクセス可能なSQL Serverなどにセッションを保存する方法が考えられます。
Azure自動スケール環境でのセッション検証
理論的な議論にとどまらず、セッションを使用する小さなプログラムを実際に作成して、Azure上でセッションがどのように動作するかを確認してみましょう。
まず、以下のようなWebページを持つWebサイトを作成します(MVCアプリケーションでも構いません)。デプロイ手順については、過去の記事「Azure App Service – Visual Studioから既存アプリケーションを構成」を参照してください。
Login.aspx
このページではユーザー名とパスワードを受け付けます。「Login」ボタンがクリックされると、セッションを作成し、ログインしたユーザー名をセッションに保存して、「Default.aspx」へリダイレクトします。
なお、このページにはWebサイトがホストされている仮想マシンのIPアドレス「10.202.116.91」も表示されます。
Default.aspx
このページは、セッションの値に応じて以下のようなメッセージを表示します。
セッションに値が存在する場合は、「Logged in user as admin. You are in 10.202.116.91」と表示されます。一方、セッションに値がない場合は、「Session is NULL. You are in 10.202.116.91」と表示されます。
ここで注目すべきは、Login.aspxとDefault.aspxの両方で表示されるサーバーのIPアドレスが同じである点です。つまり、すべてのリクエストが同じサーバーに転送されています。
また、下記のスクリーンショットのように、セッションは「ASP.NET_SessionId」という名前のCookieによって維持されています。これは多くの方にとって馴染みのある仕組みでしょう。
インスタンス数を2にスケールしてみる
ここで、Webサイトへのトラフィックが増加し、2インスタンスへスケールしたい状況を想定します。インスタンス数を2に増やしてみましょう。
注意: フリー(Free)および共有(Shared)プランではインスタンス数を増やすことはできません。App Serviceが「Basic」「Standard」「Premium」いずれかのプランである必要があります。
再びログインページにアクセスし、ページを何度か更新してみてください。何回更新してもIPアドレスは変わりません。
つまり、2インスタンスへのスケールを有効化しても、App Serviceは依然として1台のサーバーからリクエストに応答しているのです。
実運用では、これが問題になる可能性があります。ロードバランシングが有効になっているにもかかわらず、同じサーバーだけがリクエストを処理し、他のサーバーが十分に活用されていないからです。
すべてのリクエストが同じサーバーに送られる理由は、後述のARR Cookieにあるためです。
簡単に説明すると、このCookieには最初のリクエストを処理したサーバーの情報が含まれており、以降の同一セッションからのリクエストはすべて同じVMで処理される仕組みになっています。
ARRアフィニティの無効化
それでは、App Serviceの「アプリケーション設定」からARR Cookie機能を無効にしてみましょう。
画面のとおり「ARR Affinity」をオフにし、「保存」ボタンをクリックして変更を保存します。その後、すべてのセッションをクリアするためにApp Serviceを再起動してください。
重要な注意点
ARR Affinityをオフにすると、ARRAffinity Cookieの生成プロセスが無効になります。Cookieが無効になると、リクエストは利用可能な任意のサーバーに送信される可能性があります。そのため、セッションが正しく維持される保証はありません。セッションが機能するのは、リクエストが同じサーバーから処理された場合のみです。もしリクエストがセッションが格納されているサーバー以外で処理されると、セッションはNULLになってしまいます。
さて、Webアプリに戻り、ログインページに移動すると、新しいIPアドレス「10.202.174.84」が表示されるはずです。(最初は以前と同じIPアドレスが表示される場合もあります。その場合はページを更新すると変わります。筆者の場合は2回更新して新しいIPアドレスを確認できました。)
ただし、「Login」ボタンをクリックしても、フォームが必ず10.202.174.84に送信される保証はありません。別のサーバー(筆者の場合は「10.202.116.91」)にデータが送信される可能性もあります。
実際に、上記の状態で「Login」ボタンをクリックすると、Defaultページに次の値が表示されました。
- IPアドレス: 10.202.174.84(ログインページと同じ)
- セッション: NULL。理由は、「Login」クリック時のリクエストが別のインスタンス(「10.202.116.91」)に送られ、そこにセッションが保存されていたため。
- ページを数回更新すると、次の結果が得られます。
ここから以下のことが確認できます。
- セッションに値「admin」が存在する。
- IPアドレスは10.202.116.91。
結論として、自動スケール機能でロードバランサーを構成した場合、Azure App Serviceではセッションは期待どおりに機能しないということです。
解決策:Redisキャッシュプロバイダー
ここで救世主となるのが、Redisキャッシュプロバイダーです。Azure公式サイトには以下のように定義されています。
「Azure Redis Cacheは、広く普及しているオープンソースのRedisキャッシュをベースとしています。Microsoftが管理する安全な専用Redisキャッシュへのアクセスを提供し、Azure内の任意のアプリケーションから利用できます。」
セッションを期待どおりに動作させるために必要な手順は以下のとおりです。
- Azure管理ポータルからRedisキャッシュを作成する。
- アプリケーションを構成してAzure Redis Cacheを使用できるようにする。
- セッションを利用する。
ステップ1: Azure管理ポータルでRedisキャッシュを作成
Azure管理ポータルを使ってRedisキャッシュの作成を始めましょう。
Redisキャッシュの詳細情報(DNS名、サブスクリプション、リソースグループ、場所、価格レベルなど)を入力します。「作成」ボタンをクリックすると、Redisキャッシュの作成には数分かかります。
ステップ2: アプリケーションをAzure Redis Cache向けに構成
次に、作成したRedisキャッシュを使用するようアプリケーションを構成します。
Package Manager Consoleを開き、コマンド「Install-Package StackExchange.Redis」を入力してEnterキーを押します。
これで必要なパッケージのインストールが完了しました(なお、.NET Frameworkは4以上である必要があります)。
アセンブリを使用するためには、まずLogin.aspxページとDefault.aspxページの両方に、次の名前空間を追加します。
using StackExchange.Redis;
続いて、「RedisConnection」という新しいクラスをプロジェクトに追加します(添付のプロジェクトを参照してください)。
Redisキャッシュに接続するには、ConnectionMultiplexer.Connect関数に以下の情報を渡す必要があります。これらはポータルから取得できます。
- Redis Cache URL
- キー(アクセスキー)
注意: キーをソースコード内に直接保存することは避けてください。本記事では説明を簡単にするためコード内に記載していますが、実際の開発では、資格情報を安全に保存・管理する方法(App Settings、Key Vaultなど)を活用してください。
ステップ3: Redisキャッシュを使ったセッションの実装
これでRedisキャッシュに接続できるクラスが準備できました。このクラスを使って、Redisキャッシュにセッションを作成してみましょう。
Login.aspx.csファイルを開き、次のコード行を置き換えます。
Session["login"] = this.txtUsername.Text.Trim();
置き換え後のコード:
IDatabase cache = RedisConnection.Connection.GetDatabase();
cache.StringSet("login", this.txtUsername.Text.Trim());
次に、Default.aspx.csを開き、以下のコードを置き換えます。
protected void Page_Load(object sender, EventArgs e)
{
if (Session["login"] == null)
{
Response.Write("Session is NULL. You are in " + Request.ServerVariables["LOCAL_ADDR"]);
}
else
{
Response.Write("Logged in user is " + Session["login"] + ". You are in " + Request.ServerVariables["LOCAL_ADDR"]);
}
}
置き換え後のコード:
protected void Page_Load(object sender, EventArgs e)
{
IDatabase cache = RedisConnection.Connection.GetDatabase();
string strLoginValue = cache.StringGet("login");
if (strLoginValue == null)
{
Response.Write("Session is NULL. You are in " + Request.ServerVariables["LOCAL_ADDR"]);
}
else
{
Response.Write("Logged in user is " + strLoginValue + ". You are in " + Request.ServerVariables["LOCAL_ADDR"]);
}
}
動作確認
コードをAzure App Serviceにデプロイし、変更内容を確認したら、ログインページに移動します。
IPアドレスは「10.202.174.84」です。ここで「Login」ボタンをクリックすると、Default.aspxページに遷移します。
注目すべきは、IPアドレスが異なるにもかかわらず、ユーザー名「admin」が表示されている点です。これは、セッションが両方のインスタンスからアクセス可能な分散型の場所「Redisキャッシュ」に保存されているためです。
さらに、ページを何度も更新してみてください。IPアドレスは変化しますが、ユーザー名の値は変わりません。
これで完了です。負荷分散環境においてセッションを適切に保存・管理する方法を学びました。Redisキャッシュには、セッション情報だけでなく、あらゆる種類のデータを保存できます。
本記事がお役に立てば幸いです。フィードバックをお待ちしております。
-
Redis TLS – Redis Enterprise 6.2.4のノード間暗号化を徹底解説
Redis Enterprise 6.2.4では、ノード間暗号化(Internode Encryption)機能が導入されました。この機能の目的は、Redisクラスター内のノード同士をつなぐすべての内部接続にTLS暗号化を実現することです。具体的には以下が対象となります。 コントロールプレーン接続の強化:CCS(Cluster Configuration Store)レプリケーションの暗号化レプリカノードからプライマリノードのCCSへのすべての接続データプレーン接続:ノード間のシャードレプリケーションの暗号化ノード間におけるプロキシとシャード間のすべての接続 Redis Enterprise
-
Redisで整数値をインクリメントする方法 – INCR・INCRBYコマンドの使い方
このチュートリアルでは、Redisデータストア内のキーに保存された整数値を表す文字列を、INCRコマンドとINCRBYコマンドを使ってインクリメント(数値を増加させる)する方法を解説します。アクセスカウンターやいいね数の管理など、カウンター機能の実装で頻繁に使われる重要なコマンドなので、しっかり理解しておきましょう。 INCRコマンドとは INCRコマンドは、指定したキーに保存されている整数値を表す文字列を「1」だけ増加させます。主な仕様は以下の通りです。 キーが存在しない場合は、まず新しく作成され、値が「0」に設定された上でインクリメントが実行されます(結果として「1」が返ります)。 キー