Windows Azure AppFabric ACS使ってフェデレーション認証するのはいいけど、味気ないサインイン画面で辟易してる方も多いんじゃないでしょうか。
↓味気ないサインイン画面
というわけで今日はこいつのカスタマイズをしたいと思います。
ゴールとして今回はASP.NET MVC3のログイン画面内で上記のサインイン画面(というかセレクタ画面)を埋め込んでアプリケーションでちゃんと(?)制御できるようにしたいと思います。
Windows Azure AppFabric ACS使ってフェデレーション認証するのはいいけど、味気ないサインイン画面で辟易してる方も多いんじゃないでしょうか。
↓味気ないサインイン画面
というわけで今日はこいつのカスタマイズをしたいと思います。
ゴールとして今回はASP.NET MVC3のログイン画面内で上記のサインイン画面(というかセレクタ画面)を埋め込んでアプリケーションでちゃんと(?)制御できるようにしたいと思います。
というわけでWindows Azure SDK 1.5がでましたね。
さくっとインストールしてみましょう。(※Visual Studio “11”やWindows 8ではTools for Visual Studioがインストールできませんでした。Visual Studio 2010が入ってたら行けそうですけど .NET Framework 3.5と4が必要なところでこけそうです・・・)
インストールはWeb Platform Installerからするのが楽ちんなのでそのようにします。
※念のために古いSDKやTools for VSはアンインストールしました。
注意点:Storage EmulatorのDBが更新されるので、必要なデータいれてる人はバックアップしておきましょう。古いDB削除されます。
Azureで検索したら Azure AppFabric SDK 1.5とWindows Azure Tools for Microsoft Visual Studio 2010 – 2011年8月(リリース日が2011/09/14)の2つがでるので追加しましょう。
依存関係はこんな感じです。
日本語期待した人はサヨウナラ・・・
でインストールするとAzure SDK 1.5とAzure AppFabric SDK 1.5、Azure Tools for VSが入ります。
※ASP.NET MVC3 Tools Update Installerが入ってなかったようなので一緒にはいりました。
Visual Studioで見るとちゃんと更新されてますね。
さて、インストールが終わったらStorage Emulatorを起動して初期化しましょう。
更新後はDBこんな感じに。
DevelopmentStorageDb20110816 が今回使ってるDBです。以前のは消えてますので注意。
Compute Emulatorもちゃんと新しくなってます。
あとAzure SDK 1.5ではパスワード(文字列)の暗号化用ツールが追加されました。
今まで手動で証明書指定したり暗号化用のコマンド叩いたり超面倒だったのが一発で!!!(
以前のバージョンで作ったプロジェクトを移行する場合は Microsoft.WindowsAzure.StorageClient.dll を 1.1.0.0 に更新しましょう。
またWindows Azure Connectのプラグインで利用している以下の設定が無くなりましたのでコンフィグから削除する必要があるようです。
<Setting name="Microsoft.WindowsAzure.Plugins.Connect.Diagnostics" value="" /> <Setting name="Microsoft.WindowsAzure.Plugins.Connect.DNSServers" value="" />
元ネタ:Windows Azure SDK Release Notes (September 2011)
どう変わったか等細かいところはMSDNにドキュメントがありますが、おいおいということで。
われらが(?) SmarxさんがWindows Azure上でMemcachedを動かすためのNuGet パッケージを作ってくださいました。
早速利用してみましょう。
性懲りもなく4回目。
@kazuk さんが「無駄な処理をしない事と、重い処理をバックグラウンドにする事」ということで、
削除処理を毎回するんじゃなくてある程度間隔あけて+別スレッドでやるべきだろJKとおっしゃられました。
というわけで @takepara さんのHighPerformanceSessionStateProviderに追加してみました。
↓結果
少しパフォーマンスよくなりました。ただMAXも2秒とかなので、平均して落ち着いた感。エラーもなくなってますね。
クライアントは↓
多少ばらつきありますがネットワーク的に遠いところ(日本)なのでまぁこんな感じですかね。ゴミセッションの削除がはしったと思われるタイミングでもそんなに重くなってないのが特徴です。
とまぁこんな感じでかなりまともに使えるセッションプロバイダになった気がします。
ロールやメンバシップの部分はASP.NET Univarsal Providersのが良しなに利用できると思うので組み合わせるといいんではないでしょうか。
最終的なソースはこちら↓
// create: 2011.08.23
// author: @takepara (http://tinyurl.com/3kebwlu) , @kazuk (http://tinyurl.com/3r5vjkq)
using System;
using System.Configuration;
using System.Data.Common;
using System.Reflection;
using System.Web;
using System.Web.Providers;
using System.Web.Providers.Entities;
using System.Threading;
namespace UniversalProviders
{
public class HighPerformanceSessionStateProvider : DefaultSessionStateProvider
{
private string _connectionStringName;
private bool _initialized = false;
private DateTime _lastSessionExpireInvoked = DateTime.MinValue;
private object _lockSessionExpire = new object();
private bool _working = false;
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
_connectionStringName = config["connectionStringName"];
base.Initialize(name, config);
}
private void ExecuteSql(ConnectionStringSettings connectionStringSettings, Action<DbProviderFactory, DbCommand> functor)
{
var providerName = connectionStringSettings.ProviderName;
var factory = DbProviderFactories.GetFactory(providerName);
using (var connection = factory.CreateConnection())
{
connection.ConnectionString = connectionStringSettings.ConnectionString;
connection.Open();
var command = connection.CreateCommand();
functor(factory, command);
command.ExecuteNonQuery();
connection.Close();
}
}
private void CreateSessionIndex(ConnectionStringSettings connectionStringSettings)
{
ExecuteSql(connectionStringSettings, (factory, command) =>
{
command.CommandText = @"IF not EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[Sessions]') AND name = N'IX_Sessions')
begin
CREATE NONCLUSTERED INDEX [IX_Sessions] ON [dbo].[Sessions]
(
[Expires] ASC
)
end
";
});
}
private void RemoveExpiredSessions(
ConnectionStringSettings connectionStringSettings)
{
if( _working ) return; // やってる人が居るならやらんでいいでしょ
_working = true;
try {
ThreadPool.QueueUserWorkItem( (object state)=>
{
try
{
ExecuteSql(connectionStringSettings, (factory, command) =>
{
command.CommandText = "delete Sessions where Expires < @0";
var parameter = factory.CreateParameter();
parameter.ParameterName = "@0";
parameter.Value = DateTime.UtcNow;
command.Parameters.Add(parameter);
});
}
finally { _working = false; }
});
} catch { _working = false; throw; }
}
private MethodInfo GetCreateSessionEntities()
{
var modelHelper =
Assembly.GetAssembly(typeof(Session)).GetType("System.Web.Providers.Entities.ModelHelper");
return modelHelper.GetMethod("CreateSessionEntities",
BindingFlags.NonPublic | BindingFlags.Static);
}
public override void InitializeRequest(HttpContext context)
{
var connectionStringSettings = ConfigurationManager.ConnectionStrings[_connectionStringName];
if (!_initialized)
{
var initializer = GetCreateSessionEntities();
initializer.Invoke(null, new object[] { connectionStringSettings });
CreateSessionIndex(connectionStringSettings);
_initialized = true;
}
if( _lastSessionExpireInvoked < DateTime.UtcNow - TimeSpan.FromSeconds(30) )
{
if( Monitor.TryEnter( _lockSessionExpire ) ) {
// ロックに入れたスレッドでだけ処理する、入れなかったスレッドでは処理しない
DateTime old = _lastSessionExpireInvoked;
_lastSessionExpireInvoked = DateTime.UtcNow;
try
{
try
{
RemoveExpiredSessions(connectionStringSettings);
}
catch
{
// 失敗した時はタイマ値を戻すことで別のスレッドにやってもらう
_lastSessionExpireInvoked = old;
throw;
}
}
finally
{
// 絶対にMonitorを出る
Monitor.Exit( _lockSessionExpire );
}
}
}
}
}
}
ASP.NET Universal ProvidersおよびカスタムのHighPerformanceSessionStateProviderを見てきたわけですが、本命(?)のWindows Azure AppFabric Cachingだと、どんなもんなんでしょうか。
構成等は今までと同じで、Windows Azure AppFabric Cachingも同じエリア(東アジア)に作って試してみました。
※Azure AppFabric Cachingの使い方等は適当に検索で(
※アセンブリ追加してWeb.configにもろもろ追加+セッションプロバイダの変更をしただけです。(ちなみに今回はSSLなエンドポイントを使用)
↓結果
えらい早い ( ゚д゚) 12万セッションだだだっと作るのに3分ぐらい。
作りながら同じセッションで継続アクセスした場合はこんな感じ
削除処理とかはAzure AppFabric Caching側が面倒みるのでいつかわかりませんが、きちんと破棄されたりしているようです。
とまぁこんな感じでした。
前回はそのまんまASP.NET MVC3ロールに含まれてるASP.NET Univarsal Providersを使って軽く負荷テストしたりしてみました。
結果は想定通りだったわけですが、なんと、たけぱらさんにこんなコメントをつけて頂き、しかもHighPerformanceSessionStateProvider という実装までしてくださいました。
こりゃー再度テストしてみないといけないな!ということで。
条件は前回と同じくAzure上にSサイズで5インスタンスほど、たけぱらさん作HighPerformanceSessionStateProviderを使ったMVC3ロールを展開。クライアントも同様にSサイズなWorkerロールを20インスタンスほど作って並行で6000セッションづつ作りまくる感じにしました。
HighPerformanceSessionStateProvider はたけぱらさんのBlogからコピペ。ただSQL Azureで利用する際に1点だけ修正が必要なので注意です。
CreateSessionIndex メソッド内
private void CreateSessionIndex(ConnectionStringSettings connectionStringSettings)
{
ExecuteSql(connectionStringSettings, (factory, command) =>
{
command.CommandText = @"IF not EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[Sessions]') AND name = N'IX_Sessions')
begin
CREATE NONCLUSTERED INDEX [IX_Sessions] ON [dbo].[Sessions]
(
[Expires] ASC
)
end
";
});
}
SQL Azure上で利用できないT-SQLの構文があるので、上記10行目に書かれてた「ON [PRIMARY]」を削除します。
Web.configも書き換えたら準備完了です。
で、実際に見てみると…だいぶ早い感じ。セッションタイムアウトは10分にしてたのですが、6000セッションx20作成中に生存してるセッションが2万弱とかだったのに対し4万弱ほど作れてたりします。
ログみると
タイムアウトになったりなNGが5000ちょいほどありますが…概ね良好な感じですね。
※タイムアウトなるのはコードの問題な気がしないでもない…例外まで補足してなかったので詳細不明(ひょっとしたらSQL Azure側?うーん)
で、前回まともに動作しなかった同じセッションIDで更新したり、ごみセッションを削除する処理ですが
こんな感じでゴリゴリセッション作成してる間でも500msとかでアクセスできてますね。(=セッションテーブルの更新もOK)
また最後の15分経過後のアクセスも5秒かかっていますが問題なくできてます。
ちなみにこの時のごみセッション数は32000弱。
ちょっとしたひと手間でまともに使える感じがします!
HighPerformanceSessionStateProvider イイネ!
全開前回の続きです。
前回、セッションIDにAppDomainAppIdの値が付与されるから複数台な環境だとAppDomainAppIdを合わせておかないとセッション共有ができないんじゃね?という話をしました。
理屈上はそうなので、どうしようかと思って実際にWindows Azure上で見てみると…
おや。複数インスタンスでもAppDomainAppId同じですね。Full IISでマルチサイトしてみましたけど、(法則性はよくわかりませんが)全部同じようです。
想像するにWeb Roleを初期化する際、サービス定義ファイル等に基づいてサイトを作成しますがAppDomainAppIdも指定して作成している感じですか。
というわけでセッションIDに関する懸念はWindows Azure上で使う分には気にしなくて良さそうですね。
さて次はパフォーマンスの話。
前回見てみた限りだと明らかにパフォーマンスに問題がありそうな仕組みでした。
ということで実際どれぐらいなんだろう、というのをぱふぉちゅー素人であるおいらが見てみようと思います。
※言うまでもなく実際にASP.NET Univarsal Providersを採用する際はご自身で確認してください。あくまで参考程度に。
さてテストするにあたってASP.NET Univarsal Providers+SQL Azureを使用してセッション共有するようにしたWebアプリを5インスタンスで動かします。それからWebアプリにアクセスしてセッションを作成したりするWorker Roleを20インスタンスほど。
台数にあまり深い理由はないんですがセッションの応答速度みるのにサーバー自体がネックになってもなぁと思いつつ5インスタンスに。クライアント側は多めにということで20インスタンスにしました。
クライアント側は単純にWebClient使ってページを読み込むだけです。で、前後で時間を計測する感じに。
class Program
{
static void Main(string[] args)
{
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
GetPage();
int count = int.Parse(args[0]);
for (int i = 0; i < count; i++)
{
sw.Reset();
sw.Start();
GetPage();
sw.Stop();
Console.WriteLine("{0}\t{1}",DateTime.UtcNow.ToString("yyyy/MM/dd hh:mm:ss.fff"),sw.Elapsed);
}
}
static private void GetPage()
{
WebClient wc = new WebClient();
using (Stream st = wc.OpenRead("http://penguindrum.cloudapp.net/"))
{
Encoding enc = Encoding.GetEncoding("UTF-8");
using (StreamReader sr = new StreamReader(st, enc))
{
string html = sr.ReadToEnd();
sr.Close();
st.Close();
}
}
}
}
Webアプリ側は前回と同じやつでセッションオブジェクトが無ければ現在時刻を放り込むだけの簡単仕様
とりあえずひたすらセッションを作っていくわけですが、Insert処理だとそんなに(?)重くない感じですね。
2万セッションぐらい作って(ページ取得の)平均応答時間が0.5秒ぐらい、長くて2秒。
さてセッションを作りまくるワーカー走らせてる横で、同じようにセッション作って、再度そのセッションを使って1分おきにアクセスするような(=DBを更新するような)クライアントで処理時間見てみようと思います。(※WebClientだとCookieセットできなかったのでHttpWebRequestを使用)
適当にクライアントアプリ作った所為で落ちてますが、うまく更新できるケースもあればタイムアウトでちゃんと応答が返ってこないとか、返ってきても6秒かかってるとか。ちょっと不安定。
一番最後の実行結果は、作ったタイムアウトになった2万セッションがDBに残ってる状態で新規アクセスしたときのですが、期限切れセッションを取得して削除処理が入ってるので52秒かかってます。これ他にアクセスが無い状態での結果なので…
同時アクセスがそんなになく、ユーザー数も少ないようなケース、もしくは多少応答が遅くなってもいいケースでは簡単に使えるので便利ですね。
DB側のチューニングでもう少し改善しそうですけど、あまり詳しくないので触れないでおきます。
まぁなんというか、このエントリそのものはあまり役に立たない内容でしたが、捨てるのもあれなので晒してみました。恥じらいとたくし上げ。
ASP.NET Univarsal Providersのセッションプロバイダって、セッションの破棄もしてくれるの?という話題から。
結論から言うと、「期限切れのセッションは削除してくれます。但しパフォーマンスに問題がありそうなやり方で。」となります。
@kazuk さんにさらっと見てもらえたので先に結論がわかりました。ありがとうございます。
さて実際にその様を見てみましょうか。
ちょっと前の話題ですが、Codeplexに「Windows Azure Accelerator for Web Roles」なるものが公開されました。
簡単に言うと、Visual Studioから簡単にWebサイト/アプリをWindows Azure上のWebロールに発行できて、しかも発行したサイト/アプリが永続化されてて複数インスタンスにも対応、という優れたヤツです。
まぁ詳しいことはAzureの小ネタさんのところに書かれてるのでそちらを参照。
さて、細かいのは見てもらって把握された前提で以下2点ほど小ネタを。
もう2か月近く経つ気がしないでもないですが、Windows Azure Connectが更新されてます。
といってもまだベータ(CTP)ですけどね。
今回の更新では以下の2点が強化されています。