この記事について

この記事は Movable Type Advent Calendar 2013 の3日目の記事です。

概要

Data API をキャッシュする仕組みとしては既に PHP を使ったものなどがあるようですが、この記事では .htaccess を使い、以下のような方針でキャッシュする方法を紹介します。

  • クライアント側の設定 (mt-data-api.cgi へのエンドポイント) を変更をせずにキャッシュを利用できる
  • 共用サーバーでも利用できる
  • プログラムのレイヤまで降りずに Apache から直接データを返すことができる

サンプルコード

.htaccess とプラグインのサンプルコードが以下の URL から参照できます。

DataAPIPublicCache

ここからはサンプルコードを参照しながら説明をしていきます。

キャッシュの戦略

キャッシュをする際にまず考慮しなくてはならないのが、以下の点になります。

  • MT 側でデータが更新されたらキャッシュがクリアされること
  • 閲覧に認証が必要なデータをキャッシュしてしまい、未認証のユーザーに見られてしまうことがないこと

この記事で紹介する方法では、それぞれ以下の方針で対応をしています。

  • データが更新された場合にはプラグインを使ってキャッシュをクリアする
  • 閲覧に認証が必要なデータはキャッシュしない

.htaccess の戦略

静的なファイルを mt-static/support/data-api-public-cache/ 以下に書き出し、mt-data-api.cgi/v1/... へのアクセスを .htaccess を使ってマッピングして、ファイルが作成済みの場合にのみそれを返すようにします。

キャッシュファイルへのマッピングは一般的にはURLとパラメータキーにして、それを MD5 などでハッシュ化することが多いと思いますが、.htaccess ではそういった操作はできないので基本的にはパラメータをそのままファイル名にしています。

ただパラメータを直接ファイル名にすると任意の名前でファイルを作成できてしまい「/..」などで問題が発生しそうだったので、キャッシュする際にパラメータの文字列をホワイトリストで制限した上で、「/..」と「%」をエンコードする形をとっています。

ファイル名の制限について

ファイル名に関して一つのポイントとして、OS やファイルシステムにより文字数に制限がある場合があります。一般の UNIX 的な環境だと、個々のディレクトリやファイルの文字数の制限は 255 文字程度のようなので、名前が制限内に収まるように適当にディレクトリに分割する必要があります。(トータルの長さは 255 文字を超えても問題ないので)

またディレクトリを含めたトータルの長さについても、1023 文字などの制限がある場合があるようです。手元の Linux 環境では特にこの点は工夫をしなくても Apache 側、Perl 側、共に1023 文字より長くても大丈夫でしたが、Mac 環境では Apache 側でファイルを見つけられなくなりました。この記事ではこの問題に関しては解決策を用意できていないので、この問題にあたった場合には制限事項となっています。

.htaccess の内容

.htaccess の内容は以下のようなものになります。

RewriteEngine On
RewriteBase /mt/

RewriteCond %{HTTP:X-MT-Authorization} =""
RewriteCond %{REQUEST_METHOD} =GET
RewriteCond %{QUERY_STRING} ^[a-zA-Z0-9!""\$\%\&\'\(\)\=\~\^\|\\\{\[\`\@\}\]\*\:\+\;\_\?\/\>\.\<\,-]*$
RewriteRule data-api.cgi/(v.*) mt-static/support/data-api-public-cache/$1/%{QUERY_STRING}c.js [N,E=HTTP_X_DATA_API_PUBLIC_CACHE_TRY:1]

RewriteRule (mt-static/support/data-api-public-cache/.*?)%(.*c.js)/v\d+ "$1 $2" [N]
RewriteRule (mt-static/support/data-api-public-cache/.*?)/\.\.(.*c.js)/v\d+ $1/_..@$2 [N]
RewriteRule (mt-static/support/data-api-public-cache/.*?)(/[^/]{250})([^/].*c.js)/v\d+ $1$2/$3 [N]

RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{ENV:HTTP_X_DATA_API_PUBLIC_CACHE_TRY} =1
RewriteRule mt-static/support/data-api-public-cache/(.*c.js)/v\d+ mt-data-api.cgi [L,E=HTTP_X_DATA_API_PUBLIC_CACHE_FILENAME:$1]

RewriteCond %{SCRIPT_FILENAME} !-f
RewriteCond %{ENV:HTTP_X_DATA_API_PUBLIC_CACHE_TRY} !=1
RewriteRule mt-static/support/data-api-public-cache/ - [R=403,L]

静的ファイルの書き出し

基本的には単純にプラグインで Data API の出力をフックし、未認証の場合にファイルに書き出すだけです。

書き出すファイル名の生成は Perl 側で実装すると .htaccess とルーチンが重複するので、.htaccess から渡すようにします。ただその際、リクエストパラメータとして渡すと外部からファイル名を指定できてしまう可能性があり危険なので、環境変数として渡すようにしています。(そのため PSGI 環境では動作しません、CGI 環境でのみ動作します)

環境変数として渡す場合に一つ注意が必要なのは、CGI が SuExec 環境で実行されていると渡すことのできる名前に制限があるという点です。任意の値を渡す場合には HTTP_X_ で始まるようにする必要があります。

性能の比較

パラメータを MD5 でハッシュ化して readfile するような簡単な PHP を追加して、「この記事の .htaccess を使った方法」「PHP」「CGI」「PSGI」で、いくつかのパラメータのパターンでリクエストにかかる時間を比較してみました。

fields=id,title,excerptfields=...&search=あfields=...&search=あ...(5文字)fields=...&search=あ...(10文字)
.htaccess 0.394 0.424 0.474 0.565
PHP 0.46 0.469 0.469 0.464
CGI 408.793 422.225 421.653 417.161
PSGI 57.613 64.643 62.714 63.373
  • "ab -c 1" で計測した "Time per request" の値です
  • .htaccess と PHP に関してはキャッシュは作成済みの状態での値です
  • 開発環境の MacBookPro で計測した簡易的なものなので、絶対値にはあまり意味はありません

.htaccess と PHP の比較

.htaccess と PHP の結果をグラフにしたものが以下の図になります。

htaccess-php.png

PHP はパラメータの長さによらずほぼ一定の速度ですが、.htaccess の方は長くなるに従って (特に%が増えることによって) 内部でのリダイレクトの回数が増えるために急激に遅くなっています。

パラメータが長いことを前提とした場合には、以下の様な書き方をすることである程度速度の低下を抑えることもできますが、.htaccess の記述が増える分パラメータが短い場合の性能が悪化してしまいます。

RewriteRule (mt-static/support/data-api-public-cache/.*?)%(.*?)%(.*?)%(.*?)%(.*?)%(.*?)%(.*?)%(.*?)%(.*c.js)/v\d+ "$1 $2 $3 $4 $5 $6 $7 $8 $9" [N]
RewriteRule (mt-static/support/data-api-public-cache/.*?)%(.*?)%(.*?)%(.*?)%(.*c.js)/v\d+ "$1 $2 $3 $4 $5" [N]
RewriteRule (mt-static/support/data-api-public-cache/.*?)%(.*?)%(.*c.js)/v\d+ "$1 $2 $3" [N]
RewriteRule (mt-static/support/data-api-public-cache/.*?)%(.*c.js)/v\d+ "$1 $2" [N]

この記事の方法を使う限り、パラメータが長くなるにつれて性能が大きく悪化するのは避けられなそうです。

参考URL

次の記事について

次の記事(4日目)を書く人がまだ決まっていないようです!どなたかよろしくお願いいたします。

Google Maps v3 でなぜか shadow がつかないとき

  • 投稿日:
  • by

2013年11月9日現在、

<script
  type="text/javascript"
  src="http://maps.googleapis.com/maps/api/js?sensor=SET_TO_TRUE_OR_FALSE">
</script>

で読み込むと、オーバーレイ のサンプルに倣って以下のように指定しても、

new google.maps.Marker({
    position: myLatLng,
    map: map,
    icon: image,
    shadow: shadow
});

shadowが有効にならないようです。

<script
  type="text/javascript"
  src="http://maps.googleapis.com/maps/api/js?v=3&sensor=SET_TO_TRUE_OR_FALSE">
</script>

とすると有効になります。

どうやら、

  • v=n を指定しない -> Experimental Versionがロードされる
  • v=3 を指定する -> v3のRelease Versionがロードされる

という動作になっていて、現在のExperimental Versionである3.14.12だと、サンプルにある指定方法だとshadowが反映されないバグ(か仕様変更)があることが原因のようです。

つまり、v=nを指定していない場合には以前は表示されていたとしても現在は表示されていないかもしれないので、Google Map v3でshadowを使っている人は確認した方がいいかもしれません。

MTとPSGIミドルウェア

  • 投稿日:
  • by
  • カテゴリ:

MTでは5.2.4からレジストリを使ってPSGIミドルウェアを各アプリケーションに適用できるようになっているので、この仕組を使ってMTを拡張する方法をいくつか考えてみました。(検証したのはMT6 Beta1です)
公式ドキュメント

PSGIミドルウェアとは

PSGIミドルウェアとは、PSGIアプリケーションに対してリクエスト時の環境やレスポンスを動的に書き換えるための仕組みです。PSGIミドルウェアを使うことでレスポンスのHTMLを書き換えたり、認証のレイヤーを別に挟んだりということができます。MTはプラグインによる拡張の方法が充実しているので、機能の拡張としてはそれで事足りる部分も多いですが、

  • PSGIミドルウェアの資産(MTに限らない)を利用することができる
  • 拡張するためにMTの知識を必要としない

という点がPSGIミドルウェアを利用するメリットであると思います。

逆にMTでPSGIミドルウェアを使う場合にあらかじめ考慮しておくべきところとしては、

  • そもそもPSGIアプリケーションとして実行していないと(CGIで実行している)と利用できない。

という点があります。

以下、ミドルウェアの適用例をいくつか紹介していきます。

MTのアカウント情報を使ってBASIC認証を設定

PlackにはPlack::Middleware::Auth::Basicというミドルウェアが同梱されていて、これを使うと簡単にBASIC認証を設定することができます。以下のような config.yaml でプラグインを作成すると、

  • mt-search.cgi
  • mt-data-api.cgi

に対してBASIC認証が必要になり、MTアカウントの「ユーザー名」と「APIパスワード」が送られてきた場合にだけ結果を得ることができるようになります。

「options」にサブルーチンを渡すパターンとしては「code」と「handler」があり、この違いが少しややこしいですが、

  • code: 初期化時に一度だけ実行され、サブルーチンから返された値がミドルウェアに渡される
  • handler: コードのリファレンスがミドルウェアに渡される

という違いがあります。

プラグイン: P-M-Auth-Basic

デバッグ情報を表示

開発時に有用なミドルウェアもたくさん公開されており、Plack::Middleware::DebugRequestParamsはその一つです。以下のような config.yaml でプラグインを作成すると、環境変数のDebugModeが有効である場合に、リクエストパラメータの一覧が標準エラー出力(多くの場合にはstarmanを実行した端末)に表示されるようになります。

「condition」はPlack::Builder::add_middleware_ifに渡され、リクエストの度に評価されます。

ライセンス的に問題のないミドルウェアであればプラグインのextlibに同梱することもできるので(他のプラグインとのバージョンのコンフリクトは気にする必要はありますが)、配布する場合にはexlibに同梱してしまうのもMTらしい形だと思います。

プラグイン: P-M-DebugRequestParams

mt-search.cgiの検索条件にエイリアスを設定

PSGIミドルウェアを使えることの最大のメリットは既存のPSGIの資産を利用できることだと思いますが、専用のPSGIを新しく書くことも簡単です。例えば以下のようなコードを書くと、mt-search.cgiの検索条件へのエイリアスを作成することができます。

config.yamlを以下のように書き、

mt-config.cgiで以下のように設定すれば、

http://host/mt/search/food

で、

http://host/mt/mt-search.cgi?search=food&IncludeBlogs=1&limit=20

にアクセスしたのと同じ結果が得られるようになります。

プラグイン: P-M-AliasedSearch

サーバーのステータスを取得

PSGIミドルェアの中に、Plack::Middleware::ServerStatus::Liteというサーバーのステータスを取得するものがあります。以下のような config.yaml でプラグインを作成するとServerStatus::Lite を適用することができるのですが、mt.cgi/server-status のようなURLになってしまうので、これはあまりいい感じではありません。

PSGIサーバー全体に適用するような場合には「サクッとPSGIなMTOSの開発環境を用意する方法」でも紹介されているような、専用の.psgiファイルを作成して起動する方がいいと思います。

その他

その他、MTと一緒に利用できそうなプラグインをいくつか紹介します。

  • Plack::Middleware::JSONP
    • JSON形式で返されるレスポンスを、JSONPで呼び出せるように変換します。
    • DataAPIからのレスポンスに適用することができそうです。
  • Plack::Middleware::Deflater
    • レスポンスデータをGzipやDeflate圧縮します。