PHP
ReactPHP

PHPはReactで非同期処理対応のWEBサーバを構築する

More than 3 years have passed since last update.

こんにちは皆さん。

いきなりですが、このエントリーは、JavascriptのフロントのReactとは別物です。

私はPHPerなんですが、最近はnode.jsの話題ばかりだしています。

やはり、やすいサーバで効率よくアプリを動かそうとすると、非同期処理を使ったWEBサーバって魅力的に思うのですよ。

DBアクセスがひどいと、通信に時間とられるので。

では、PHPにノンブロッキングI/Oとか、非同期処理に対応した仕組みはないのかといえば、あるのです。


React


ApacheとNginx

そもそも前回の結果はちょっと八百長臭いところがあります。

PHPの処理は普通にusleep関数使っていましたが、javascriptの方はsetTimeout使って、非同期処理化していました。

そもそもPHPは通常、非同期処理をしないので、仕方ないといえば仕方ないですが、既にこの辺りで差をつけられています。

しかも、Apacheがマルチプロセスであることもはじめからわかっていたことなので、メモリに不安が来ることは自明でした。

一方のNginxはというと、Nginx自体はイベントループとノンブロッキングI/Oを持っていますが、実処理を行うPHP-FPMに於いては、結局非同期処理をしていないのと、同時処理数が限られている(= 初期設定のプロセスの個数になっている)はずです。

PHPプロセスの個数が制限されている以上、メモリに余裕ができるのは当然ですし、そのために、Nginx側ではPHP-FPMに渡しきれなかったリクエストが待ち状態で残っています。

これが、Nginxが長時間の待を必要とする処理に対する大量アクセスについて、他に比べて著しいパフォーマンスの低下を呼ぶ原因となったと思います。

軽量な処理であれば、Nginx+PHP-FPMの機構は力を発揮できますが、大量の待ち時間、代表的な例ではやはりDBへの大量アクセスが発生しうるようなアプリでは、Apacheを使うほうが性能の低下に強いといえると思います。


React

NginxだろうとApacheだろうと、PHP自体が同期処理を基本としているため、レスポンスかメモリのどちらかを犠牲にする必要があります。

そこで、PHPに非同期処理を持ち込むことで、これらの問題を解決しようということになります。それがReactです

ReactはイベントループとノンブロッキングI/Oの実現を目指した、ループ機構です。パッケージにはWEBサーバとして機能する機構が同梱されています。


Reactの導入


Reactのインストール

では、Reactを導入しましょう。

公式とそのGithubで書いてあることが違うのですが、ここはGithubの方で行きましょう。

$ mkdir react_test

$ composer init
$ composer require react/react

これで、これで導入完了です。

次に以下のコードを書きます


app.php

<?php

require 'vendor/autoload.php';

$app = function($request, $response) {
$response->writeHead(200, []);
$response->end('hello world');
};

$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket);

$http->on('request', $app);

$socket->listen(3000, '192.168.33.10');
$loop->run();


$socket->listenの第二引数を省略すると、localhostでしか繋げなくなります。

この状態でapp.phpを実行すると、サーバが起動します

$ php app.php

これで終わりです。

なんでもいいからとにかく動かす、というのであれば、とても簡単ですね。

実際にcurlで確かめると、確かにWEBサーバが立ち上がっています。

$ curl http://192.168.33.10:3000/ #hello world


Promise in React

ReactにはPromiseの機構が同梱されています。

たとえば、先ほどのapp.phpのアプリケーション部分を次のように書き換えることができます(等価ではないですが)。

$app = function($request, $response) {

$deferred = new React\Promise\Deferred();
$deferred->promise()
->then(function ($res) {
$res->writeHead(200, []);
return $res;
})
->then(function ($res) {$res->end('Hello World!!');});

$deferred->resolve($response);
};

Promiseの文脈を使うことで、簡単なタイマーを導入することができます。

まず、Timerのパッケージをインストールしましょう

$ composer require react/promise-timer

これでタイマーがインストールされたので、次のようにアプリケーションを書き換えましょう

$app = function($request, $response) use ($loop) {

React\Promise\Timer\resolve(1.5, $loop)->then(function() use ($response) {
$response->writeHead(200, []);
$response->end('hello world');
});
};

これで、非同期で1.5秒後にレスポンスが帰ってくるようになりました。


Reactの非同期処理

ところで、タイマー関数に$loopという変数が入っていますが、これはReactオブジェクトになっています。

あくまでPHPは同期処理が基本であるため、非同期を処理を行うためのイベントループ機構はReactが担っています。

タイマー関数は、このReactオブジェクトにタイマーイベントを登録するためのユーティリティとなっているわけです。

javascriptでは、そもそもの言語機構としてイベントループが入っているため、非同期処理の際にいちいちループオブジェクトを意識する必要はありませんが、Reactで非同期処理を行う場合は、事情が違うということです。

この辺の事情は、ReactPHPがあまりはやらない原因かもしれません。PHPの従来の利点はその圧倒的な生産性に有りますが、ループを常に意識しつつの実装は非常に面倒です。


Reactの性能

最後に性能検証をしましょう

同時接続数
平均レスポンス時間(ms)
メモリ使用量(KB)

10
1606.893
132

20
1610.885
404

100
1612.481
1688

300
1680.148
9384

1000
(3300回程度で停止)
(19264)

いかがでしょうか

流石に1000同時接続だと、途中で切れてしまいましたが、それ以外は非常に優秀なデータを叩き出しているように思えます。


まとめ

Reactは、3年以上前からあった技術ですが、あまり日の目を見ていないのは残念に思います。

ただ、検証でも見ましたが、性能自体は十分ですし、実装のしにくさをなんとか解決できるようなフレームワークが出れば、apacheやnginxに変わる新しい選択肢にもなれるのではないかと思います。


参考

ReactPHP

タイマー

公式サイト