2011年12月31日土曜日

MongoDBでお気楽MapReduce

MongoDBにはMapReduceの仕組みが用意されてて楽しそうですよね。

MapReduce - Docs-Japanese - 10gen Confluence

ちゃんと日本語。

で、コンソールで入力しながらの開発は面倒なので、こんな時はMongo VUE。安くならないかなー。

Yet another MongoDB Map Reduce tutorial | MongoVUE

使い方、これじゃよく分からない。

How to perform MapReduce operations in MongoVUE | MongoVUE

こっちですね!この手順どおりに実行すれば、ちゃんとMapReduce!Sharding環境ならそれぞれのShardで分散処理してくれます。

せっかくなので、以前作ったMongoTraceListnerのサンプルを利用して、各URLの処理時間の平均をMapReduceで計算してみます。

public class PerformanceTraceModule : IHttpModule
{
  private string ItemKey = "_mongoDbTraceStart";

  public PerformanceTraceModule()
  {
  }

  public void Init(HttpApplication context)
  {
      context.BeginRequest += new EventHandler(context_BeginRequest);
      context.EndRequest += new EventHandler(context_EndRequest);
  }

  void context_BeginRequest(object sender, EventArgs e)
  {
      var httpContext = (sender as HttpApplication).Context;
      var startTime = httpContext.Items[ItemKey] = DateTime.Now;
  }

  void context_EndRequest(object sender, EventArgs e)
  {
      var httpContext = (sender as HttpApplication).Context;
      var startTime = (DateTime)httpContext.Items[ItemKey];

      Trace.WriteLine(new
      {
          Method = httpContext.Request.HttpMethod,
          Status = httpContext.Response.StatusCode,
          RawUrl = httpContext.Request.Url.ToString(),
          Milliseconds = (DateTime.Now - startTime).Milliseconds
      });
  }

  public void Dispose()
  {
  }
}

このHttpModuleを仕掛けて処理時間をMongoDBに入れます。

実行!何度も何度もリロードやら、About、Loginへのアクセスを繰り返す。

mr2

データも溜まって来ました。

mr1

ここで、MapReduceを実行。

Map

mr3

Reduce

mr4

Finalize

mr5

In & Out

mr6

結果

mr7

ただ、ここでmap/reduce/finalizeのコードに間違いがあっても、わかりにくい。どこのなにがエラーなのかサッパリ。そんな時はコンソールを見てみましょう。サービス化してたらログファイル。

mr8

finalizeで適当にエラーコードを入れて実行するとコンソールにエラーメッセージが出てます。開発中はMongoDB Shellでやるか、ローカル実行してコンソール確認しながらすすめるのがいいかもー。

Mongo VUEやShellで見るだけじゃなくて、ASP.NET MVCのアプリケーションから見れるようにするために、以下のようにしてみました。

private MongoDatabase GetDatabase()
{
  var serverName = ConfigurationManager.AppSettings["MongoDb:Server"];
  var databaseName = ConfigurationManager.AppSettings["MongoDb:Database"];

  if (string.IsNullOrEmpty(serverName))
      serverName = "mongodb://localhost";

  if (string.IsNullOrEmpty(databaseName))
      databaseName = "TraceListner";

  var server = MongoServer.Create(serverName);
  return server.GetDatabase(databaseName);
}

private BsonJavaScript GetMapReduceCode(string name)
{
  return new BsonJavaScript(System.IO.File.ReadAllText(Server.MapPath("~/MapReduce/" + name)));
}

public ActionResult MapReduce()
{
  var map = GetMapReduceCode("map.js");
  var reduce = GetMapReduceCode("reduce.js");
  var finalize = GetMapReduceCode("finalize.js");

  var collection = GetDatabase().GetCollection("MyTrace");
  var options = new MapReduceOptionsBuilder();
  options.SetFinalize(finalize);
  options.SetSortOrder("_id");
  options.SetOutput(MapReduceOutput.Inline);

  var result = from r in collection.MapReduce(map, reduce, options).GetResults()
               select new
               {
                  RawUrl = (string)r["_id"],
                  Count = r["value"].AsBsonDocument["Count"].ToInt32(),
                  Elapsed = r["value"].AsBsonDocument["Elapsed"].ToInt32(),
                  Average = r["value"].AsBsonDocument["Average"].ToDouble()
               };
  return Json(result.ToArray().OrderByDescending(mr=>mr.Average), JsonRequestBehavior.AllowGet);
}

HomeControllerに追加。

mr9

やったー。出たー。あとは、見た目よろしくしていけばOKでしょう。今回は気にしないけど。

こんな感じでMongoDBにデータ入れとけばMapReduceで集計をMongoDBサーバーに任せられていいですね!

っていう、結論じゃなくて、結局はV8でスクリプト動かしてるから、大量のデータの場合そんなに早くなかったりしてショック。対象データをすべてフェッチしてC#で処理したほうが早かったりも...。

速度的には思ったほどじゃないけど、これはこれで面白いので、PowerPivotとあわせて業務系の人が扱えるようになると、嬉しいですよねー。

URLRewrite+CloudFrontでパフォーマンスを取り戻す

年末ですね。年末だからこそ大量のアクセスが発生することもありますね。コンシューマー向けのものだと、平時に比べれば多かったりしますよね。爆発的なアクセスになることもありましょう。おめーさん、容赦無いね!

特にHTTPS。コレはもうホントしんどいですよね。SSLアクセラレータとかSSLオフロードっていうか、そういう前段でさばくように構成して、実処理はHTTPのみにしとかないとHTTPSの処理に尋常じゃないCPU使用率を持っていかれたりしちゃいましょう。そーなると、本来の処理にCPUが割り当てられない。SSLハンドシェーク。悪魔のようです。でも、そんな構成すぐに取れない!そんな時の選択肢としてCDN。え?なんで?だってアクセスの総量が減ればその分処理量も減じゃない!コードチューニングより効果が高いこともあります。パフォーマンス20%アップのコードチューニングより200%アップ(することもある)のCDN。

お手軽なのはAmazon CloudFront。

  • カスタムオリジンで既存サーバーをそのまま利用することで、オリジンサーバーにコンテンツを事前にアップロードしなくてもいい
  • URLRewrite2.0から導入されてるOutboundRulesでのHTML書き換えで、CDNを指すようにしてしまうことで、レンダリングコードに(ほとんど)手を加えずにCDNが使えるようになる
  • CNAMEでカスタムドメインでもそれなりに(HTTPSのカスタムドメインはダメだけど)
  • コンテンツへのHTTPSアクセス負荷をCloudFrontに肩代わりさせれる(コレ!!)
  • 使ったぶんだけ課金は、いまさらですけどやっぱり素敵ポイント

もちろんIISね。Apacheもなんか書き換えありましたよね。mod_ext_filter?IIS以外はよく知らないです。

ココはひとつ、mvcPhotos(覚えてますか?)に実験台として登場してもらいましょう。

cf2

http://mvcphotos.takepara.com

まだ見れる状態になったままでした。そろそろ閉鎖しないと...。

http://mvcphotos.codeplex.com/

CloudFrontの登録とか、そういうのはいろんな人が書いてるので、その辺は省略。

cf3

こんな感じですね。コレといって何のへんてつもない設定。カスタムオリジンとProtocol PolicyをHttp Only、CNAMEでカスタムドメイン(HTTPSの時は割り当てられたドメインをそのまま使う)。

あとは、Web.configにoutboundRulesを追加(ExpressWebでURLRewrite使えると書かれてるけど、バージョンが見当たらなかったから不安だったけど、ちゃんと2.0以降が入ってる模様)。

    <rewrite>
      <outboundRules>
        <rule name="CloudFrontContents" preCondition="html" enabled="true">
          <match filterByTags="A,Img" pattern="^/Photos/Image/(.*)"/>
          <action type="Rewrite" value="//cdn.mvcphotos.takepara.com/Photos/Image/{R:1}"/>
        </rule>
        <preConditions>
          <preCondition name="html">
            <add input="{RESPONSE_CONTENT_TYPE}" pattern="text/html"/>
            <add input="{REQUEST_URI}" pattern="/mobile" negate="true"/>
          </preCondition>
        </preConditions>
      </outboundRules>
    </rewrite>

これだけ。これで、/mobile以外の時に/Photos/Imageへのアクセス(a hrefとimg src)すべてCDNへ変更します(詳しくはCreating Outbound Rules for URL Rewrite Module : URL Rewrite Module 2 : URL Rewrite Module : The Official Microsoft IIS Site )。/mobileを除外してるのには理由があります。mvcPhotosはHTTPSでのアクセスが無いので意味ないんですが、実際は有りましょう。その際、ケータイからのアクセスだとCloudFrontで使ってるワイルドカード証明書が残念なコトになります。まだまだ正常に処理できないですよね。

cf

なので、もともと素材の少ないケータイアクセスの場合(/mobile配下へのアクセス)は、CDNを利用しないようにして、コレまで通りのアクセスにしときます。PCやスマフォでのHTTPアクセスなら処理が負担にならない、って言う場合はconditionsを以下のように追加してHTTPSの時だけCDN参照するようにするのがいいでしょう。

        <rule name="CloudFrontContents" preCondition="html" enabled="true">
          <match filterByTags="A,Img" pattern="^/Photos/Image/(.*)"/>
          <action type="Rewrite" value="//cdn.mvcphotos.takepara.com/Photos/Image/{R:1}"/>
          <conditions>
            <add input="{HTTPS}" pattern="^on$"/>
          </conditions>
        </rule>

ただ今回、mvcPhotosのちょっと残念だったところと、Cloud Frontの制限が丁度マッチして、ちょびっとだけViewとスクリプトの変更がありました。

画像を返す部分をPhotosControllerにやらせてるんですが、そのパラメータをRoutingじゃなくてQueryStringで渡してたんです。が、Cloud FrontはQueryString無視します。なので、そこだけ変更してQueryStringでのサイズと変換方法の指定をRoutingパラメータにしました。

/Photos/Image/1?size=100&type=fit

↑こうだったものを↓こう。

/Photos/Image/1/100/fit

そのためにRoute登録。

    routes.MapRoute(
        "PhotoImage",
        "Photos/Image/{id}/{size}/{type}",
        new {controller = "Photos", action = "Image"});

コードいじってるじゃん!さーせん。でも、普通はいじる必要ないはず。あっても、ViewやScriptだけで済むはずです。サービスとして公開するならURLの設計もちゃんとするはずなので。今回の微調整もRouting以外はViewだけ。View内にknockoutで使うHtml Template埋め込んでるし、パス関連は全てそのテンプレートに展開してるから、OutboundRulesの対象になる。

これで、mvcPhotosにアクセスしてみましょう。

cf4 cf7

クリックして拡大すると見えると思いますが、/Photos/Image配下はCDNへ。その他の要求(JavaScript/CSS/Ajax)は自サーバーに行ってますね。もちろんホントはJavaScriptやCSSも持っていくのがいいでしょう。

cf5 cf6

↑こちらは/mobile配下。今度は同じ/Photos/Image配下のものもCDNに行かず、自サーバー参照のままです。

なんてお手軽。

実際HTTPSを利用してるサイトでBLOBコンテンツのダウンロードが大量に発生している場合、この方法でサーバーへのコネクションを1/10とかに抑えられることになって、ウハウハ。もちろんお金はかかりますけど。それでもサーバー増強やSSLアクセラレーションする機器の購入に比べれば、安いし経費で落とせます!HTTPSでのカスタムドメインが使えないのは気に入らない、って言うことがあるなら他の方法をとってくださいってことになります(ARRでSSLオフロードとかね、お金貰えればなんでもいいでしょう)。

とりあえず、今回のような方法でCDNを利用する時に、こういうふうにページ(View部)作るといいかもと、思ったことを書きだしておきます。

  • 素材を意味ごとに決まったフォルダに分けておく
    → パターンを増やすとその分の処理にCPU使っちゃう
  • スクリプトでスクリプトをロードしないほうがいいかも
    → スクリプト内のパスを書き換えるのが面倒
  • スクリプト内に極力パスを書かない
    → HTMLの属性を使う。DOMにデータを持たせとく。か、クライアントサイドのHTML Templateで。
  • 同じくCSSの中でimportしないほうがいいかも
    → 相対パスなら許容可能
  • HTML内のsrc/hrefはルートからの絶対パスで書いておく
    → 相対パスだとパターンがぶれるし、思わぬ参照先に。どうしても相対ならCSSにする
  • HTML内でスタイル指定しない
    → パスの書き換え問題。同じ理由でタグ要素にstyle属性も困る。

トリッキーなことしないで、メンテナンス性を考慮したページの作り方をしてれば、CDNへの振り向けはすんなり行きやすいという感じですね。

目指せ Ultra-Fast ASP.NET!

2011年12月27日火曜日

新しい.NET PaaS

Iron Foundryっていうのがあるらしいですね。Tier 3っていうIaaSの会社が、Cloud Foundry(vmware発のオープンソースPaaSフレームワーク)を利用して、.NET PaaSを自社クラウド上のサービスとして提供。

Tier 3 Contributes .NET Framework Support for Cloud Foundry™ Platform as a Service to the Open Source Community « Tier 3 Company Blog

InfoQ: Tier 3がCloud Foundryで.NET Frameworkをサポート

この記事を見るまで、Cloud Foundryを全然しらなかったっす。面白いですね。IaaSのレイヤに依存しないPaaSフレームワーク。

なるほどー。面白そうついでにどんなものなのか、ちょびっとだけ触ってみました。触っただけなので中身はないです。具体的に知りたい(Cloud Foundryそのもの)場合は、ソースを見るといいかもね!

IronFoundry's Profile – GitHub

いろいろ試してたんですけどね、勝手が分からないっていうのもあるかもしれないけど、とにかく情報がNai!どーなんよ!

まずはIronFoundryのサイトからCloud Foundry ExplorerとVS2010用のVisual Studio Extensionをダウンロードしてインストール。

Sign upしてもSign inページもなくて「どーこー」。まぁね、サイトにサインインする場所なんか無いわけです。すべてツール経由。

手順はコミュニティサイトに書かれてるのでその通りにどーぞ。

Using the Cloud Foundry Explorer for Windows : Iron Foundry

ちなみに単体起動したCloud Foundry ExplorerとVS起動したものとで、プロファイルが共有されてないっぽい。ので、試すときはVSからの起動だけにしてしまいましょう。入力が2度手間。

んで、何より初期選択肢にIronFoundryのURLが登録されてない!(api.gofoundry.net)ので、そこから自分で登録。凄いね!さすが出たばっかり。その辺はご愛嬌。

試しに起動したサイトは↓こちら。

たけはらの国

分かりやすくServerVariables全部書き出しときました。

どういう設定になってるかというと↓こんなです。

if 

クリックして拡大してね。

右下がインスタンス。メモリ512MBのインスタンス4つ起動しました。なんでかというと、アカウントに2048MBシバリがあるから。それ以上のメモリを必要とするインスタンスは起動できなかったです。LOCAL_ADDRの値がInstanceのHostアドレスと同じものが繰り返し表示されることが確認できるのできちんと4つのインスタンスに振り分けられてるのがわかります。

if2

メモリは↑この中から選択して起動できました。インスタンスの合計が2048MBを超えなければいいので、インスタンス毎にメモリ容量が違っても起動できます。なるほどね。これって、どういうことですかね。AppDomainのメモリ上限指定なんでしょうか。超えるとリサイクルされるアレ。それともWeb.configに指定してるのかな?と思って、ちゃんとWeb.configがどういうふうになってしまうのか(変更されるはず)を見るためのページも用意。

if3

どこまで隠したほうがいいのかよく分からないから、一応それっぽいところはモザイク。各自で確認してみてね!

DBがSQLServer認証だから、ConnectionStringにuidとpwdが埋まったものが自動追加されてて、AppSettingsにイロイロ追加されます。AppSettingsの値を見て挙動を制御するといいようですが。なるほど。でも項目の意味がわかりません!

えと、パッと見、Web.configにはメモリ制限らしきものは無いですね。VM自体に割り当てるメモリ量なのかなー。それにしては起動がめちゃめちゃ早いのが気になる。その辺Cloud Foundryに詳しい人なら知ってるのかな。まぁ、いいや。

VCAP_SERVICES/VMC_SERVICESそれぞれの値の中にplan:freeっていうのが見えます。無料お試しアカウント!

最初のスクリーンショットの真ん中したが各種サービスの登録で、選択できるのはMSSQLとMongoDB。

if4

でも、MongoDBはエラーになって作れませんでした。んじゃ、MSSQLは?と思ってどんどこ作って行ったら10個までは何も言われずに作成できちゃいましたよ。どこまで作れるんだろ。怖くなって10個でやめちゃった。実際には、ServicesのアイテムをドラッグしてApplication Servicesにドロップして初めて利用可能になるみたい。なので、そういうふうにしたら、Web.configに"Default"って言う名前で接続文字列が追加されました。ココはまだちょっと不具合があるみたい。なんかちゃんと生成した文字列が追加されてる気がしない。DBの容量がいくらに制限されてるのかの情報も見つけられない...。すげーぜ!

ソースを確認してみると、ちゃんとサービスのクレデンシャルからInitial Catalogを指定するようになってたり、すでに"Default"って言う名前のものがあったら、そこに追加する(SqlConnectionStringBuilderを使ってる)ようになってるんだけど、そういう動きをしてくれない。

しょうがないので、接続文字列は自分で追加しておきました。追加される接続文字列を参考にDefaultConnectionっていう名前でね。なんか、VSからのデプロイ時に確定させたらPaaSのメリットが~、とは思うけど、いつかきっとちゃんと動くようになるはずだから、今はこれでもいいかな。ちなみにVSの発行機能とは別の機能(Cloud Foundryの機能)でデプロイされるから、Web.configの構成変更が全然動いてくれない。これもまた残念。ソースがあるんだからカスタムしちゃえばいいかもしれないけどね。試したいだけだからスルー。

で、結局Initial Catalogが分からないので、SQL実行ページを用意して、SQLServerに問い合わせ(Universal ProviderがInitial Catalog必須。デフォルトデータベースに指定があってもダメなんす)。

if5

なるほどね。そんな文字列なのか。ん?これってVCAP_SERVICESのcredentialsのnameの値。なるほど。ソースは正しくかかれてるんだね。ちゃんと生成してくれればいいのに。

後は、MachinKeyをちゃんとWeb.configに書きたして、晴れてログインまで動くようになりました。Sessionは...。まぁ、いいや。

Web.configの表示とSQL実行のコード

[Authorize(Users = "許可したいアカウント")]
public class SecretController : Controller
{
  public ActionResult Config()
  {
    var config = XDocument.Load(Server.MapPath("~/web.config"));

    return View(config);
  }

  public ActionResult Database()
  {
    var connectionString = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
    ViewBag.ConnectionString = connectionString;

    return View();
  }

  [HttpPost]
  public ActionResult Database(string sql)
  {
    if (string.IsNullOrWhiteSpace(sql))
      return View();

    var response = new StringBuilder();
    var connectionString = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
    ViewBag.ConnectionString = connectionString;

    using (var connection = new SqlConnection(connectionString))
    using (var command = new SqlCommand(sql, connection))
    {
      connection.Open();

      var reader = command.ExecuteReader();
      var table = new DataTable("result");
      table.Load(reader);

      var writer = new StringWriter(response);
      table.WriteXml(writer);
    }
    ViewBag.Database = response.ToString();
    return View();
  }
}

雑...。どんまい。

ConfigのView

@model System.Xml.Linq.XDocument

@{
    ViewBag.Title = "Config";
}

<h2>Config</h2>
<pre>
@Model.ToString()
</pre>

DatabaseのView

@{
    ViewBag.Title = "Database";
}

<h2>Database</h2>
ConnectionString: @ViewBag.ConnectionString
<h3>SQL</h3>
@using (Html.BeginForm())
{
    @Html.TextArea("sql",new{cols=80,rows=10})
    <button type="submit">実行</button>
}
<h3>Result</h3>
<pre>
@ViewBag.Database
</pre>

Iron Foundry自体ソースが公開されてるから、Tier 3じゃなくても自分で環境作って(SQLServer最新にしてMongoDBも最新にして、とか待たなくても自分で用意すれば解決するし)、実行環境にすることもできるので、コレはコレで面白い選択肢。知っといて損はなさそーですね。今後にも期待できそうだし!

追記

追跡調査の結果、メモリ制限はリサイクルのプライベートメモリ制限でした!

2011年12月17日土曜日

MongoDBを本格利用するために

MongoDB使ってますか?RavenDBのほうがいいですか?それともCassandraですか?いやいや、NoSQLなんて使わない派ですか?RDBよりも使い勝手のいいところがイロイロあると思うので、積極的につかてみてはどうでしょう。

先日MongoDBのSessionStateStoreに手を加えて、高速化してみたわけですが、そもそもMongoDBをどこに構築しておきましょう。自社内サーバー?DC内サーバー?いろいろ悩ましいところだと思いますが、ココは思い切ってAWS EC2に構築するのはどうでしょう。最近AzureでもOpennessでMongoDBが取り上げられてましたね。それもいいですけど、今回はEC2です。

おまたせしました。楽しいMongoDBの時間デス(1/5):企業のIT・経営・ビジネスをつなぐ情報サイト EnterpriseZine (EZ)

MongoDBも2になってからはジャーナルが標準だし、ReplicaSetとShardingもあるし、アクセス認証もかけれるしで、良い感じですよね!

がしかし!つい最近までは致命的な問題があったんです。認証使えるはずなのに、Sharding環境で認証をかけてるとC# Official Drivderが例外おきて接続できなくて、実質認証を使えないという問題。DC内ならいいけど、クラウドに持って行こうと思うと、これが非常に都合が悪い。パフォーマンスを考慮すると大変都合が悪かったです。

そもそもは、なんでMongoDBを運用環境に利用しようとしたか、ですけど、そんな話はどーでもいいですね。ちなみになんで運用環境に持って行こうと考えたかというと、SQLServerのミラーリングのコストが馬鹿にならなくなってきたから。あとログを1箇所に集約したかったっていう理由も大きい。イベントログめんどくね?フォワーディングするにしても、操作しにくいし。DBのマシン性能が低いっていう問題があったんだけど、そこに投資しないと解決出来ないってわけでも気がしたんですよね。違うアプローチで解決出来そうだとゴーストがささやいた。もちろん投資すれば一発解決。大人の解決方法です。でも、なんかNoSQLで解決できる気がしたんすよ。そういうことあるでしょ?

TraceListner into MongoDB
MongoDBにASP.NETのSessionを格納する
MongoDBにASP.NETのSessionを格納する - 完結編  

ASP.NETのセッションデータと、トレースログをSQLServer以外に保持したい。そのために必要なパーツは用意して、パフォーマンスとか可用性とか拡張性とか検証して行けそうだと感じたら、どこに構築しましょうか、ってことになりますよね。DC内に構築するのがネットワーク的にも近いし、効率いいのはそうなんですけど、んじゃサーバーどこから調達するのさー、ってなるし、よく分からないものに投資してくれろ、とは言いづらいしそもそも投資するならまずDBだろ、みたいな事になって行ったり来たり。むふー。とりあえずは、仮想マシン用意してその中にMongoDBの構築を行うっていうところでスモールスタートするわけですが、それも時限式。余裕ない。こうなったら、本格的にクラウドで構築するしかねー!MongoHQっていう選択肢も無いわけじゃないけど、いっその事、AWS EC2にがっつり構築するのがいいんじゃないかと、一人プロジェクトスタート。クラウド使ってるっていうとなんかカッコイイ気がするじゃない。

イロイロ試した結果、ReplicaSetとShardingでmongodインスタンスを6個(rsを3台ずつ2シャード)、mongosインスタンス4個、コンフィグインスタンス3個。MongoDBだけでまずは13個。それとDNSが2台。コンフィグ2台とDNS2台は相乗りにして全13台。DNSはAWS内部での名前解決に使う目的と、既存ドメインのサブドメインで外向きの名前解決用。既存のDNSの管理者権限なんて欲しくないし、弄りたくもないので、既存DNSにはゾーン委譲だけしてもらうようにして、AWS用のサブドメインを用意しました。その方が自分で好きなように制御出来るもんね!なぜmongosが4個なのかって?スモールスタートなら2個でいいじゃん!と思うところですが理由は別。

予算の関係もあって、すべてマイクロインスタンスで構築。なので、DC内からのアクセスでVPCは除外。VPNが簡単でよかったんだけど、そもそもDCにVPNルータを設置する余裕すらない。スペースと電源ね。大丈夫だとは思うけど、EC2がマイクロじゃないと予算が辛い。だってこの時点ではまだ自腹。そんなに稼いでませんから!

で、まぁ、今のMongoDBのバージョン(2.0.1)じゃShardingで認証が出来ないってところにたどり着いてガックリしてたわけです。

[#SERVER-3929] Arbitrary replica set authentication "need to login" errors. - MongoDB

しょうがないよね。できないものは出来ないと諦めて、IP制限で接続制限かけることにしました。アクセス経路はDCからAWSなので暗号化はしておきたい。でもVPCは厳しい。そうなるとIPSecかSSL tunnelか。ELBにSSL tunnel出来る機能があるので、それがいいなと思ってたんだけど、ELBはIP制限かけられない。インスタンスに届いた時のIPはELBのIPだし、SSL tunnelだとHTTPなわけでもないからX-Forworded-Forなんか無いし。そうなるとIPSecでマシン間接続の暗号化かってことになって、IPSecの設定の勉強から。認証できれば一気に解決なのにー、と思いつつできないものはしょうがない。

なので結局選択肢はなくIPSec。初めてIP セキュリティポリシー使いました。ウィザードの使い方をちゃんと理解すれば大丈夫だった。あとネゴシエーションのためにUDP500,4500開けるのを忘れずに。この話長いから省略。

ちなみにMongoDBにアクセスするすべてのマシンにIPSecを登録するのは大変なので、Delegateを利用してポートフォワード。コレはコレで懐かしい感じですね。NIFTYの時以来。

DeleGate Home Page (www.delegate.org)

やっと、経路がセキュアになって、IP制限をかけれるようにもなったので、EC2で構築したMongoDBへのパフォーマンステスト。ただ残念ながら、IPSecしないのに比べて随分遅い。4:3くらいの比率で遅い。暗号化のコストが発生するからmongosがヒーヒー言ってる...。

先ほどmongos4台用意したといったのには意味があって、外向き(IPSecで外部から接続する場合)と内向き(AWS内のMonogDB以外のインスタンスからの接続用、暗号化しなくていいもんね)でそれぞれ構築しないとパフォーマンスしんどいからです。なので、外向きはIPSec対応mongos、内向きは素のmongos。それぞれ2台ずつ用意して計4台。

IPSecを使ったら遅いのはCPUがパワー不足なのも原因だからインスタンス大きいの使うしかないのかなー、と思いつつもそれでもSQLServerに入れるよりは早いし、まぁ、いいかと。いつの日か先のMongoDBの不具合が改修された暁には再度設定しなおそうと思い、まずはこれで開始。

いろいろ大変だね。OpenSSL使って証明書を作ったり、Delegateでゲートウェイ用意したり、IPSecの接続環境作ったり、そもそもMongoDBの環境作ることも含め、やること山盛り。それらのHowTo書こうと思うと大変面倒なので全てまとめて省略!勉強になったよ。MongoDBでReplicaSet+Shardingでの認証のやり方(ReplicaSetで個別にPRIMARYにユーザー登録して、mongos起動後configサーバーにもユーザー登録、最後にauthとkeyFile指定してサービス化。ちなみにそこまではコンソール起動でちょこちょこ調整しながら作業しました)がわかりにくかった。これまでのバージョンでうまくいってない部分だったみたいだからやむなし。インフラ屋って大変ね。

OpenSSL: The Open Source toolkit for SSL/TLS

今年ももう十分やりきっただろ。と、思った矢先にMongoDB2.0.2リリースのお知らせ。

Downloads – MongoDB

数日前にRC2が出たときには今年はもうないな、と思ってたのに。ウズウズ。ワクワク。ちなみにRCで先の不具合が解消されてたのは確認済みだったので、リリースをひたすら待つだけだったんですけど、まさかこのタイミングで出てくるとはねー。

こうなるとELBを利用してSSL tunnel(ELBではSecureTCPっていう名前)を利用する環境を用意してしまおうじゃないかとなりましょう。ReplicaSet+Shardingで認証かけれれば、後は経路の問題だけだし、何よりELBで暗号化復号化してくれればmongosが通信に専念すればいい状態になるし、更にはELBがラウンドロビンでmongosを選んでくれるから、パフォーマンスは大幅向上。1.25倍(2倍近くさばける時もある)くらい早くなりました。ELBだとアクセスが増えると受け口を自動でスケールしてくれるので、負荷がもっと高くなると、5分くらいで自動でよしなにしてくれるらしい。5分だっけ?そこはいいか。そうするとIPSec用mongosは用なし。さらにEIPでグローバルも不要。DNSにELBのCNAMEを登録すればいいだけになって更にお買い得。

運用の目処もたち、Reserved Instanceで見積もったり(超間違えたけど)して、最終的にやっと会社が払ってくれるようになって一安心。DBもWebもある程度はAWSに持っていっちゃう予定だけど、最終形態はDCとクラウドのハイブリッドが良い感じに思えますが、一般的にはどうなんだろ。

今はまだテスト環境のセッションストアと、トレースログの取得だけにしか使ってないけど、徐々に全てのトラフィックをDC内のMongoDBからAWSのMongoDBに変更していく予定です。どーなるかなー。楽しみだなー。

せっかくなのでPPT用の素材を使って今回の構成図を書いてみたよ。

MongoDB 2.0.1でIPSecの時

スライド1

MongoDB 2.0.2でELBの時

スライド2

あは。センスないー。素材があってもコレじゃ...。なんとなくでも伝わるといいな。mongosからReplicaSetをShardingとして参照するように設定してるのをうまく表現できませんでした...。

ところで、CloudWatchって数値高めに出てない?実際ログインして直接パフォーマンス見てみると、CloudWatchで表示されるほど高くないんだけどなー。なんだろ。そんなもの?

2011年11月20日日曜日

MongoDBにASP.NETのSessionを格納する - 完結編

前回のあらすじ

不思議な光る石のペンダントをつけた少女が空から降りてきた。天使が舞い降りたと思い走って近づく少年。その少女は伝説となった天空の城の王の末裔。少年もまた亡き父の言葉を信じ、天空の城の存在を信じていた。悪意を持った別の王の末裔が少女をさらい、城を悪用しようとすることから、少年は少女と城を守るため、海賊とともに空に旅立った。

ドキドキするストーリーですね!

...。

続きです。最終的に接続エラーが出て、処理が継続できない状態に陥るっていうところでした。問題解決する前に、現時点でのパフォーマンスを確認してみます。

前回同様Apache Benchで実行しますが、10000回だとエラーになってしまって計測に失敗してしまうので、桁を下げて1000回(-n 1000 –c 8)としてましょう。

計測方法はabを3回実行してRequest per secondの平均を取ります。

InProc

ms1

1回目:538.39
2回目:588.10
3回目:660.41

平均:595.63

StateServer

ms11

1回目:525.43
2回目:567.28
3回目:599.09

平均:563.93

SQLServer

ms2

1回目:282.39
2回目:370.53
3回目:352.21

平均:335.04

MongoDB

ms3

1回目:129.71
2回目:146.35
3回目:145.03

平均:140.36

うん。早くない。ちなみに無印UniversalProviderだと 30.15 とドンマイな感じ。

Universal Provider

ms4

ちなみに、5回で平均を取りたかったけど、MongoDB版が5回目で接続エラーになって取れなかったので3回にしました。これは良くない!まずはちゃんと10000回普通に動かせるようにしないと。

と、いうわけで、MongoDB C# Official Driverのドキュメントに目を通してみると、内部でConnection Poolingを利用していると書かれています。が、挙動からはPoolがちゃんと利用されてないんじゃないかと、疑ってみました。 だって、接続エラーって...。使いまわしてるならそんなことにならないはず、という根拠のない診断。うさんくさいドクターハウス。

CSharp Driver Tutorial – MongoDB

MongoDB C# Driver API Documentation - Table of Content

詳細はドキュメントから判断出来なかったので、ソースを追いかけてみました。オープンソースだといろいろ調べれていいですねー。

mongodb/mongo-csharp-driver – GitHub

チラチラ見た結果からいうと、MongoServerクラスに保持しているMongoServerInstanceがConnectionPoolを管理していて、MongoServer.Disconnectを呼ぶとすべてのPoolをクローズして回る仕組みのようです。なんでだろ。理由はいいとして、そういう作りなんだということは、finallyで必ずDisconnectを呼び出すのはよろしくないかなー。そんな事しなくても、Poolに保持しているものは10秒ごとにMaxConnectionLifeTime/MaxConnectionIdleTimeを確認して破棄されるようになってるみたいなので、ほっといてもいいんじゃね?みたいな。

と、いうわけで、ソース中のすべてのDisconnectを削除します。

その状態で、abで確認します。

3回実行した結果は525.43 / 599.09 / 572.34。平均:565.62。ワォ!一気に上がりました。せっかくなので10000回出来るかどうかもチェック。

ms5

行けるようになりましたー。ちゃんと接続を使い回してくれてるようです。パフォーマンスも接続を使いまわすことで一気に改善しました。んじゃ、なんで、そういう作りにしてなかったのか?っていうのが気になります。

理由はありました。どういうことかというと、Poolが保持しているMongoConnectionは生成された時点でReplicaSetのPrimaryを判定して保持しています。つまり、Failoverした直後、Poolが破棄されない限り接続エラーがおきて、正しくデータの書き込みができなくなるということでしょう。

abを実行し、その最中にstepDownでFailoverしてみます。

ms6ms7

Safemode detected an error 'not master'. (Response was { "err" : "not master", "code" : 10056, "n" : 0, "lastOp" : NumberLong("5676991341247070789"), "connectionId" : 13356, "ok" : 1.0 }).

masterじゃないダニ!と言われ、書き込みできないエラーが発生しました。SafeModeじゃなくても同じようにエラーになることは確認済み。

と、得意げにドヤ顔したところで、それはつまりDisconnectしなくなったからでしょ?と、おっしゃるとおりな理由でのエラーなんですけどね。つまり、Failoverした後もちゃんと接続が継続できるようにするために毎回Disconnectしてたわけですね。

あちゃー、これはどうしたものか。なんて、あたかも今、解決策を思いつくような振りになってますけど、事前に調査してわかってることを書いてるだけなんで...。てへ!

つまり今回のエラーの原因はPoolから取り出したMongoConnectionがPrimary(Master)を指してないから例外が起きるわけです。そして、MongoServer.Disconnectを呼ぶことでPoolのすべてのコネクションが破棄されるというのはソース見て確認済み。つまりMongoDBに対する処理で例外が発生したらMongoServer.Disconnectを呼び出し、処理をリトライすることで、Failover後に決定したNodeに接続するようにしてあげればよい、という方法が取れましょう。MaxConnectionLifeTimeを短くしてみては?っていうのもあるでしょうが短すぎると接続エラーになりそうだし。

で、用意したのが以下のようなリトライ実行するヘルパー。

public class MongoDbInvokeSetting
{
  public string ServerName { get; set; }
  public string DatabaseName { get; set; }
  public int RetryCount { get; set; }
  public int RetrySeconds { get; set; }
}

public class MongoDbHelper
{
  /// <summary>
  /// リトライを繰り返すInvoker
  /// </summary>
  /// <param name="setting"></param>
  /// <param name="functor"></param>
  public static void RetryInvoker(MongoDbInvokeSetting setting, Action<MongoServer> functor)
  {
    var retry = setting.RetryCount;
    Exception exception = null;
    while (retry > 0)
    {
      var server = MongoServer.Create(setting.ServerName);
      try
      {
        functor(server);
        break;
      }
      catch (MongoException ex)
      {
        exception = ex;
        // ↓こいつでPool内の全コネクションクリアが走るはず。
        server.Disconnect();
      }
      retry--;

      // とりあえずn秒待ってみる。
      Thread.Sleep(setting.RetrySeconds * 1000);
    }

    // ここでイベントログに書き出すか、Exceptionを出力する必要がある。
    // 後続の処理に進んだら困るならException。
    // ログなら進んでもいいかもねー。
  }
}

ダサくない!ダサくないよ!これでいいんだもん!

このコードを書いてる途中、さすがに心配になったんですけど(これでいいのかっていうね)、そんなおり以下のページを見つけました。

File: REPLICA_SETS — MongoRuby-1.4.1

似たようなリトライさせてますね。だもんで、いいってことで。

これを使って実行する書き方は↓こう。

MongoDbHelper.RetryInvoker(GetSetting(), server =>
{
  // serverを使ってCollection検索したり、
  // データ追加したり...
});

GetSettingは設定を入れたものを返しましょう。サーバー名とかリトライ回数とか。

これつかって丸っとしたもので、実行しつつFailoverさせてみます(abでエラーが出ないことを確認しました)。

ms8

大成功!エラーも起きずFailover成功しました。データも欠落なくいけてます。

で、この状態でのパフォーマスンスを測定してみます。

3回実行したのが485.62 / 496.92 / 552.61 平均:511.72

遅くなったじゃん!っていう感じしますけど、あったまって無かったみたい。実質Disconnect削除のパフォーマンスと同じ数値でした。

でも~、これだと~、なんていうかパンチが弱い。十分いいとは思うんだけど~。どこか早くできそうなところないかなー、とMongoSessionStateStoreのソースを見ているとありましたねー。

private string Serialize(SessionStateItemCollection items)
{
  using (MemoryStream ms = new MemoryStream())
  using (BinaryWriter writer = new BinaryWriter(ms))
  {
    ...
    return Convert.ToBase64String(ms.ToArray());
  }
}

private SessionStateStoreData Deserialize(HttpContext context,
string serializedItems, int timeout)
{
  using (MemoryStream ms =
    new MemoryStream(Convert.FromBase64String(serializedItems)))
  {
    ...
  }
}

ほら、ここ。Base64に変換してstringにしてるじゃないですか。コレいらなくない?BsonBinaryArray使えばbyte[]をそのまま入れることが出来るんじゃないのー?と、気になったので試した結果が↓こちら。

3回実行したのが 542.95 / 538.68 / 534.19、 平均:538.61。微妙...。まぁ、いいか。

ms9

ちなみにこのままだと、Expireしたセッション削除が残り続けるので、SetAndReleaseItemExclusiveの中で2000回に一回くらいクリーンアップするように仕込んでみました。その状態で10000回実行。

ms10

10000回中5回クリーンアップしてるけど、まぁまぁ。外部タスクにしてしまえば影響はでないものなので、ここはいいでしょう。

ち・な・み・に、SQLServerだと平均335.04ですからね。いいじゃないですか。ねぇ。Shardingしてみても同じマシンだとあまり変化でなかったです。と、いうのもCPUを最も使ってるのがIISExpressのプロセスだったので、アプリケーションが遅いってことですから。あとは、マシン分けて計測しかないですが、少なくとも同一環境内ではMongoDBでのSessionStateStoreがStateServerに負けないくらいの速度を実現しました(何度か試したらStateServerはもっと早かったけどー、そこは情報操作!)。

2011年11月19日土曜日

MongoDBにASP.NETのSessionを格納する

ASP.NETのSessionといえば

  • InProc(アプリけーションプロセス内のインメモリステート管理)
    シリアライズコストも発生せず、同一プロセス内で言わばstatic Dictionary<string,object>の実装で最速。ただし、アプリけーションのリサイクルと同時にセッションが破棄されるのと複数プロセス間で共有できないです。
  • StateServer(専用プロセスによるインメモリステート管理)
    InProcの弱点である、アプリけーションのリサイクルによる破棄とプロセス間共有を実現出来るようにしたもの。プロセスをまたぐのでシリアライズコスト発生するけど、インメモリなのでInProcにつぐ速度を実現。ただし、シングルプロセスでの実装になるので、耐障害性という意味では弱い。ステートをホストしているマシンの障害や再起動などでセッションが破棄されてしまいます。
  • SQLServer(ストーレジに保持するステート管理)
    StateServerの弱点であり、耐障害性をクリアしたストレージ保存型でのセッション管理。シリアライズコスト+DBサーバーのパフォーマンス次第で遅くもなるし早くもなる。
  • Custom(SessionStateStoreProviderBaseを派生したカスタム実装)
    キャッシュクラスタ(Velocity)を利用したり、EntityFrameworkを利用した実装(UniversalProvider)を使ったりSessionStateStoreに対してイロイロです。DBをSQLServer以外にする場合にも利用されてます。

選択肢多くていいですね。標準のものもそれぞれ優秀で素晴らしいです。SessionIDの生成や、クライアントとのやり取りは別の仕組みで実装されてるので、SessionStateStoreProviderのカスタムとは分離されてて安心です。

MongoDB ASP.NET Session State Store Provider | AdaTheDev

最近、すっかりMongoDBに心奪われてる身としては、SessionをMongoDBに入れてしまいたいという衝動にかられてます。あれやこれやの問題もあり、SQLServerだけにたよるソリューションだとよろしくないなというのもありまして。

MongoDB、大変優秀ですね。ReplicaSet(レプリケーション)による可用性の確保(自動フェールオーバー有り)と、Sharding(データのパーティショニング)によるパフォーマンスの確保、素晴らしいです。

いろいろ試して部分的に利用を始めてる段階で、まだまだ自信をもって使いまくるって言う状況ではないですが、ちょっとずつです。ちょっとずつ使っていってSQLServer+MongoDBで行けるところまで行ってみようと企んでます。

まずはTraceListnerのMongoDB化。前回のエントリでそれっぽい感じのものを提示しましたが、これでサーバー群のトレース情報を低コストなストレージに保持出来るようになりますね。トレース情報くらいなら万が一情報欠落しても致命的になることも無いでしょう。しかし!セッションはそういうわけにはいきませんね。システムとして提供している表の機能に影響がでちゃいますからね。

そうなると、ReplicaSetの機能を利用することは必須となりましょう。そうするとFailoverの時の挙動を確認したりする必要も出てきますよね。そういうテストを繰り返してこそですよね。楽しい時間ですね。

先ずはReplicaSetでMongoDBを2つ起動。ArbitarっていうSQLServerでいうところのウィットネスサーバーを1台追加してないと投票結果が偶数とかになって次のPrimary決定時に困っちゃうことがあるので、3ノード起動します(詳しくはオフィシャルサイトでどーぞ)。

rem arbitar
start "arbitar" c:\mongo\bin\mongod.exe --port 27020 --replSet mongo --dbpath c:\mongo\dba

rem replicaset
start "mongo1" c:\mongo\bin\mongod.exe --rest --noauth --port 27031 --replSet mongo --oplogSize 20 --dbpath c:\mongo\dbr1
start "mongo2" c:\mongo\bin\mongod.exe --rest --noauth --port 27032 --replSet mongo --oplogSize 20 --dbpath c:\mongo\dbr2

これで起動してるので(c:\mongoに一式ある前提です)、まずは初期化。

27031のノードにMongoDB Shellで接続。

mongo localhost:27031

初期化コマンド順番に実行すればOKです。

rs.initiate()
rs.add("hostname:27031")
rs.add("hostname:27032")
rs.addArb("hostname:27020")

ms

まずはこの状態で公開されてるソースを利用してSessionを使ってMongoDBにデータを入れてみましょう。

MVC4DPでアプリケーションを作り、Home/IndexアクションでSessionに値をいれて、Home/Indexビューで表示するだけのものです。

public ActionResult Index()
{
  ViewBag.Message = "Modify this template to kick-start your ASP.NET MVC application.";
  Session["message"] = ViewBag.Message;

  return View();
}
<h2>from session:@Session["message"]</h2>

MongoSessionStateStore/MongoSessionStateStore.cs at master from AdaTheDev/MongoDB-ASP.NET-Session-State-Store - GitHub

↑ここからソースを取得して、プロジェクトに追加。後はNuGetでMongoDB Official Driverを入れておきましょう。

ms2

コメントに従い、接続文字列を指定するとレプリカセットにならないので、今回起動したMongoDBを指すように接続文字列を変更。

connectionString="mongodb://localhost:27031,localhost:27032/"

ms3

ちゃんと出来てますね。

MongoVUEで27031(primary)データを確認。

ms4

入ってるねー。

続いて27032(Secondary)のデータを確認。

ms5

もちろん入ってますね。

ここで、ReplicaSetを入れ替えましょう!

MongoDB ShellでPrimaryにつないで rs.stepDown() で強制フェールオーバーを実行。

ms6

ちゃんと切り替わりました。

この状態で再度先ほどの起動したブラウザでF5でリロード。ちゃんと動くなら、これでFailoverしたほうを参照して、表示されるはず!

ms7

ms11

タターン!うまくいきました。内部ではSafeModeっていう書き込み確認モードで動作するようになってるので、手堅いです。

27032(SecondaryからPrimaryに変更したノード)でSessionsコレクション(テーブルですね)を確認して見つつブラウザのF5リロードを繰り返すと、ちゃんとLockIdがカウントアップしていくので、読み込みも書き込みもFailover後にちゃんとできてるのが確認できます。

だがしかし!ここで大問題が!!

パフォーマンスを測定しようとApache Benchでリクエストを投げまくってみると、最後まで完了せずにエラーで途中終了してしまいます。

ms9

ms8

ms10

Unable to connect to the primary member of the replica set: システムのバッファー領域が不足しているか、またはキューがいっぱいなため、ソケット操作を実行できませんでした。

なかなかの男気あふれる強気のエラーメッセージ。
どーしたんだMongoSessionStateStore!
これが精一杯なのかOfficial Driver!

長くなったのでつづく...。

2011年10月29日土曜日

URLRewriteのoutboundRulesでセッションIDを含んだHTML内のURLを普通のURLに戻す、リベンジ

タイトル長っ!

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

ちょっと前にエントリしました。が、間違えてました。思いっきり。これを信じてくれた人すいません。リベンジです!今度はこないだのよりだいぶマシ。

目的は、OutputCacheを利用する際にCookieless URL(セッションIDとかを含んだURL)をHTMLに保持してると、他の人とセッション共有しちゃうから、それを防ぐ!です。セキュリティ的に守らなきゃ、という理由だけでページ全体をno-cacheにするのはあまりにも富豪。

さらに、gzipで動的コンテンツを圧縮することで、CPUは多めに使うことがあるけど、OutputCacheで相殺。もちろんレスポンス性能が劇的に向上するので、ユーザーにとってはいいことづくし。

<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="(.*)/\([SFA]\([^/]+\)\)/(.*)"/>
      <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>

これが、たぶん正解のRewrite用のconfig。ちゃんと確認してみます。

せっかくなので、MVC4DPを利用してみましょう(意味なくはない)。プロジェクト作って実行すると表示される画面は↓こうですね。

rewrite1

この時のURLは http://localhost:61972/ です。

このままではやりにくいので、sessionStateでcookieless="UseUri"にします。

rewrite2

見た目が変わるわけじゃないです。URLを見てくださいね。今度は http://localhost:61972/(S(l2ulbdgxdrbdoyrozczknp1p))/ となっていますね。この状態でソースを確認します。

rewrite3

バッチリセッションIDがURLに含まれてますね。先ほどのconfigをsystem.webServer内に追記するとどうなるか。

rewrite4

セッションIDが消えました!概ね...。部分的に残ってる部分があるんですけど、それは多分仕様。と、いうのもAタグのhref属性の前にdata-dialog-title属性が入ってますよね。これがあるとURLRewriteが対象だと判断してくれないみたいです。試しに、_LogOnPartial.cshtmlを変更してみます。

@if (Request.IsAuthenticated) {
    <p>
        Hello, @Html.ActionLink(User.Identity.Name, "ChangePassword", "Account", null, new { @class = "username" })!
        @Html.ActionLink("Log Off", "LogOff", "Account")
    </p>
} else {
    <ul>
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink", data_dialog_title = "Registration" })</li>
        <li>@Html.ActionLink("Log on", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "logonLink", data_dialog_title = "Identification" })</li>
    </ul>
}

↑こっちがオリジナル。で、↓こっちが修正版。違いはhtmlAttributesのdata_dialog_titleの有無。

@if (Request.IsAuthenticated) {
    <p>
        Hello, @Html.ActionLink(User.Identity.Name, "ChangePassword", "Account", null, new { @class = "username" })!
        @Html.ActionLink("Log Off", "LogOff", "Account")
    </p>
} else {
    <ul>
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log on", "LogOn", "Account", routeValues: null, htmlAttributes: new { id = "logonLink" })</li>
    </ul>
}

そうするとレンダリングされるHTMLは↓こうなります。

rewrite5

RegisterとLog onのURLからもSessionIDが消えて正しくなりました。htmlAttributesで追加した属性はTagBuilderで展開されるときにアルファベット順に出力(SortedDictionary)されるんですね。なので、hrefより先にdata属性が展開される、と。この辺、どうするんでしょうね。正しくはURLRewriteのOutboud Providerが対応することなんだと思うけど...。

とりあえず、今のところスルー。さーせん。

ちなみに前回はこれを仮想ディレクトリ配下にデプロイせずに「出来たできた~」と浮かれてて、実はちゃんと消えないっていうダメっぷり。あと、SessionIDだけじゃなくCookielessの場合は認証チケット(F)も匿名ID(A)もURLに含まれるのにSだけ見てて、これまたちゃんと消えないっていうダメダメっぷり。

今回はちゃんと確認。

rewrite6

ローカルIISのmvc4dpにデプロイ。URLは view-source:http://localhost/mvc4dp/(S(cow2znspocwggst1mah50myt))/ です。これもちゃんとHTML中のURLからはちゃんとSessionIDが消えました!

でー。この状態で今度はアウトプットキャッシュをOnにします。そのためにHomeControllerに以下の追記。

    [OutputCache(Duration = 60)]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {

これをVS実行環境で見てみる。

rewrite7

ちゃんと出力キャッシュが効いてる証に、max-ageとExpiresが出てますね。

ローカル環境でもキャッシュの有無で少しだけ、結果が違いますね。

rewrite8 rewrite9

少しだけね。で、このキャッシュされてるHTMLにはもちろんSessionIDは含まれてません。

ココからさらに動的圧縮をOnにするために以下の記述を追加。

    <urlCompression
      doStaticCompression="true"
      doDynamicCompression="true"
      dynamicCompressionBeforeCache="false" />

と、いいたいところですが、残念ながらこれはこのままでは機能しないんです。

URL Rewrite Outbound Rules w/ Compression : The Official Microsoft IIS Site

↑ココに書かれてる通り、 レジストリに項目追加が必要です。切ないですね。でも、まぁ、いいでしょう。

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\InetStp\Rewrite]
"LogRewrittenUrlEnabled"=dword:00000000

これをレジストリに追加。

で、いざ!と思いきや...。

rewrite10

ざんねーん。IIS Expressではどうもこのレジストリの値を見てくれないみたいです。対策も特に見つけられなかったです。なのでローカルのIISにデプロイしたほうで試します。

rewrite11 rewrite12

たたーん!出力キャッシュも効いてるし、gzipも効いてます。さらにHTML中のSession IDを持ってるURLもなくなってます!Content-Lengthが5.77KBから2.61KB。

標準(追加モジュールですけどMS謹製)でもココまで出来ました。

これ以上は独自のResponse Filterを書いてHTML中のURLを操作する方法になるでしょう(もちろんURL RewriteのProviderを実装という手もあるけど、それ書くくらいならFilterのほうが低コストじゃないですかねー)。

大規模サイトもコレで安心ですね。

ちなみに、ですけど、すべてのサイトでコレを設定すれば早くなるわけじゃないので用法・用量を守って正しくお使いください。