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でやってくれるんじゃないでしょーか。