目次


Varnish を使って PHP アプリケーションのスケーリングを行う

リバース・プロキシーを配備してサーバーの容量を増加させる

Comments

World Wide Web の歴史は短いかもしれませんが、その仮想世界を眺めてみると、もう既にデジタル機器がゴミとなって大量に散乱しています。倒産した大量のドット・コム企業の色あせたロゴが散乱し、使われなくなった (あるいは回収された) サーバーは放置されてホコリが積もり、シリコン・バレーからシリコン・アレーに至る、ほとんどすべての人が大げさに昔の話をします。「そりゃあ、俺らがまだ若かった頃には WYSIWYG エディターなんて便利なモンはなかったからな。HTML は全部手打ちしてたし、それが楽しかったよ。そうだな、まだボー・レートなんて言ってた時代だな」

幸いなことに、World Wide Web は 1990年代中頃のすべてが不便だった時代から、さまざまなものが大きく変わりました。デザイナーは開発者と同じように、Web サイトを作成するための便利なツールを手にしています。PHP を含むスクリプト言語は便利であり、また CakePHP (「参考文献」を参照) のようなフレームワークのおかげで、コーディングのあらゆる段階の作業を高速に行うことができます。また、需要に合わせてサイトのスケーリングを行う方法も学びました。もっと帯域幅が必要ならば利用している回線を太くし、もっと処理速度を上げたいならクロック・サイクルを高め、提供するページ数を増やしたければ Web サーバーの数を増やします。

もっとサーバーが必要でしょうか。必要なのかもしれませんが、それができるのは、有り余るほどのお金を持っていればの話です。

実際には、サイトのスケーリングを行う方法は数多くあり、サーバーの数を増加させることは (多くの場合は現実的で必要な方法ですが) その中の 1 つの方法にすぎません。別の方法として、既存のサーバーを配置しなおし、過大な受信トラフィックを緩和する方法があります。この方法の考え方の核心は、なぜ何度も何度も新たにページを生成する必要があるのか、という点です。生成されたページが何秒間も、あるいはもっと長く有効であるケースが数多くあります。この方法のポイントは、2 番目、3 番目、そして10,000 番目の訪問者がその URL を訪れた時に、即座にそのページを見られるように維持するところにあります。

ここでは、リバース・プロキシーと呼ばれる優れたソフトウェアと PHP を組み合わせてページをキャッシュし、サーバー・リソースを節約します。

なぜリバース・プロキシーなのか

コンピューターのメモリー・キャッシュと同様 (あるいはPHP のオペコード・キャッシュと同様)、リバース・プロキシーを利用すると、リワークの必要がなくなり、また頻繁に要求されるデータをより迅速に送信できるようになります。

具体的には、リバース・プロキシーは Web クライアントと Web サーバーとの間に立ち、受信される各 HTTP と、それに対応する HTTP レスポンスをキャプチャーします。そして、リクエストとそれに対応するレスポンスの内容に応じて、リバース・プロキシー自身があたかも正真正銘の Web サーバーであるかのように動作します。場合によると、リバース・プロキシーは受信されるリクエストを単にそのまま Web サーバーに渡すこともあります。しかし一方で、リバース・プロキシーがリクエストそのものを処理することもできます。

キャッシュされた HTTP レスポンスはいわゆるフォーム・レターと考えることができます。リバース・プロキシーは単純に、ある特定のリクエストへの応答としてフォーム・レターを送信します。あるアセット (例えばページまたは画像) に対する 2 番目、3 番目 (等々) のリクエストは、最初のリクエストと同じレスポンスを受信します。(この交換の例を図 1 に示します。)

図 1. 架空のリバース・プロキシーが同じレスポンスを多くのクライアントと共有する
架空のリバース・プロキシーが同じレスポンスを多くのクライアントと共有する
架空のリバース・プロキシーが同じレスポンスを多くのクライアントと共有する

図 2 は、クライアント、サーバー、リバース・プロキシーの間の関係を示しています。Web クライアント (例えば Firefox や Safari など) は、外部に公開されている Web「サーバー」にポート 80 を使って接続します。この「サーバー」は実際にはリバース・プロキシーです。このプロキシーのみが、ポート 2001 をとおして実際の Web サーバーに接続することができます。もしプロキシーがリクエストに対応できない、あるいはリクエストに応じることがキャッシング・ルール (このすぐ後に説明します) で許可されていない場合には、プロキシーは Web サーバーに処理を委ねます。また、キャッシング・ルールとリクエストのタイプ次第で、プロキシーはレスポンスをキャッシュし、クライアントに転送する場合もあります。

図 2. Web クライアント、リバース・プロキシー、Web サーバーの間の関係
Web クライアント、リバース・プロキシー、Web サーバーの間の関係
Web クライアント、リバース・プロキシー、Web サーバーの間の関係

リバース・プロキシーによるキャッシングの仕組みには、送信処理を高速化できること以外にも多くの利点があります。例えば次のようなものです。

  • リバース・プロキシーが維持し、提供するキャッシュによって、Web アプリケーションの負荷を軽くすることができます。レスポンスを生成する上で必要となる計算の実行頻度が減少します。これは、一般的なリクエストは (新たなレスポンスが必要になるまで) キャッシュによって処理されるためです。
  • リバース・プロキシーによってデータベース・サーバーの負荷を軽くすることができます。大部分の Web アプリケーションはデータベースに依存するため、各リクエストには 1 つあるいは複数のクエリーが含まれています。リクエストの数が減少すれば実行するクエリーの数も減少し、データベース・サーバーの動作が速くなります。実際、全体としてデータベース接続が不足気味、あるいはデータベース・サーバーのレスポンスが遅い場合には、1 つあるいは複数のリバース・プロキシーをシステムに組み込むことで、さまざまな状況が改善されます。
  • キャッシュによってサーバーの負荷を軽くすることができます。コードとクエリーが減少するため、スループットを改善することができるのです。

メモリーはキャッシュのための最高の永続保存媒体です。アクセス時間は (事実上) 瞬時であり、また通常 RAM は豊富にある (あるいは安価に入手できる) ためです。しかし、ファイルシステムもキャッシュの保存媒体とすることができます。ファイルシステムはメモリーよりも圧倒的に豊富で安価ですが、アクセスはずっと低速です。

もちろん、Web 上ではアセットはすぐに変更されることがあり、実際すぐに変更されます。またキャッシュされたアセットもやがて古くなったり有効期限が切れたりします。各リクエストとレスポンスはそれぞれ独自の「賞味期限」を指定することができ、またキャッシュの各データの有効期限は、スケジュールに従って、通常はキャプチャー後数秒間で切れます。

キャッシュの内容は、各 HTTP リクエストとレスポンスの先頭にある HTTP ヘッダーによって操作することができます。ヘッダーによってアセットの有効期限を設定することができ、またキャッシングを回避させることができます。(クライアントやサーバー、プロキシー、その他のエージェントによる、複雑かつ巧妙で、時に矛盾するキャッシング方法については、この記事では触れません。) 実際、HTTP によるキャッシュ・コントロールは HTTP V1.1 のプロトコル仕様の一部です (「参考文献」を参照)。

Cache-Control ヘッダー

下記のかぎ括弧「」でくくった内容は、HTTP 1.1 プロトコル仕様の Section 13.1.3 と Section 14.9 を引用あるいは要約して内容を補い、組み合わせたものです (「参考文献」を参照)。『』はこの仕様で強調されている部分に対応させたものであり、この記事の編集上追加したコメントではないことに注意してください。

「場合によると、サーバーまたはクライアントは HTTP キャッシュに対して明示的な指示をする必要があるかもしれません。この仕様ではその目的のために Cache-Control ヘッダーを使います。Cache-Control ヘッダーを利用することによって、クライアントまたはサーバーはリクエストまたはレスポンスの中でさまざまなディレクティブを送信することができます。リクエスト/レスポンスのやりとりに関わるすべてのキャッシュ機構は、Cache-Control ディレクティブに従わ『なければなりません』。また Cache-Control ディレクティブは、すべてのプロキシーおよびゲートウェイのアプリケーションをパススルーし『なければなりません』。原則として、ヘッダーの値の間に明らかな矛盾がある場合には、最も制限的な解釈 (つまりリクエスト/レスポンスのデータをそのまま維持できる可能性が最も高いもの) が適用されます。」

従って、レスポンスに Cache-Control: no-cache が含まれている場合は、リバース・プロキシー、または中間に介在する他のエンティティーは、キャッシュされたレスポンスの内容を送信側のサーバーで再検証せずに新たなリクエストに対して送信することはできません。

別の例として、リクエストに Cache-Control: max-age=60 が含まれている場合は、クライアントは 60 秒を経過した古いレスポンスを受け取りたくないと宣言していることになります。

他にも、次のような便利なディレクティブがあります。

  • ディレクティブ public を利用すると、可能な限りあらゆる場所でレスポンスがキャッシュされます。対照的に private は、そのレスポンスはクライアントのキャッシュ (通常はブラウザーのキャッシュ) にのみ保存できることを意味します。
  • ディレクティブ must-revalidate は強制的にすべてのキャッシュにレスポンスを検証させます。サーバーが 304 Not Modified レスポンスを返し、何も変更がないことを示せば、そのレスポンスは有効なままです。それ以外の場合には、サーバーは新しい完全なレスポンスを返し、その新しいレスポンスによって、それまで保持されていたものを置き換える必要があります。このバリエーションである proxy-revalidate ディレクティブは、公開キャッシュを検証するように要求します。

注意: Cache-Control ディレクティブの一覧は HTTP 1.1 仕様の Section 14.9 にあります (「参考文献」を参照)。

ディレクティブを組み合わせることもできます (例えば Cache-Control: public,max-age=30 など)。

Cache-Control と並んで他にも 2 つのヘッダーがキャッシュによる保持を制御するために使われます。

  • Expires ヘッダーは GMT (Greenwich Mean Time: グリニッジ標準時) で有効期限を指定します。Expires ヘッダーと Cache-Control ヘッダーを組み合わせてキャッシングが許可されている場合には、アセットをキャッシュする期間を Expires によって制御することができます。しかし、max-age (あるいはその仲間である、公開キャッシュ用の s-maxage) が設定されていると、その設定値が Expires よりも優先されます。
  • Last-Modified ヘッダーは、そのアセットが最後に変更されたのはいつかを、これも GMT で示します。先ほど触れたように、このヘッダーはキャッシュの内容を検証するためにとても頻繁に使われます。

従って、PHP が生成するアセットをキャッシュするためには、Cache-ControlExpiresLast-Modified というヘッダーのうちの 1 つ以上を設定する必要があります。(なぜ HTML では十分にキャッシュをコントロールできないかについては、囲み記事「なぜ meta タグでは不十分なのか」を見てください。

Varnish をビルドし、インストールする

HTTP ヘッダーの動作を見るために、簡単な PHP アプリケーションの出力をキャッシュする HTTP リバース・プロキシーをビルドし、インストールし、そして実行してみましょう。Varnish は比較的新しいものですが、非常に高機能でハイパフォーマンスの HTTP リバース・プロキシーです。(詳しく学ぶためには Varnish が構築された経緯を Varnish のウィキで読んでください (「参考文献」を参照)。) また Varnish にはモニターと、完全なスクリプト言語である VCL (Varnish Configuration Language) が提供されており、振る舞いを微調整することができます。例えば下記のコード・スニペットは、通常は静的コンテンツを表す、ある種のファイル・タイプをキャッシュするように Varnish に命令します。

sub vcl_recv {
  if (req.request == "GET" && req.url ~ "\.(gif|jpg|swf|css|js)$") {
    lookup;
  }
}

大部分のオープンソース・パッケージと同様、Varnish は FreeBSD、Linux®、Mac OS X を含め、いくつかのプラットフォームに容易にビルドすることができます。また aptport などのパッケージ・マネージャーを使いたい場合には、いくつかのシステム用にバイナリー形式の Varnish を入手することもできます。この記事は 2007年12月時点での最新リリースである Varnish V1.1.2 をベースにしています。

まず Varnish の Web サイト (「参考文献」を参照) からソース・コードをダウンロードし、圧縮された TAR ファイルを解凍し、新たに作成される varnish-1.1.2 ディレクトリーにカレント・ディレクトリーを変更します。次に、スクリプト ./autogen.sh と ./configure を、この順序で実行します。(./configure スクリプトで想定されている構成は、通常は妥当なものです。しかしシステムの詳細に合わせてビルドをカスタマイズするためには、./configure --help と入力し、どのオプションが調整可能かを調べます。例えば、システムが、ローカルでビルドしたバイナリーを /usr/local ではなく /opt/local に保持している場合には、./configure --prefix=/opt/local を実行します。) そして最後に make && sudo make install と入力します。

リスト 1. ビルドとインストール
$ ./autogen.sh
+ aclocal
+ glibtoolize --copy --force
+ autoheader
+ automake --add-missing --copy --foreign
+ autoconf

$ ./configure
checking build system type... powerpc-apple-darwin9.1.0
checking host system type... powerpc-apple-darwin9.1.0
checking target system type... powerpc-apple-darwin9.1.0
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
...
config.status: config.h is unchanged
config.status: executing depfiles commands

$ make && sudo make install 
...
/usr/bin/install -c .libs/varnishd /usr/local/sbin/varnishd
...

ビルド・プロセスでは、C コードが 2 分か 3 分でコンパイルされた後、いくつかのバイナリーがローカルのユーティリティー・ディレクトリーにコピーされます。(varnishd はシステム・ユーティリティーなので、慣例的に /usr/local/sbin にコピーされることに注意してください。)

$ ls /usr/local/*bin/v*
/usr/local/bin/varnishadm /usr/local/bin/varnishreplay
/usr/local/bin/varnishhist  /usr/local/bin/varnishstat
/usr/local/bin/varnishlog /usr/local/bin/varnishtop
/usr/local/bin/varnishncsa  /usr/local/sbin/varnishd

ファイル varnishd は名前からもわかるように、Varnish のデーモン、つまりメモリーの内容をキャッシュし、提供する永続サービスです。上記のリストにある、それ以外のユーティリティーは、varnishd の動作の制御と監視を行います。例えば varnishstat は Varnish の統計を連続的に提供します。ファイル varnishadm を利用すると実行中の varnishd に管理コマンドを送信することができます。

varnishd はそのままの状態ではクッキーが含まれるレスポンスをキャッシュせず、また Cache-Control のディレクティブ privateno-cache が使えません。幸いなことに、ちょっとした VCL を利用することで、この問題を修正することができます (リスト 2)。このコードは Jean-François Bustarret が提供したものです (「参考文献」を参照)。

リスト 2. PHP に準拠させるための VCL のフラグメント
backend default {
  set backend.host = "127.0.0.1"; 
  set backend.port = "80"; 
}

sub vcl_recv {
  if (req.request != "GET" && req.request != "HEAD") {
    pipe;
  }
  if (req.http.Expect) {
    pipe;
  }
  if (req.http.Authenticate) {
    pass;
  }
  if (req.http.Cache-Control ~ "no-cache") {
     pass;
  }

  lookup;
}

sub vcl_fetch {
  if (!obj.valid) {
    error;
  }

  if (!obj.cacheable) {
    pass;
  }

  if (obj.http.Set-Cookie) {
    pass;
  }

  if (obj.http.Pragma ~ "no-cache" 
    || obj.http.Cache-Control ~ "no-cache" 
    || obj.http.Cache-Control ~ "private") {
    pass;
  }

  insert;
}

ではコードの中身をざっと調べてみましょう。

  • backend default というセクションは、コマンドライン・オプション (-b hostname:port ) が指定されない場合に、どのサーバーに接続するのかを指定します。
  • 関数 vcl_recv() は、このデーモンがクライアント・リクエストを受信すると呼び出されます。逆に vcl_fetch() は、リクエストされたオブジェクトが実際の Web サーバーから取得できた時、あるいは Web サーバーへのリクエストが失敗した時に呼び出されます。リストの中に書かれているとおり、vcl_fetch() は、Cache-Control ヘッダーまたは Pragma ヘッダーが no-cache に設定されていると、キャッシングを拒否します。
  • このコードの中で、pass という操作は、この個々のリクエスト/レスポンスの交換に対して「パススルー」、あるいは何もしないことを意味します。pipe もクライアントからサーバーへそのままデータをパススルーしますが、pipe 命令の後にある、クライアントとサーバー間のすべてのトランザクションに対してパススルー動作を行います。(pipe は、いずれかの側が接続を閉じるまで継続的に行われる pass 操作です)。lookup はキャッシュの中からレスポンスを見つけようとし、一方 insert はキャッシュにレスポンスを追加します。

この先を続けるためには、内容をファイル (例えば /usr/local/etc/varnish/php.vcl) に保存し、以下のコマンドを使って varnishd を起動します。

$ sudo varnishd -a localhost:8080 \
  -f /usr/local/etc/varnish/php.vcl

しばらくすると、次のような出力が表示されるはずです。

file ./varnish.einpln (unlinked) 
size 1069547520 bytes (1020 fs-blocks, 261120 pages)
Creating new SHMFILE

これで varnishd デーモンは接続準備が整いました。ターミナル・ウィンドウから varnishstat を実行します。図 3 からわかるように、varnishstate はこのデーモンが実行中であることを示しています (実行時間は最上部の左側に表示されます) が、まだ動作は記録されていません。キャッシュの空きバイト数は最下部に表示されます。

図 3. varnishstat でキャッシングと接続のアクティビティーを表示する
varnishstat でキャッシングと接続のアクティビティーを表示する
varnishstat でキャッシングと接続のアクティビティーを表示する

次に、何らかの動作を行います。ポート 8080 に接続し、皆さんの Web サイトをブラウズします。varnishd のモニターをよく見てください。いずれかのページあるいはアセットがキャッシュに現れたでしょうか。

キャッシュしやすい PHP にする

先に進む前に、最新バージョンの Firefox をダウンロードしてインストールします。インストールできたら Firefox を起動して add-ons ページを訪れ、Live HTTP Headers プラグインをインストールします (「参考文献」を参照)。Live HTTP Headers にはさまざまな機能がありますが、その中に、受信されるすべてのレスポンスの HTTP ヘッダーを表示する機能があります。(必要であれば、画像ファイルや CSS ファイルへのリクエストをフィルターで削除することもできます。) 促されたら Firefox を再起動し、Live HTTP Headers のウィンドウを開きます。

リスト 3 のコードを保存して Web サーバーがこのコードを見つけられるようにし、また Firefox でリバース・プロキシーによる新しい PHP ページのアドレスを指定してこのページを表示します。例えば、 URL が http://www.example.com/misc/cache.php だったとすると、http://localhost:8080/misc/cache.php のように指定します。同じURL に対して、数多くのブラウザーを使って、また wget を使ってアクセスしてみます。キャッシュが有効に使われていることがわかります。リクエストとリクエストの間で 20 秒間休止すると、明らかにキャッシュ・ミスが発生していることがわかるはずです。これは送出されるヘッダーに対してキャッシュの内容の有効期限が切れているためです。

リスト 3. キャッシュを操作するための PHP コードの例
<?php
  // 
  // Emit headers before any other content 
  // 
  cache_control( "public,max-age=10");
  expires( to_gmt( time() + 10 ) );
?>
<html>
  <head>
  </head>
  <body>  
    <?
     print to_gmt();
    ?> 
  </body>
</html>
<?php
  function to_gmt( $now = null ) {
    return gmdate( 'D, d M Y H:i:s',  ( $now == null ) ? time() : $now );
  }
  
  function last( $gmt ) {
    header("Last Modified: $gmt");
  }

  function expires( $gmt ) {
    header("Expires: $gmt");
  }

  function cache_control( $options ) {
    header("Cache-Control: $options");
  }
?>

もちろん、この簡単なアプリケーションは Hello World よりもほんの少し便利な程度ですが、この中には Varnish や他のリバース・プロキシーを活用するために必要なことがすべて網羅されています。たとえ大部分のページを動的に生成する場合であっても、少なくとも数秒間はそうしたページをキャッシュできる可能性が高く、その間のサーバーの負担を軽減することができます。あるいは逆に、他のページがキャッシングされないようにしたいこともあるかもしれません。ここまでの説明で、そのための規則を理解でき、そして Varnish というソフトウェアを使って、非常に価値のある最適化を実現できるようになりました。

成功への準備を整える

最近の World Wide Web のほとんどのページはハンドコーディングされることはなく、アプリケーションによって生成され、要求に基づいて提供されるため、各ページはカスタマイズされ、またパーソナライズされています。

しかし、それは魔法のように実現されるわけではなく、HTML を作成するために必要な時間と作業が、単に人間からマシンに移ったにすぎません。マシンは人間よりも桁違いに処理が速いかもしれませんが、有限のリソースであることに変わりはありません。賢明な PHP 開発者は、この事実を踏まえてスケーリングの計画をします。データベース・クエリーは効率的で、サーバーには冗長性があり、メモリーは効率的に使われています。そして今や動的なページをキャッシュできるようになりました。ではトラフィックを呼び込みましょう。


ダウンロード可能なリソース


関連トピック

  • Varnish が作られた経緯を Varnish のウィキで学んでください。
  • HTTP V1.1 プロトコル仕様の Section 13.1.3Section 14.9 について学んでください。
  • HTTP V1.1 プロトコル仕様を読み、HTTP ヘッダーについて、またサーバーとクライアントの間に介在する公開されたエンティティー、そしてプライベート・キャッシュの動作に関する情報を得てください。
  • Varnish が作られた経緯を学んでください。
  • Jean-François Bustarret のブログを訪れ、VCL と PHP に関して学んでください。
  • PHP.net は PHP 開発者のための中心的なリソースです。
  • Recommended PHP reading list」を調べてみてください。
  • developerWorks には他にも PHP 関連の資料が豊富に用意されています。
  • IBM developerWorks の PHP project resources を調べ、PHP のスキルを磨いてください。
  • PHP でデータベースを使うのであれば、Zend Core for IBM を調べてみてください。これはシームレスでそのまま使用でき、インストールも容易な、IBM DB2 V9 をサポートする PHP の開発環境であり本番環境です。
  • developerWorks の Open source ゾーンをご覧ください。オープン・ソース技術を使った開発や、IBM 製品でオープン・ソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報など、豊富な情報が用意されています。
  • Varnish のソース・コードをダウンロードしてください。
  • PECL リポジトリーを調べてみてください。ここは PHP の拡張機能をダウンロードし、また開発するために最初に訪れるべき場所として、既知の拡張機能やホスティング機能がすべて集められています。
  • Live HTTP Headers Firefox plug-in で Firefox 用のアドオンをダウンロードしてください。
  • 皆さんの次期オープン・ソース開発プロジェクトを IBM trial software を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
  • IBM 製品の試用版をダウンロードし、DB2® や Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品をお試しください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=298704
ArticleTitle=Varnish を使って PHP アプリケーションのスケーリングを行う
publish-date=03042008