2011年8月28日日曜日

IIS7でnode.js

Installing and Running node.js applications within IIS on Windows - Are you mad? - Scott Hanselman

node.jsをIISで動かす!ファイル更新で自動リサイクルは楽チンでいいですね!受け口がIISだからIISのその他の便利モジュールがそのまま使えるのもいい。

install.batでやってることも紹介してくれてるので、同じ手順でIIS Expressでも大丈夫!なはず。

iisnode.dllっていうネイティブモジュールが橋渡し。よさそうな雰囲気。プロセス数の設定とか、いろいろ調整できるのも素晴らしす。

と、いうわけで、試しに動かしてみました。

インストール手順。

  1. node.jsのバイナリダウンロードしてc:\nodeにコピー
  2. iisnodeのバイナリをc:\inetpub/iisnodeにコピー
    ※ソースから自分でビルドしても可
  3. iisnode内のinstall.batを実行

以上。簡単ですね!node.exeのパスを変えたい場合は、install.batの確認場所の変更と、web.configでnodeProcessCommandLine指定でどーぞー。

node2

node

なんかいいね。6000req/sec。たいしたもんだ。ただ、iisnode.dllがちょいちょい落ちる(アプリケーションプールごと道連れ)。まだまだ発展途上。でもネイティブモジュールのスゴさを垣間見れるし、手軽にnode.jsをWindows環境で走らせることが出来ていいです!

ちなみにハンセルマンさんのエントリでも書いてる通り、AppPoolのアカウントをLocalSystemにしとかないと動きません。あと、wcatのsettings.ubrはちゃんとsettings{}で囲んでおきましょう。wcatについてはよく知りませんが(すません)、wcclientが動いてくれなくて、ちゃんと走りませんでした(手動起動で動かした)。

こんなに簡単にできるなら、何か面白そうなことを考えて試してみたり、node.jsでできることいろいろ追いかけて見たくなりますね!

2011年8月19日金曜日

HighPerformanceSessionStateProvider

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サイトで実行した結果(クリックでズーム)。

up1

セッションを生成するコンソールは3個でCPUはMAX。白行はエラーなく進んだところで、赤行はASP.NET側でエラーになったところ。

up2

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デス!

up3

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

2011年8月14日日曜日

Controllerを名前から生成するしHTMLを書き換えたりもしてみる

Developer @ ADJUST : ASP.NET MVC のコントローラクラスを、コントローラ名から取得したいのだけれど...結局、ぜんぶ列挙!?

前回のエントリーからはや1ヶ月以上経過したこのブログ。エアコンが壊れてて、バリバリ節電に貢献できてる気はするけど、辛い毎日でブログどころではないですよ!

で、坂本さん、上記の件ですが以下のような方法はいかがでしょうか。単純に型が欲しいだけならインスタンスは不要なので無駄な感じはしなくもないですが。必要なのがインスタンスであれば一番いい方法だと思います。

var factory = ControllerBuilder.Current.GetControllerFactory();
var controller = factory.CreateController(Request.RequestContext, "Home");
var type = controller.GetType();
factory.ReleaseController(controller);

コレと言って、自信で発明したものはなく、やってることはMvcHandlerでやってることと同じです。MvcHandlerってあれです、MvcRouteHandlerが返してくるIHttpHandlerのことです。

内部でタイプキャッシュとかしてくれてるので、パフォーマンスには自信があります!あると思います!

ここから別件。

もう暑さで脳みそが働かない...。けど、これだけじゃ坂本さんも納得しないと思うので、全然関係ない面白い機能紹介。といっても、URLRewrite 2.0の機能です。ハイパフォーマンスWebサイトの構築には必須のOutputCacheなんですが、クローラーなんかはUser AgentがCookieに対応してないと言ってくるじゃないですか。browserファイルで対応するのが王道ですが、それだと追従していくのが大変面倒くさい。

Creating Outbound Rules for URL Rewrite Module : URL Rewrite Module 2 : URL Rewrite Module : The Official Microsoft IIS Site

想定している状況を簡単にいうと、SessionのcookielessはuseDeviceProfile。PCサイトとケータイサイトを同一アプリケーションで実行。PCサイトは積極的にOutputCacheを利用し、ケータイサイトでは消極的(Viewでは使わない)に利用。この場合、PCサイトにクローラが来るとそのままではCookielessとしてセッションが生成されるので、OutputCacheにSessionIDを保持したHTMLが出力されてしまって都合悪い!

ということなんですが、伝わるでしょうか...。

OutputCacheをVaryByUserAgentにするのも手ですね。いろいろ方法はあると思うんですけど、こないだ試したのがPCサイトの場合、出力されるHTMLにSession IDが含まれることが正常系ではありえないので、PCサイトの場合、Response.FilterでURLにSession IDが含まれてたら消してしまうという方法。Response.Filterでやるよりももっと簡単なのがURL Rewrite 2.0の出力書き換えを利用するっていう方法。どうですか、面白そうじゃないですか?

ちなみにOutbound Rule用に独自Provider書いたりもできるけど、そこまでやることはあんまりないかなー、と思いますがどうでしょう。

<rewrite>
  <outboundRules>
      <rule name="Sessionless" preCondition="html" enabled="true">
          <match filterByTags="A, Area, Base, Form, Frame, Head, IFrame, Img, Input, Link, Script, CustomTags" customTags="All" pattern="(.*)/\(S\([0-9a-z]+\)\)(.*)" />
          <action type="Rewrite" value="{R:1}{R:2}" />
          <conditions>
              <add input="{REQUEST_URI}" matchType="Pattern" pattern="\)/mobile" ignoreCase="true" negate="true" />
          </conditions>
      </rule>
      <customTags>
          <tags name="All" />
      </customTags>
      <preConditions>
          <preCondition name="html">
              <add input="{RESPONSE_CONTENT_TYPE}" pattern="text/html" />
              <add input="{REQUEST_URI}" pattern="/mobile" negate="true" />
          </preCondition>
      </preConditions>
  </outboundRules>
</rewrite>
<urlCompression doDynamicCompression="false" />

だいたいこんな感じの設定です。これをWeb.configに書いておきましょう。system.webServer配下です。ケータイサイトが/mobile配下という前提です。

実験。MVCサイトをデフォルトのまま生成し、Web.configに上記設定を追加しないで動作を確認。

この状態で出力されるのは↓。

cookieless1

そりゃそうですね。中身は↓。

cookieless2

なんてことないですよね。

今度はsessionStateのcookielessをUseUriにしてみましょう。見た目は同じなのでソースだけ。

cookieless3

画像をクリックして大きくしてみるとわかりますが、Session IDを含んだURLですよね。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Home Page</title>
    <link href="/(S(f4grns0hiatkwquk55qulbmd))/Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="/(S(f4grns0hiatkwquk55qulbmd))/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="/(S(f4grns0hiatkwquk55qulbmd))/Scripts/modernizr-1.7.min.js" type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
                <h1>My MVC Application</h1>
            </div>
            <div id="logindisplay">
                    [ <a href="/(S(f4grns0hiatkwquk55qulbmd))/Account/LogOn">Log On</a> ]
            </div>
...以下省略

今度は先のURLRewrite設定をOnにした場合。

cookieless4

あら素敵。ウソじゃないよ!URLにはちゃんとSession ID入ってるでしょ?

これでクローラが来てもOutputCacheにへんてこなものが仕込まれることないですね!あと、動的圧縮をオフにしてるのは既知です。設定いいんですけど今回はこれで。

役に立つやら立たないやらな情報でした!