Gree Fast Processor: PHPを3倍(くらい)速く

ごあいさつエントリだけというのもなんなので、引き続きfujimotoです。実質上1つめのような気がするこのエントリでは、PHPが3倍くらい(少なくとも2倍くらいは…)速くなるGree Fast Processorというのを先月作ってみたのでご紹介です。

すぐわかるまとめ

Gree Fast Processorというのを使ってみると、シンプルなsymfonyのプロジェクト(xav.ccで試しました)でも2倍弱、結構複雑なアプリケーションだと7倍くらい速くなったりします。いくつかの制約がありますが、パフォーマンスに飢えているかたはお試しください。

こちらはなんかすごい速くなっている感じのグラフ(一番上が速くなった版のRequests per Second、赤が通常版のRequests per Second):

これはさすがにbest caseすぎる気がしますが、普通にやっても2倍弱くらいは速くなるはずなグラフ:

ということで興味を持たれた方は以下が詳細でございます。

APC or eAcceleratorで十分?

ウェブサービスを提供していると、例えばRDBMSのチューニングの話やスケーラビリティについての話題は非常に豊富なのですが、アプリケーションサーバのほうはあまり話題にならないような気がします。たしかに、アプリケーションサーバはロードバランサの下にサーバを並べていけばスケーラビリティを確保出来そうですし、PHPを使っていれば(ちなみにGREEでは幸か不幸か、多くのコードがPHPで書かれています)APCeAcceleratorを利用して「うん、結構速くなった」というところで満足してしまいがちです(最近core数も増える一方ですし)。

しかしながら、当然アプリケーションサーバも速ければ速いほどよいわけです。レスポンスは速いほうが満足度も上がるでしょうし、なにより1リクエストあたりで利用するマシンリソース(アプリケーションサーバの場合は往々にしてcpuリソース)が少なければ、サーバの台数も少なくてすみます。例えば今サーバ20台で動かしているアプリケーションが2倍速くなればサーバを10台に減らせる(かもしれない)ということです。1台¥40,000/monthとかだとすると¥400,000もコストが下がりますし、これが1,000台とかだと影響はさらに大きくなります。何にしても、速いことはいいことです。

どこを速くしましょう?

とはいえ、速くしようと思ったら速くなるわけでもないので、いろいろと考えてみました。で、とりあえずrequireのコストって(たとえコンパイルキャッシュがあったとしても)結構高いよなー、というかGREEはシステムとして結構歴史が長い上に拡張を続けているので、ファイル数が膨大なのでやはりこのコストはばかにならないな、と思いはじめた次第です。正確にいうと、前からずっと思ってたんですが特に何もしてこなかったということなんですが。

ということでとりあえずものすごいシンプルな例で試してみます。まず何もしないひと(Aとします):


これと、requireを1回するひと(Bとします):


最後に、requireされるファイル:

 'value1',
    'key2' => 'value2',
...
    'key1000' => 'value1000',
);

もうこの時点で「いやいやrequireは絶対パスで書くだろ、常識的に」とか「autoloadじゃないの?」とか「<?php、ね」とかいろいろ聞こえてきそうですが、聞こえないフリです。というか別にrequireする必要もなくて直接かけばいいのですが。

で、これをPHP 5.2.6 w/ eAccelerator 0.9.6がインストールされているマシン(Intel Core2Duo 2.20GHz x 2)で、原始的にab -c 4 -n 1000くらいしてみると

 A: Requests per second:    4340.75 [#/sec] (mean)
 B: Requests per second:    1670.98 [#/sec] (mean)

とかになります。調子にのって10,000行くらいあるarray()だけを書いたファイル(Cとします)をrequireしてみると(いやだから直接書いてもいいんですが)

 C: Requests per second:    270.11 [#/sec] (mean)

順調に遅くなります。今度はreturn trueするだけのメソッドが10,000あるクラスが記述されたファイル(Dとします)をrequireしてみると

 D: Requests per second:    178.17 [#/sec] (mean)

となってきます。意外に影響するんですねー、ということでなんとなくグラフにするとこんな状況です:

と、いうことで「だったら、requireだけして(socketか何かで)リクエストを待っているひとがいれば速くなるんじゃない?」ということになるわけです。mod_perlとか、そんな感じです。

Gree Fast Processor

前振りが思いほか長くなってしまいましたが、ようやく本題です。なんとなく速くなりそう...かもしれない、ということで「requireしてまっているひとにリクエストを投げてみる」仕組みで本当に速くなるのか気になってしかたなくなったので、とりあえず「Gree Fast Processor」と名づけて作ってみました。動作イメージは以下のようなものです:

と、これだけではよくわからないと思いますので、適当なアプリケーションで試してみます。GREEで試した例をお見せできれば楽しいかもしれないのですが、実際に試しやすいようにということで、symfonyで作られているURL短縮サービス:xav.ccを例にGree Fast Processorを今回の対象にしてみます(たまたま見つけました)。

インストール

本題ではないので、細かいところは省略していますが、とりあえずもろもろインストールします。まずはxav.ccです:

$ svn co https://opensource.lacot.org/xav.cc/svn/trunk/ /path/to/project/
$ cp config/app.yml-dist config/app.yml; mv config/databases.yml-dist config/databases.yml
(適当に編集)
$ ./symfony doctrine:build --all
$ chmod 777 cache log

かんたんです。次にextensionとライブラリをインストールします。自分でも試してみたい、という一風変わったかたは、こちらにソースコードがあるのでご利用ください(ちなみに、まだものすごい勢いで実験的ステータスなので、何も保証できませんし、コードもgdgdです)。

$ tar zxvf gree_fast_processor-0.0.1.tgz
$ cd gree_fast_processor-0.0.1/
$ phpize; ./configure; make; sudo make install
$ cp gree_fast_processor.php gree_fast_processor_listener.php /path/to/project/lib/vendor/gree/

ふつうです。あとはphp.iniにextension=gree_fast_processor.soを追加してください。そして、とりあえずこの状態でトップページのRequests Per Secodがどれくらいでるかみてみると

Requests per second:    63.12 [#/sec] (mean)

まぁこんなものです。

index.php

つぎに上の図のindex.phpにあたる部分を書いておきます。symfonyはすべてのリクエストをindex.phpで受けるので非常に都合がよいです(/path/to/project/web/frontend/index.php):

  2 require_once '/path/to/project/lib/vendor/gree/gree_fast_processor.php';
  3 
  4 if (Gree_Fast_Processor::run('xav') == false) {
  5     require_once(dirname(__FILE__).'/../../config/ProjectConfiguration.class.php');
  6 
  7     $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
  8     sfContext::createInstance($configuration)->dispatch();
  9 }

5-8行目がオリジナルな処理です。追加しているのはGree Fast ProcessorのPHPライブラリのrequireとGree_Fast_Proessor::run()メソッドの呼び出しのみです。この状態でアクセスすると、listenerのソケットが存在せずにrun()がfalseを返すので今までどおりの処理が行われます、安心です。

いずれにせよ、このrun()メソッドが、図のunix domain socketを通じたlistenerとのI/Oを行っています。

listener

次に上記run()メソッドからのリクエストを受け付けるlistenerを起動します。その前に、run()に引数で渡される識別子とhandlerとの関連をgree_fast_processor.phpに記述しておきます:

 23     var $ident_list = array(
 24         'xav'   => '/path/to/project/lib/vendor/gree/gree_fast_processor_handler.php',
 25     ); 

あとはlistenerを起動します(上の図のgree_fast_processor_listenerの部分です)。これでunix domain socketが作成され、run()を通じてリクエストを受けることができるようになります(が、実際にはhandlerがないので何も起こりません)。

$ /path/to/project/lib/vendor/gree/gree_fast_processor_listener.php --start --concurrency=4 --max-request=1024 --ident=xav

handler

最後に、listenerが受け付けたリクエストを処理するhandlerを準備します。listenerは、リクエストを受け付けると、concurrencyの数だけ起動されているhandlerのどれかにrequestを送信します。ちょっと長くなってしまいますが貼ってしまうとこんな感じです:

  3 require_once '/path/to/lib/vendor/gree/gree_fast_processor.php';
  4 $gfp = new Gree_Fast_Processor(); $gfp->initialize();
  5 // requires (anything you want)
  6 require_once '/path/to/project/config/ProjectConfiguration.class.php';
  7 for (;;) {
  8     $gfp->startup();
  9     $request = $gfp->getRequest();
 10     ob_start(null);
 11     $configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'prod', false);
 12     sfContext::createInstance($configuration)->dispatch();
 13     $content = ob_get_contents();
 14     ob_end_clean();
 15     $gfp->setResponse($content, strlen($content));
 16     $gfp->shutdown();
 17 }

handlerは起動されると9行目のgetRequest()でlistenerからのリクエストを待ちます。で、11-12行目に書いてある処理をリクエストごとに実行して、出力をlistenerに返すことで、ぐるぐると処理を行っています。

ベンチマーク

とりあえずabしてみると:

Requests per second:    107.98 [#/sec] (mean)

んー、シンプルなプロジェクトだと2倍まではいかないですが、速くはなるみたいです。結構荒いデータですが、並列数1〜8でRequests per Secondをみると以下のようになりました:

ちなみに、この仕組だとコードが多ければ多いほど速くなるはずで(逆に、コードはシンプルでも処理が重いケースではあまりパフォーマンスの向上は期待できません)、とあるサーバを借りて、とある重めのプロジェクトでベンチマークを以前とってみると、7倍以上速かったりしました(たぶんこのあたりがbest caseですね)。

さいごに (と注意点)

ここまで読んでいただいた相当に辛抱強い皆様はお気づきの通り、Gree Fast Processorは既存のコードをほとんど変更せずにパフォーマンスを得ることができますが、いくつかの制約があります。

  • static変数やglobal変数がリクエストごとにクリアされません

    • なので、お行儀よく(あるいはある程度意識して)コードをかく必要があります
    • global変数はunset()しまくればいいんですが、static変数を初期値に戻すのは結構面倒なのでやっていません (zend_extensionを書けばできるはずですが、それもどうか、という)
    • リクエストを処理するコード中でdefine()していると2回目以降warningが出ます (undef()は実装は簡単なのですが...)
  • exit()していると台なしです

    • ただ、その場合run()からfalseが返るので、リクエストは処理されます
  • header()関数やsessionはどうするの?

    • Gree_Fast_Processor::header()をつかえば大丈夫です
    • sessionもsession_cache_limiter(false)なら問題ありません (同様の機能はちょっとがんばれば実現できるんですがまだやってないだけな状態です)

安定動作にはもう少しテストなどが必要ですが、うまくはまるケースなら割と低い労力で数倍速くなる、かも、しれません(個人的にはHipHop for PHPよりも使えるケースはあるんじゃないかとか思っています。そもそも高速化のアプローチが全く以て異なっていますが)。

ただ、まだ現状は「ほんとにパフォーマンスでるのかとりあえず実装して試してみた版」なので、人柱さんやパッチ、お待ちしております。

そして最後になりましたが、GREE Engineers' Blogを今後ともよろしくおねがいします:)

Author: fujimoto