ASP.NET Univarsal Providers のセッションプロバイダを使ってみる (4)

性懲りもなく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 ); 
					} 
			   } 
			}
		}
	}
}

Windows Azure AppFabric Caching を使ってみる

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 Univarsal Providers のセッションプロバイダを使ってみる (3)

第1回、第2回に続いて第3回です。

前回はそのまんま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 イイネ!

ASP.NET Univarsal Providers のセッションプロバイダを使ってみる (2)

全開前回の続きです。

前回、セッション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 のセッションプロバイダを使ってみる

ASP.NET Univarsal Providersのセッションプロバイダって、セッションの破棄もしてくれるの?という話題から。

結論から言うと、「期限切れのセッションは削除してくれます。但しパフォーマンスに問題がありそうなやり方で。」となります。

@kazuk さんにさらっと見てもらえたので先に結論がわかりました。ありがとうございます。

さて実際にその様を見てみましょうか。

続きを読む →

ASP.NET Universal Providers

さっきの投稿でも書いた System.Web.Providers ですが、名称としては ASP.NET Universal Providers というそうです。

簡単に何をするものか説明すると、「ASP.NET におけるセッションステート、メンバシップ、ロール、プロファイル機能を、SQL Compact と SQL Azure にも対応させるツール」 という感じでしょうか。パクリですけど。

Node.js、Ruby、Python を Windows Azure で利用する Smarx Role ~ クラウドカバー Episode 48 より引用

ASP.NET におけるセッションステート、メンバシップ、ロール、プロファイル機能を、SQL Compact と SQL Azure にも対応させるツール “ASP.NET Universal Provider” がアルファ版としてリリースされました。

これにより、これまでのオンプレミスな Windows & SQL Server という組み合わせに加え、低トラフィックな Web サイトでは共有ホスティング & SQL Compact を利用、トラフィックが多い Web サイトでは Windows Azure & SQL Azure を利用する、といったことが可能になります。

ということで便利でナイスなプロバイダなわけですが、現時点でnugetから入手できるASP.NET Universal Providerは 0.1です。

でも、ASP.NET MVC 3 Webロールテンプレートに含まれる System.Web.Providers は1.0扱い?なんですよね。アセンブリも違うもののようです。(中身まで見たわけじゃないですがバイナリは異なる)

そのうちNugetにもあがる気はしますが、とりあえず正式版?がASP.NET MVC 3 Webロールテンプレートには含まれてるようです。

ちなみにアセンブリは<プロジェクトフォルダ>\packages\System.Web.Providers.1.0 のLibにあります。EULAやReadme.htmlもあるので使い方とか参考にどうぞ。

image

これはちゃんと使いこなしたいですね!

※EULAのどこどう見たらGoLiveなのか詳しい人に教えてもらいたいところ…ちゃんと読めという話ですが。

※EULAの補足。nugetで入手できる0.1はALPHA版ということで、商用利用が制限されています。ただ、ASP.NET MVC 3 Webロールに含まれてる1.0のほうはこの制限がないので、商用利用OK、Go-liveとみていいと思います。

image

 

いろいろ盛り込んでくれますね!Azure Team!

Windows Azure ASP.NET MVC 3 Web Role で使っているプロバイダ

先日の投稿でも書きましたが、Windows Azure Tools for Visual studio v1.4 (August 2011 Update)でASP.NET MVC 3のWebロールテンプレートが追加されました。

で、このASP.NET MVC 3なWebロールを使うと今まで面倒だったアセンブリの追加とかが不要なわけですが、そのまま何も気にせずデプロイするとエラーになります。

image

よくみる(?)画面ですね。

さて、この原因ですが Deploying the Windows Azure ASP.NET MVC 3 Web Role にもある通り、ASP.NET MVC 3 Webロールプロジェクトテンプレートに含まれてるWeb.configにて、SQL Expressなカスタムセッションを利用するようになっているのが原因です。

解決方法はWade Wegner氏が書いてるとおりSQL Azure使うように正しく接続文字列変えてあげると良いのですが、面倒な場合はsessionState属性のmodeをOffとかInProcにすればいいんじゃないでしょうか。(投げやり)

※というのもちゃんとアプリケーション作るときはこの辺きちんと考慮するだろうし、デフォルトのままデプロイしたりするのは訓練されたあじゅらーぐらいな気もするので…

まぁ、そのうち修正されるかもしれませんがちょっとしたbad know-howということで…

– 重要な補足 –

たけはらさんにTumblrで教えていただきました。

どうもこのASP.NET MVC 3 Webロールテンプレートで使っているセッションやメンバシッププロバイダ等各種プロバイダはEntityFrameworkベースのSystem.Web.Providersだそうです。

Introducing System.Web.Providers – ASP.NET Universal Providers for Session, Membership, Roles and User Profile on SQL Compact and SQL Azure – Scott Hanselman

未来を先取りテンプレート!!

ということは、Bad know-howで問題点として片付けるんじゃなくて、新しいテクノロジを活用しよう!っていう方向に持っていくべきですかね。

たけはらさんありがとうございます。

Windows Azure Tools for Microsoft Visual Studio 2010 August 2011

ひさびさの開発者向け大型アップデート来ました。

Windows Azure Tools for Microsoft Visual Studio 2010 August 2011 です!

今までTools for VSは1.3ベースだったんですが、これで1.4ベースとなりました。

続きを読む →

JAZ結成一周年総会のお知らせ

というわけで、8月26日で1周年をむかえるJapan Windows Azure User Groupですが、このたび1周年を記念して総会を開くことになりました。

( ゚Д゚ノノ"☆パチパチパチパチ

参加申し込みはこちらから → JAZ結成一周年総会

開催概要

イベント名 JAZ結成一周年総会
日時 2011年9月3日 (土) 12:30 – 17:30
場所 日本マイクロソフト株式会社 品川本社オフィス
東京都港区港南 2-16-3 品川グランドセントラルタワー

当日はライトニングトーク(LT)てんこもりでお送りする予定。

ひとえに、このイベントが楽しくなるかどうかはLTするみなさまの熱意次第ということです!!
※プレッシャーかけるな ( ‘д‘⊂彡☆))Д´) パーン

おいらもそうでしたけど、ライトニングトークは(今回は)せいぜい5分だし、失敗しても恥かいても5分ですぐ終わるので傷が浅いです。慣れるにはもってこい!綺麗に終わった後の爽快感は何とも言えません。
スライドなくて話だけで5分でも、スライドだけでも、極端な話踊ろうが歌おうが何でもいいわけで(言い過ぎか)、発表する側の「きっかけ」としてはすごくいいかなと思います。

なので、スピーカー/ライトニングトーク初めての方大歓迎。JAZで一緒にステップアップしていきましょう!的なノリで。
ネタも「Azure」絡めば何でもOKという緩さ。(※Windows Azure 、としてないところがミソですよ)
LTもいろいろな場所で行われていますけど、なかなか機会が~とか踏ん切りが~とか悩んでる方はこの機会を是非利用していただければー!

他にもいろいろ楽しんで頂けるネタ用意しようとは思ってますので、Windows Azureに興味のある方のご参加お待ちしております~!