最近PythonのTornadoというのを使っていて、はじめてノンブロッキングWebサーバをまともに触ったのですが、いままで自分がやってきたWebプログラミングとは結構違っていて、面白く感じました。
特に1つのアクセスが1つのクラスに対応するというのが面白かったです。これ説明したら「は?」って言われたんですけどね。
で、僕が管理しているサイトはほとんどWordPressなので、残念ながらいわゆる普通のPHPアプリケーションです。最近はBackbone.JSとかAngularJSでページ遷移なしにガンガンUIが変わるWebアプリケーションが流行りですが、そういうサイトは凄まじい数のAjaxリクエストが飛んでくるので、WordPressだとサーバがパンクしてしまいます。
特にWordPressのAjax APIは毎回WordPressの起動処理を行うので、大変遅いです。
じゃあNode.JSなりTornadoなりでサイトを作り直すのかというと、それもそれでメンドクサイですね。それに、データベース周りとか、せっかくWordPressの資産があるのだから、それを利用したいものです。
で、そういうノンブロッキングWebサーバになるPHPないかなーと探してみたところ、Reactというのがありました。
Reactを試してみる
このReactはComposerとかでサクッとインストールできます。Bring High Performance Into Your PHP App (with ReactPHP)っていう記事を見ながらやったところ、すぐできました。今回はこんな構成でインストールしてみます。
- ローカルのMacにはWordPressをインストール済み。ドメインは
takahashifumiki.info
で ルートディレクトリは/opt/local/www/fumiki
- WebサーバはNginx + PHP-FPM
- Reactはルートディレクトリ直下の
react
にインストール
で、とりあえず動くとこまで持って行きます。
# とりあえずディレクトリに移動。 cd /opt/local/www/fumiki/react # composerでインストール composer require 'react/react=*’ # サーバ起動スクリプトを作る touch server.php
このserver.php
がメインになります。これを編集。上の参考サイトからまんまコピーして、コメントを追記してます。
<?php // オートローダーを読み込み require 'vendor/autoload.php’; // アクセスごとにインクリメントしてく $i = 0; // メイン関数。アクセスごとにこれが起動。 $app = function ($request, $response) use (&$i) { $response->writeHead(200, array('Content-Type' => 'text/plain')); $response->end("Hello World $i\n"); $i++; }; $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server($loop); $http = new React\Http\Server($socket, $loop); $http->on('request', $app); echo "Server running at http://127.0.0.1:1337\n"; $socket->listen(1337); $loop->run();
で、再び黒い画面に戻り、コマンドラインからサーバを起動します。
php server.php //-> Server running at http://127.0.0.1:1337
これでサーバが起動したので、ブラウザからhttp://127.0.0.1:1337
にアクセスしてみます。すると、”Hello World 1” という感じで表示され、リロードするごとにインクリメントしていきます。
サーバの終了はCtrl-C
です。
WordPressとReactを同居させる
さて、これだけだとなんのこっちゃですが、WordPressと連携してみます。まず、/reactでアクセスしたときはReactのサーバが反応し、それ以外だと普通のWordPressとして動くという設定にしましょう。たぶんですが、Nginxじゃないとダメだと思います。
で、Nginxの設定ファイルをこんな感じに設定します。httpブロックにReactサーバを追加、serverブロック(WordPressの設定がゴチャゴチャ書いてある部分)にプロキシへ渡す処理を書きます。
http{ # プロキシを定義 upstream react { server 127.0.0.1:1337; } # WordPressの定義に追加 server{ # serverブロックにゴチャゴチャ書いてあるはず location ~ ^/react { proxy_pass_header Server; proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Scheme $scheme; proxy_pass http://react; } } }
これでReactサーバを起動したままNginxを再起動すると、/reactでだけさっき作った Hellow World が出て、それ以外はWordPressとして普通に動くという仕組みになるはずです。
ここまででも「だからなんなの?」と感じが否めませんが、気にせず続けます。
WordPressのAjax APIとReactをパフォーマンス比較
さて、やっと本題です。WordPressには元々Ajax APIが存在しますが、ここで次のようなパフォーマンステストを行ってみます。
- データベースからブログのタイトルを表示するという関数を作成
- WordPressのAjax APIとReactで同じ関数を実行してみて、速度を比較
では、まず関数を定義しましょう。これはテーマのfunctions.php内に書いてしまいます。
get_option
を使わないのは、僕がMemcachedを使ってしまっているからですね。あまり深い意味はないです。
/* * WordPressのデータベースに保存されたブログ名を返す * @return string */ function fumiki_greeting(){ global $wpdb; // 名前を毎回取得する $name = $wpdb->get_var("SELECT option_value FROM {$wpdb->options} WHERE option_name = 'blogname'"); // クエリを空にして、次回も取得されるようにする $wpdb->last_query = ''; // HTMLを生成 $html = <<<EOS <html> <head> <title>Performance Test</title> </head> <body> <h1>Hello, this site is %s</h1> </body> </html> EOS; return sprintf($html, esc_html($name)); }
では、Ajax経由でこの関数を実行してみましょう。同じくfunctions.php内に書きます。
add_action('wp_ajax_nopriv_greeting', function(){ echo fumiki_greeting(); exit; });
これで takahashifumiki.info/wp-admin/admin-ajax.php?action=greeting にアクセスすると、HTMLが表示されます。
続いて、Reactのserver.phpも変えます。ポイントとしてはwp-load.phpを読み込むことですかね。
<?php // wp-load.phpを読み込む。 require '../wp-load.php'; require 'vendor/autoload.php'; $app = function($request, $response) use ($wpdb) { // Entry Point $response->writeHead(200, array('Content-Type' => 'text/html')); $response->end(fumiki_greeting()); }; $loop = React\EventLoop\Factory::create(); $socket = new React\Socket\Server($loop); $http = new React\Http\Server($socket, $loop); $http->on('request', $app); echo 'Server running at http://127.0.0.1:1337'.PHP_EOL; $socket->listen(1337); $loop->run();
おそらく、wp-load.phpを読み込むことでエラーがぐわーっと出るかと思いますが、これはまあしょうがないですね。なんかのプラグインが if ($_SERVER[‘REQUEST_METHOD’] == ‘POST’)
とか書いてるんですよ。これはPHPerのカルマみたいなもんです。
Reactを再起動したら、takahashifumiki.info/react にアクセスしてみます。すると、先ほどとまったく同じ画面が表示されます。
では、この二つのURLに対してApache Benchをかましましょう。コマンドは ab -n 1000 -c 100 TARGET_URL
ですね。
WP Ajax | React | 意味 | |
---|---|---|---|
要した時間 | 24.713秒 | 2.329秒 | 短いほどいい |
秒間リクエスト | 40.46 | 429.3 | 多いほどいい |
1リクエストの時間 | 24.713 | 2.329 | 小さいほどいい |
なんとなくですが、Reactの方が10倍ぐらい早い結果ですね。
まとめ
WordPressは起動するたびに毎回データベースへ接続して、どのプラグイン・テーマ使うかとか、俺のURLなんやねんとか、そういうことを毎度行います。したがって、Ajaxのエンドポイントもなんとなくもっさりしています。Reactにすると爆速になること請け合い。
もちろん、Reactを導入するとWordPressのAjax APIと似たようなものを自分で実装しなければいけないのですが、現時点のAjax APIはあんまり便利ではないので別に気にならないと思われます。
ここら辺実装すると使い物になるんじゃないですかね。
- ルーティングで、Reactサーバへのリクエストを適切なクラスに振り分ける処理。CakePHPとかのコントローラーを読み込んで行くようなヤツ。
- データベース接続が起動しっぱなしなので、適宜リフレッシュするなり、再接続するなりの処理。
- MemcachedなどのKVSを適切に利用する(Object Cache API経由で)
- WordPressの管理画面からReactサーバの健全性を確認できる
- Supervisordとかでデーモン起動する。で、Nginxをロードバランサー的に使えばOK。
注意点としては以下の通り。
- WordPressのユーザー周り(get_current_userとか)は1プロセスあたり1ユーザーを想定しているので、Reactだとうまく動かないかも。認証系はWordPress側で行ってからReactに向けるか、Cookie読み取りの処理をReactにやらせるのがスマートか。
- ノンブロッキングサーバで「起動時点からMySQLにつなぎっぱなし」というのが有りなのかどうかはわかりません。
Reactと同じ方法でRatchetというWebSocketサーバも実装できるので、WordPressのユーザー情報と組み合せたチャットルームやメッセージシステムなんかも作れますね。実は僕の本命はこっちだったりするんですけどね。終わり。
[429] [429] Client error: `POST https://webservices.amazon.co.jp/paapi5/getitems` resulted in a `429 Too Many Requests` response: {"__type":"com.amazon.paapi5#TooManyRequestsException","Errors":[{"Code":"TooManyRequests","Message":"The request was de (truncated...)