ASP.NET Univarsal Providers のセッションプロバイダを使ってみる (2) « ブチザッキ
カメさんのこのエントリーに対してこんなコメントをしてみたわけですが、具体的なコードを提示しないで案だけ出すって、そんな失礼なことがあっちゃいけねー。江戸っ子なら宵越しの銭はもってちゃいけねー。江戸っ子じゃないんですけどね。
まずは現状確認。カメさんのコードをいただきつつ。ASP.NET MVC3のサイトをデフォルトで作成。ASP.NET Universal ProvidersをNuGetでインストール。web.configでSessionStateの設定をCookieless="UseUri"とTimeout="1"に変更(検証が簡単だからね)。
using System; using System.Net; namespace ConsoleApplication1 { 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(); var success = GetPage(); sw.Stop(); Console.ForegroundColor = success ? ConsoleColor.White : ConsoleColor.Red; Console.WriteLine("{0}\t{1}\t{2}\t{3}", DateTime.UtcNow.ToString("yyyy/MM/dd hh:mm:ss.fff"), sw.Elapsed, i, success ? "OK" : "NG"); } } static private bool GetPage() { var result = true; try { var request = WebRequest.Create("http://localhost:52855/"); { using (var response = request.GetResponse()) { } } } catch { result = false; } return result; } } }
ずるくないもん!オマージュだもん!
コレとMVCサイトで実行した結果(クリックでズーム)。
セッションを生成するコンソールは3個でCPUはMAX。白行はエラーなく進んだところで、赤行はASP.NET側でエラーになったところ。
1分でセッションは切れるので、1分後からドカドカ古いセッションの削除が始まります。
概ね680セッションあたりで飽和。エラーと正常実行を繰り返す感じです。切ないですね。セッションの削除が同時実行されててんやわんやな例外。
これを解消するために、InitializeRequestをoverrideしたSessionStateProviderクラスを定義します。
やることはSessionsテーブルにExpires列のインデックスがなければ作成と、セッション初期化時の有効期限切れセッションの削除をEFのDeleteObjectじゃなくSQLのDelete文を実行するように。
ちょっと長いですけど。
using System; using System.Configuration; using System.Data.Common; using System.Reflection; using System.Web; using System.Web.Providers; using System.Web.Providers.Entities; namespace UniversalProviders { public class HighPerformanceSessionStateProvider : DefaultSessionStateProvider { private string _connectionStringName; private bool _initialized = 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 ) ON [PRIMARY] end "; }); } private void RemoveExpiredSessions(ConnectionStringSettings connectionStringSettings) { 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); }); } 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; } RemoveExpiredSessions(connectionStringSettings); } } }
クラス名はもちろんHighPerformanceSessionStateProviderデス!
<sessionState mode="Custom" customProvider="HighPerformanceSessionProvider" cookieless="UseUri" timeout="1">
<providers>
<add name="DefaultSessionProvider"
type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
connectionStringName="DefaultConnection" applicationName="/" />
<add name="HighPerformanceSessionProvider"
type="UniversalProviders.HighPerformanceSessionStateProvider, UniversalProviders"
connectionStringName="DefaultConnection" applicationName="/" />
</providers>
</sessionState>
Web.configも上記のように変更しちゃいましょう!customProviderの指定がDefaultSessionProviderでオリジナルに戻ります。
セッションが飽和するのが、940件くらいのところです。もちろん例外なんて起きませんよ。有効期限切れのレコード削除なんて何回実行したって、無いものはない!潔し!ちなみに先程の数値もそうですけど、コレが同時実行の限界値なわけではないです。マイノートPCの同時実行の限界くらいです。Webの受付のね。DbProviderFactoriesを利用することでEFと同じだけのポータビリティ(言い過ぎ)。
名前ほど早くないけど、エラーが起きないから実用的なんじゃん?読み込み性能は気にしてないでーす。あくまで新規セッション作成時のパフォーマンス向上委員会です。言い訳がましい...。
ご利用は計画的に!
Azureでは動かしてないので、そちらの検証はたぶんカメさんがその3でやってくれるんじゃないでしょーか。