PHPで非同期処理
あるツールにおいて結果を得るまでの処理時間が長いのでその対策を検討しておりました。 まず原因としてはいくつかのAPIを数多くコールしていますが、それぞれのAPIレスポンスが遅いのもさることながら、 それらをシーケンシャルにコールしていることによりツール全体の処理時間が長大になっておりました。 諸般の都合上yumでインストルしたPHPで実装しており、マルチスレッドのpthreadsやPCNTLによるマルチプロセス等をインストールしてPHPをフルビルドすることが出来ないことから、 非同期処理に向け、有用なライブラリを国内外を問わず探して調査・検証を行いました。
候補選定
PHPの非同期処理に関するサイトをいくつかピックアップして候補を絞り込んでみました。
-
asyncphp/doorman()によるマルチプロセス
PCNTLを導入しなくてもマルチプロセスを実装することが出来ます。 ただし必然的にメッセージング等のプロセス間通信やその制御等が必要となり、そのあたりの仕組みを知らないとチョット敷居が高く、実装コストが高いと思いました。 -
Generatorによる非同期処理
正直、私の勉強不足で理解できませんでした。 非同期処理に限らず、大きな配列データの処理において有用な仕組みなので今後、勉強したいと思います(泣) -
guzzleによる非同期処理
httpクライアントで同期/非同期でのリクエストが出来、メソッドもGET/POST/PUT/DELETEが用意されています。 以前、開発室室チョーから本ライブラリーの情報が共有されてましたが、そのときはスルー余り必要性を感じてなかったのです(スミマセン…)が今回の性能向上対策で実装調査を行った結果、 シンプルで同期処理のように利用することができNode.jsでもお馴染みのPromiseを使っていることからこのライブラリを利用することにしました。 如何せんhttpクライアントなので機能レベルでの非同期処理は出来ません。なので自前でhttpリクエストしている箇所だけが非同期化の範囲となります。
guzzleによる調査・検証
インストール
guzzleのインストールはcomposerでサクッとインストール可能です
$ composer require guzzlehttp/guzzle
検証用サンプルソース(PHP)
指定時間スリープするAPIを連続でコールします。指定するスリープ時間は1,2,3,4,5秒と可変でAPIをコールします。
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
$client = new Client();
$promises = [];
for ( $sleep=1 ; $sleep<6 ; $sleep++ ){
$promises[] = $client->getAsync('http://hoge.com/test?sleep='.$sleep);
}
echo 'Start='.date('H:i:s').PHP_EOL;
$response = array_map( function($_e){ return json_decode((string)$_e->getBody());},Promise\all($promises)->wait());
print_r($response );
echo 'End='.date('H:i:s').PHP_EOL;
検証用対向API(Node.js+Express)
指定時間スリープするAPIです。パラメータで渡されたスリープ時間とリクエストを受け付けたときのタイムスタンプ及びスリープが終わったタイムスタンプをレスポンスとするAPIです。
■http://hoge.com/test
var express = require('express'),
app = express();
:
app.get('/test', function(req, res) {
var st = new Date();
var st_time = st.getHours()+':'+st.getMinutes()+':'+st.getSeconds();
setSleep(() => {
var ed = new Date();
var ed_time = ed.getHours()+':'+ed.getMinutes()+':'+ed.getSeconds();
res.send( {'Sleep(sec)' : req.query.sleep,'Start' : st_time, 'End':ed_time} );
}, req.query.sleep*1000);
});
イメージ図
出力結果
リクエストは一括してコールしてますが、各リクエストのスリープ時間が異なり、一番最後に処理が完了(一番長いスリープ時間を指定したAPI)するまでの間、 PHP側は待ち合わせており、その完了を以ってPHP側に制御が戻り全APIの結果が返ってきます。 この例では非同期というよりはリクエストを一括してまとめた感じになりcurl_multiでもいいんじゃないかというハナシもありますがcurl_multiよりも実装が簡素でした。
Start=10:46:43
Array
(
[0] => stdClass Object
(
[Sleep(sec)] => 1
[Start] => 10:46:43
[End] => 10:46:44
)
[1] => stdClass Object
(
[Sleep(sec)] => 2
[Start] => 10:46:43
[End] => 10:46:45
)
[2] => stdClass Object
(
[Sleep(sec)] => 3
[Start] => 10:46:43
[End] => 10:46:46
)
[3] => stdClass Object
(
[Sleep(sec)] => 4
[Start] => 10:46:43
[End] => 10:46:47
)
[4] => stdClass Object
(
[Sleep(sec)] => 5
[Start] => 10:46:43
[End] => 10:46:48
)
)
End=10:46:48
検証結果と今後の課題
このライブラリーを組み込んでテスト環境にて性能を検証した結果、対策前よりも約50%近く改善が見られました。 しかしながら対向API側の負荷が高くなり、リソース不足は否めません。リソースの増強及びAPIのチューニングが今後の検討課題として引き続き調査を実施致します。
おわりに
非同期化による性能向上の可能性を見出すことが出来、開発室のノウハウとして蓄積することが出来ました。 他のプロダクトにおいてもこのライブラリを利用し、更なる性能向上を図りたいと思います。