miso_soup3 Blog

主に ASP.NET 関連について書いています。

イベント手記 既存のASP.NETサイトをお手軽に速くする 編

2015-09-26(土)Comm Tech Festival にて、@takepara さんと登壇しました。
セッションのタイトルは「ASP.NETサイトの運用~既存のASP.NETサイトをお手軽に速くする 編~」です。
ここでは簡単にその内容をおさらいしてみます。

大まかな内容

セッションのテーマはタイトル通り、ASP.NETで構成された既存のサイトをお手軽に速くしよう、というものでした。
アプローチは

  • 既存サイトは Web.config 以外一切触らない
  • CDN を使う

の2つです。

本来 Web アプリケーションを速くするには、インスタンスを追加したり、キャッシュサービスを使用したり、バックエンドやフロントの最適化を行ったり等いろいろあります。
が、セッションでは”サイト内のプログラムは一切手を加えない”、”お手軽に安く、一番効果のある方法”にこだわりました。
対象のサイトは http://ufcpp.net です。ほぼ HTTP GET のみの ASP.NET で構成されたサイトです。

対策前後の計測

対策前

まずは現状の速度を測ります。
wget で、サイトをとあるサイズ分クロールします。

wget64.exe -mirror --secure-protocol=TLSv1 --no-check-certificate -Q 5242880 -P contents {サイトのアドレス}

f:id:miso_soup3:20150928061619p:plain
F12 の開発者ツール↓
f:id:miso_soup3:20150928061441p:plain
453ms...うっ...

対策後

で、後述する対策(CDN の用意と Web.config の変更)を行った後、サイトをデプロイします。同じコマンドで計測。

上が1回目で、下が2回目。2回目以降は同じような速度になります。
f:id:miso_soup3:20150928061325p:plain
f:id:miso_soup3:20150928061507p:plain
453ms が 51ms へ。

1回目が対策前と変わらないのは、プル型の CDN がオリジンにコンテンツを取得しにいっているためです。2回目以降は、CDN に保持されているので速くなります。
(数値自体は環境によって変化します。)

対策 ― CDN の準備と Web.config

対策の内容です。
まず、CDN を 2つ用意します。画像や CSS 等の素材を配信するための KeyCDN と、サイトをまるごとかぶせる Amazon CloudFront CDN です。

f:id:miso_soup3:20150928051414p:plain

CDN にはプッシュ型とプル型がありますが、今回はどちらもプル型として利用します。なので、CDNに対してファイルのアップロードは一切行っていません。
(プッシュ型はあらかじめ画像等を CDN のサーバーに配置しておく方法。プル型は CDN がオリジンサーバーに対して自分でファイルを取得しておく方法です。この説明は、CDN の種類についてをどうぞ。)

プル型の場合、どれくらいキャッシュするか等の設定をオリジンの HTTP Header でコントロールする必要があります。
また、画像を CDN から配信するために a タグや css タグの URL を、CDN のパスに置き換える必要があります。

これらのために、次にあるように Web.config にて設定します。

Web.config

以下のコードは、Web.config の system.webServer 要素の配下に書きます。

1.静的コンテンツの有効期限

<staticContent>
    <clientCache cacheControlMaxAge="0.01:00:00" cacheControlMode="UseMaxAge" setEtag="false" />
</staticContent>

次に、outboundRules で、HTTP のレスポンスの内容を書き換えます。(
https://technet.microsoft.com/ja-jp/library/Ee791853(v=WS.10).aspx 参照
)この Outbound Rule については、@takepara さんのURLRewriteでほとんどすべてCDNに向けるに詳細があります。

<rewrite>
  <outboundRules>

    <!-- 2.部分オフロード用のコンテンツパス書き換え -->
    <rule name="HttpCdnPathAbsolute" preCondition="IsHtml">
      <match filterByTags="Img, Link, Script" pattern="^/[^/]+(.*)$" />
      <action type="Rewrite" value="//hogehogehoge.kxcdn.com{R:0}" />
    </rule>
    <rule name="HttpCdnPathRelativeSlashEnd" preCondition="IsHtml">
      <match filterByTags="Img, Link, Script" pattern ="^((?!/.*)(?!//.*)(?!http.*)(?!https.*))(.*)$" />
      <action type="Rewrite" value="//hogehogehoge.kxcdn.com{PATH_INFO}{R:0}" />
      <conditions>
        <add input="{REQUEST_URI}" pattern ="/$" />
      </conditions>
    </rule>
    <rule name="HttpCdnPathRelativeUnslashEnd" preCondition="IsHtml">
      <match filterByTags="Img, Link, Script" pattern="^((?!/.*)(?!//.*)(?!http.*)(?!https.*))(.*)$" />
      <action type="Rewrite" value="//hogehogehoge.kxcdn.com{PATH_INFO}/../{R:0}" />
      <conditions>
        <add input="{REQUEST_URI}" pattern="[^/]$" />
      </conditions>
    </rule>
    <!-- 3.動的コンテンツの有効期限 -->
    <rule name="Private Cache To Public" preCondition="IsHtml">
      <match serverVariable="RESPONSE_Cache-Control" pattern="private" />
      <action type="Rewrite" value="public, max-age=300" />
    </rule>

    <!-- 4.リダイレクト先書き換えとno-cache  -->
    <rule name="Redirect No Cache">
      <match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
      <conditions>
        <add input="{RESPONSE_STATUS}" pattern="^(301|302)" />
      </conditions>
      <action type="Rewrite" value="no-cache" />
    </rule>
    <rule name="Remove Host in Redirect From CloudFront">
      <match serverVariable="RESPONSE_LOCATION" pattern="^http://[^/]+/(.*)" />
      <conditions>
        <add input="{RESPONSE_STATUS}" pattern="^(301|302)" />
        <add input="{HTTP_USER_AGENT}" pattern="^Amazon CloudFront" />
      </conditions>
      <action type="Rewrite" value="/{R:1}"/>
    </rule>
    
    <!-- ↑のルールで使用している IsHtml を定義しています -->
    <preConditions>
      <preCondition name="IsHtml">
        <add input="{RESPONSE_CONTENT_TYPE}" pattern="^text/html" />
        <add input="{RESPONSE_STATUS}" pattern="^[45].*$" negate="true" />
      </preCondition>
    </preConditions>
  
  </outboundRules>
</rewrite>

5.圧縮で doDynamicCompression を false に。

<urlCompression
     doStaticCompression="true"
     doDynamicCompression="false"
     dynamicCompressionBeforeCache="false" />
Web.config 詳説

セッションでは省きましたが、Web.config で書いた設定について掘り下げます。

1.静的コンテンツの有効期限

<staticContent>
    <clientCache cacheControlMaxAge="0.01:00:00" cacheControlMode="UseMaxAge" setEtag="false" />
</staticContent>

これは静的コンテンツの HTTP Response Header にて、Cache-Control:max-age=**** を指定します。
setEtag 属性は、IIS 8.0 から有効で、ETag の設定を指定します。規定値は true ですが、今回は Cache-Control によるコントロールを明示するために false としました。

クライアント キャッシュClient Cache
(※IIS のドキュメントの日本語は IIS 7.0 で止まっているため、必ず英語のドキュメントも併用して確認した方が良いです。)

2.部分オフロード用のコンテンツパス書き換え

以降は、URL Rewrite の Outbound Rule を使用します。HTTP Response の書き換えを指定するものです。
Outbound Rule の 参考:

    <!-- 2.部分オフロード用のコンテンツパス書き換え -->
    <rule name="HttpCdnPathAbsolute" preCondition="IsHtml">
      <match filterByTags="Img, Link, Script" pattern="^/[^/]+(.*)$" />
      <action type="Rewrite" value="//hogehogehoge.kxcdn.com{R:0}" />
    </rule>
    <rule name="HttpCdnPathRelativeSlashEnd" preCondition="IsHtml">
      <match filterByTags="Img, Link, Script" pattern ="^((?!/.*)(?!//.*)(?!http.*)(?!https.*))(.*)$" />
      <action type="Rewrite" value="//hogehogehoge.kxcdn.com{PATH_INFO}{R:0}" />
      <conditions>
        <add input="{REQUEST_URI}" pattern ="/$" />
      </conditions>
    </rule>
    <rule name="HttpCdnPathRelativeUnslashEnd" preCondition="IsHtml">
      <match filterByTags="Img, Link, Script" pattern="^((?!/.*)(?!//.*)(?!http.*)(?!https.*))(.*)$" />
      <action type="Rewrite" value="//hogehogehoge.kxcdn.com{PATH_INFO}/../{R:0}" />
      <conditions>
        <add input="{REQUEST_URI}" pattern="[^/]$" />
      </conditions>
    </rule>

2.では、HTML 内の Img, Link, Script の要素の URL 指定の部分を、CDN のパスへ書き換えています。
Outbound Rule で HTML の中身も書き換えられるの~とびっくりしましたが、これを使用すると HTTP 圧縮の関係で、後述する urlCompression の設定も必要になります。

3.動的コンテンツの有効期限

<rule name="Private Cache To Public" preCondition="IsHtml">
    <match serverVariable="RESPONSE_Cache-Control" pattern="private" />
    <action type="Rewrite" value="public, max-age=300" />
</rule>

先ほどの静的コンテンツの設定と同様に、動的コンテンツの Cache-Control のヘッダーを書き換えています。この設定は、Web.config でなくても、.csHtml や Global.asax 等で↓のように書くこともできます。

var cacheTimeSpan = new TimeSpan(0, 10, 0);
Response.Cache.SetExpires(DateTime.Now.Add(cacheTimeSpan));
Response.Cache.SetMaxAge(cacheTimeSpan);
Response.Cache.SetCacheability(HttpCacheability.Public);

4.リダイレクト先書き換えとno-cache

    <rule name="Redirect No Cache">
      <match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
      <conditions>
        <add input="{RESPONSE_STATUS}" pattern="^(301|302)" />
      </conditions>
      <action type="Rewrite" value="no-cache" />
    </rule>
    <rule name="Remove Host in Redirect From CloudFront">
      <match serverVariable="RESPONSE_LOCATION" pattern="^http://[^/]+/(.*)" />
      <conditions>
        <add input="{RESPONSE_STATUS}" pattern="^(301|302)" />
        <add input="{HTTP_USER_AGENT}" pattern="^Amazon CloudFront" />
      </conditions>
      <action type="Rewrite" value="/{R:1}"/>
    </rule>

この設定は、対象のサイトにて リダイレクト 301 を多用していたのでそれの対策です。リダイレクト 301 を返す HTTP Response の Location (つまりどこの URL にリダイレクトするか)を、オリジンではなく、CDN のパスへ向かうよう変更しています。オリジンにリダイレクトされてしまうと、サイトオフロードではなくなってしまう…CDN 配信による効果が得られないためです。

f:id:miso_soup3:20150928053956p:plain

対象のサイトでは、URL Rewrite により(Outbound Rule ではなく、Inbound Rule の方URL 書き換えモジュールの送信ルールの作成 の前半)、URL-A でアクセスした際に URL-B へ 301 Redirect するようあらかじめ設定されています(以下がそのコード)。

<rule name="Redirect">
	<match url="~URL-A~" />
	<conditions logicalGrouping="MatchAll">
		<add input="{HTTP_CONTENT_TYPE}" pattern="^text/html" /> 
	</conditions>
	<action type="Redirect" url="~URL-B~" redirectType="Permanent" />
</rule>

で、この場合 Location に指定している URL-B は、相対パスを指定したとしても実際に出力される Location の値は ホスト付きの URL になるので、オリジンへとリダイレクトされます。
(※上記の設定の url="~URL-B~" のところで、http://fuga.com/** とホスト付きで指定した場合は、出力される Location の値はもちろんそのホストの URL になります。ここで、CDN のパスを指定する方法もありますが、今回は既存のコードには一切触らないというルールでしたのでスルーします。)

オリジンにリダイレクトされては困るので、Outbound Rule で Location の値を相対パスに書き換えます(ここで CDN のホストを指定しても OK)。

余談ですが、RFC 2616では Location には絶対パスを指定する、となっていますが、後の RFC 7231 では相対パスも OK となっています。

5.圧縮

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

最後に Url Compression の設定です。
上記で Outbound Rule により HTML を書き換えていますが、このままだとほぼ必ず Web.config のエラーになります。それを避けるために doDynamicCompression="false" を指定しています。
詳しくは、@takepara さんのURLRewriteのoutboundRulesでセッションIDを含んだHTML内のURLを普通のURLに戻す、リベンジ や、URL Rewrite Outbound Rules w/ Compressionを。

Url Compression は、圧縮して(gzip)送信するかどうかを指定するところです。

  • doStaticCompression
    • 静的コンテンツを圧縮するか。規定値は true。
  • doDynamicCompression
    • 動的コンテンツを圧縮するか。規定値は true(IIS 7.5 より true になりました。IIS 7.0 までは規定は false)。
  • dynamicCompressionBeforeCache
    • 出力キャッシュに入れられる前に動的に圧縮されるかどうか。規定値は false。

HTML を書き換える Outbound Rule と、doDynamicCompression="true"(規定値) を併用するとエラーになる模様。レジストリを弄らなくてはいけないらしいがホスト環境では行えなかったので false を指定しています。HTML の書き換えと圧縮のタイミングがどうのこうのとか…難しい。他の値は規定値をそのまま設定しています。

URL 圧縮URL Compression
(例によって日本語サイトには 7.5 以上のことは書かれていません。英語コンテンツの Compatibility 欄は重要なので注意です。)

最後に

と、最後に速くなった様子をデモしたかったのですが、3つほど原因が重なって失敗…。

今回の内容は@takeparaさんによるものでした。教授と助手という設定で、衣装・コント・スライドに織り込みました。どうしても、どうしてもコントされたかった様子…。事前にシナリオを3本くらいいただきました。

セッションをするために@takeparaさんに内容を隅々まで教えていただきましたので、今回はそれが一番勉強になりました(ほとんどコントと衣装の打合せだったけど)。