われらが(?) SmarxさんがWindows Azure上でMemcachedを動かすためのNuGet パッケージを作ってくださいました。
早速利用してみましょう。
われらが(?) SmarxさんがWindows Azure上でMemcachedを動かすためのNuGet パッケージを作ってくださいました。
早速利用してみましょう。
紆余曲折ありましたが(?)、Windows Azure上のWeb Roleでセッションを共有する方法を簡単に(いい加減に)まとめたいと思います。
| ASP.NET標準 | ASP.NET標準 | MSDN Code Gallaryのセッションプロバイダ | ASP.NET Univarsal Providers | High Performance Session State Provider | Windows Azure AppFabric Caching | |
| ストア | メモリ(InProc) | SQL Azure | Azure Storage Table | SQL Azure | SQL Azure | Windows Azure AppFabric Caching |
| 作成者 | MS | MS | MS? | MS | @takepara @kazuk |
MS |
| SLA | 無 | 無(ストアはある) | 無(ストアはある) | 無(ストアはある) | 無(ストアはある) | 有 |
| 特徴 | 冗長化・負荷分散しない漢向け | 削除をなんとかしたら使えないことはない | Table使うので安い | 新入り。そつなく利用可。 | 自作上等。ASP.NET Universal Providerの残念なところを解消 | 大規模で本気出すなら商用サービスだよね |
| メリット | お手軽高速 | お手軽 | お手軽で安い? | お手軽。RoleやMembershipも使える | お手軽 | 高速。一番早い。 |
| デメリット | 冗長化されない・負荷分散されない | セッションが削除されない | セッションが削除されない。遅い。 | セッション削除に難あり | 自家製 | お金。 |
| 追加コスト | 無し | DB分必要 | 低(SQL Azureよりは安い) | DB分必要 | DB分必要 | 高め(128MB/月で3,933.45円) |
| パフォーマンス | 高 | 中 | 低 | 中 | 中の上 | 高 |
| その他 | パフォーマンスカウンタが付く | セッション数の把握はDB見れば可 | よくわからない | セッション数の把握はDB見れば可 | セッション数の把握はDB見れば可 | ざっくりした合計セッションサイズがある程度わかる(ただしリアルタイムでは把握できない?) |
という感じで役に立つかどうかわかりませんがまとめました。いろいろ要件にあわせて選んでくださいませ。
冗長化いらないならInProcとか。
最低限負荷分散したいけど小規模だしコストかけたくないなぁとかならAzure Storage Tableとか。
それなりにちゃんと使いたいとかならHighPerformanceSessionStateProviderとか。
いやー自分で面倒みるの大変だよとかならASP.NET Univarsal Providersとか。
大規模で性能いるんだよお金はあるぜとかならAzure AppFabric Cachingとか。
まぁ他にもMemcached使ったり、自前で負荷分散も対応したInMemoryなプロバイダ作ってもいいでしょうし。
お好みでどうぞ!
性懲りもなく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 さんにさらっと見てもらえたので先に結論がわかりました。ありがとうございます。
さて実際にその様を見てみましょうか。
さっきの投稿でも書いた 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もあるので使い方とか参考にどうぞ。
これはちゃんと使いこなしたいですね!
※EULAのどこどう見たらGoLiveなのか詳しい人に教えてもらいたいところ…ちゃんと読めという話ですが。
※EULAの補足。nugetで入手できる0.1はALPHA版ということで、商用利用が制限されています。ただ、ASP.NET MVC 3 Webロールに含まれてる1.0のほうはこの制限がないので、商用利用OK、Go-liveとみていいと思います。
いろいろ盛り込んでくれますね!Azure Team!
先日の投稿でも書きましたが、Windows Azure Tools for Visual studio v1.4 (August 2011 Update)でASP.NET MVC 3のWebロールテンプレートが追加されました。
で、このASP.NET MVC 3なWebロールを使うと今まで面倒だったアセンブリの追加とかが不要なわけですが、そのまま何も気にせずデプロイするとエラーになります。
よくみる(?)画面ですね。
さて、この原因ですが 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だそうです。
未来を先取りテンプレート!!
ということは、Bad know-howで問題点として片付けるんじゃなくて、新しいテクノロジを活用しよう!っていう方向に持っていくべきですかね。
たけはらさんありがとうございます。
ひさびさの開発者向け大型アップデート来ました。
Windows Azure Tools for Microsoft Visual Studio 2010 August 2011 です!
今までTools for VSは1.3ベースだったんですが、これで1.4ベースとなりました。