最近は Admiral Sir Cloudesley Shovell というバンドの 1st フルアルバム Don’t Hear It… Feat It をよく聴いています。
Sir Lord Baltimore のような、骨太なリフと、ブルージーでドラマ性もある楽曲が魅力のバンドですが、現在進行形で活動中のバンドです。
ストーナーロックとか好きな人は是非。

ここから本題です。
あるところに、Web API を数十万回単位で叩きまくる、PHP で書かれたバッチプログラムがあったとしましょう。
処理件数が増えるごとに処理時間が増大するので、いつしか 1 日経っても終わらないようになってしまいました。

そのプログラムは HTTP リクエストを直列に行っていたので、それを並列化させれば何とかなるのではないか、と考えて作ったのが以下のライブラリ。

yuya-takeyama/parallel_http

念のため書いておくと、全然プロダクションレベルにはなってないので、その辺りは自己責任でご利用ください。

試してみる

Packagist に登録しているので、Composer を使用してインストールすることができます。
Composer をよく知らないという方はこのあたりの資料を参考にすると良いのではないでしょうか。

composer install してやると EDPS (エディプス) というキラキラネームのライブラリも一緒にインストールされます。
これは Événement という PHP 5.3 または 5.4 以降で動作するイベントディスパッチャライブラリを 5.2 でも動くように移植したものです。

これを使えば、以下のように、Node.js ライクなコールバックスタイルで HTTP リクエストを行い、そのレスポンスを処理することができます。

PHP で並列 HTTP リクエストを実現する方法

PHP で並列に HTTP リクエスト、といえば curl_multi_* 系の関数がおなじみで、ちょっとググるだけでも以下のような記事が見つかります。

今回作った ParallelHttp も同様に curl_multi_* 系の関数を使用しての並列リクエストを行っています。

また、僕は以前にも HTTP_Parallel というライブラリを作ったことがあって、これも curl_multi_* 系の関数を使用しています。
上記の記事は curl_multi_* 系関数の紹介ということもあって、わりとゴリゴリと実装されていて再利用しづらいので、オブジェクト指向なインターフェイスにラップしよう、というモチベーションのもとに作りました。

既存のやり方がダメだった理由

先に紹介した記事の方法や、それを応用した HTTP_Parallel では、今回の問題に対応することができませんでした。

これらに共通するのは、「リクエストは並列に行うが、レスポンスの処理は最後に全部まとめてやる」というところです。
HTTP リクエストが 1 つでも残っている限りは他のタスクを一切行わず、最後のレスポンスが返ってくるまでただ待ち続けることになります。

これではリクエストを並列化させているだけで、非同期処理とは言えないでしょう。

そこで ParallelHttp は以下のようなフローで HTTP リクエストと、そのレスポンスの処理を行っています。

  1. イベントループにリクエスト情報とコールバックを登録 ($client->get($url, $cb))
  2. イベントループを開始 ($loop->run())
  3. 登録されたリクエストを同時に実行
  4. 終わったリクエストが無いかチェックし、終わったものから逐一コールバックで処理、まだ終わってないものは引き続きチェック
  5. 全てのリクエストが終了したらイベントループを抜ける

リクエストを待つ間に、先に処理できるレスポンスを処理することで、待ち時間を有効に活用することができます。
数 10 件程度のリクエストなら大した差は出ないと思いますが、今回のように数 10 万規模のリクエストを行うとなると、かなりの差が出ることが予想されます。

処理件数によるさらなる問題

レスポンスの処理を逐一行うことで、処理効率を上げられそうな感じがしてきました。
が、問題はその手前にありました。

前のセクションに書いた通り、イベントループを開始すると、登録されたリクエストを同時に実行することになるので、素直に実装するとものすごい数のリクエストが全て同時に行われてしまいます。

そうなると以下のような問題が発生します。

  1. ものすごい数の処理を同時に実行するので、CPU 等のマシンリソースを大量に消費する
  2. 対象のサーバが単一または小数だった場合には DoS 攻撃になってしまう
  3. それ以前に全てのリクエスト情報とそのコールバックの分のメモリを確保する必要がある

これらについても解決しないと、今回の問題に ParallelHttp を使うことはできませんでした。

対策 1. 並列数を保てるようにする

同時に行うリクエストの数を設定として持たせ、最大でもその数までしか並列化させなければ、ローカルマシンにもリモートマシンにも優しくなります。

ParallelHttp ではイベントループの内部にキューを持っており、並列数が最大になるまでは curl_multi リソースにリクエスト情報を登録していきます。
curl_multi に登録したリクエスト情報が最大に達すると、その後のリクエストはとりあえずキューの中にためておいて、1 つでもレスポンスが返ってきたらそれを処理しつつ、キューから次のリクエスト情報を curl_multi リソースに登録する、という方法を採りました。

イベントループの生成時に以下のようなオプションを指定することで、並列数を指定することができるようになります。

デフォルトでは 10 になっているので、必要に応じて調節することで、さらなるパフォーマンスを得たり、マシンリソースに気をつけて処理を行ったりすることができます。

これで、前のセクションで示した問題のうち最初の 2 つが解決されました。

対策 2. リクエスト情報の登録を少しずつにする

最後に残るのは、リクエスト情報とコールバックのために、大量のメモリを確保する必要がある、という点です。

結論からいうと、これは ParallelHttp の実装だけで解決することはできませんでした。
そこで、ParallelHttp の使い方を工夫することにしました。

例えば、処理対象の URL がデータベースに 30 万件入っていて、それらの全てにリクエストを行う場合を考えてみましょう。

memory_limit の設定値等にもよりますが、普通は $loop->run() の手前で、メモリ使用量が上限を超えて強制終了になるでしょう。
URL 30 万件程度ならメモリを多めに確保しておけば何とかなりますが、ParallelHttp の内部で生成しているリクエスト情報を保持したオブジェクトであったり、コールバックに使う無名関数であったりでメモリを大量に消費します。

コールバックに使う関数が単一でよければループの手前で変数にいれておき、それをコールバックとして登録するようにすれば、コールバック分のメモリは節約できます。
ですが、クロージャを使用する場合等はリクエストごとにユニークなコールバックを生成することになるので、そういうわけにもいきません。

URL を一度にまとめて取得し、その全てをイベントループに登録するのではなく、少しずつの URL をイベントループに登録し、そのリクエストが全て終わった段階で新たに URL を登録していく… というのを延々繰り返すようにしてみます。

これは擬似的な例ですが、実際にこういうのを作ってみてとりあえず 1 週間はまともに動かすことができました。

ここで UrlIteratorIterator としているものについて、具体的に説明すると、外側のイテレータはデータベースから URL の一覧を LIMIT 1001 で取得し、1000 件分の URL を内側のイテレータとして返します。
LIMIT 1001 としたのはまだデータベース内に次の値があるかどうかをチェックするためで、1001 番目の要素に値があれば、オフセットを適当に指定しつつ次の 1001 件を取得する、といったことを繰り返しています。

まとめ

PHP 5.2 で動作する非同期 HTTP 処理ライブラリ ParallelHttp について紹介しつつ、その中で直面した問題と、その解決について説明しました。

高いパフォーマンスを得る上で非同期処理は大変魅力的ではありますが、それはそれで別の問題が生まれるので、一筋縄では行きません。
とはいえ、筋のいいライブラリであったり、実装パターンを使用することである程度は緩和することができるでしょう。

今回作った ParallelHttp については ReactNode.js の標準ライブラリのコードを大いに参考にさせていただきました。
また、前にブログで紹介した AsyncMysql を実装した経験も大いに活かすことができました。

いろんな実装パターンを学んで、フレームワークや言語にとらわれないプログラミングスキルを身につけて行きたいと感じました。

, ,

Espérance という BDD っぽくアサーションを行うライブラリを書いています。
とりあえず PHPUnit の中で使ってみるイメージはこんな感じです。

あと、前に中途半端に作った xSpec フレームワーク Speciphy と組み合わせるとこんなイメージにもできそうです。 (まだ作ってませんが)

なんで should でなく expect なのか

RSpec 3 からは expect がデフォルトになる、という記事が先日ありました。

RSpec’s New Expectation Syntax

RSpec といえば “foo”.should == “foo” といった具合に、全てのオブジェクトに should メソッドを生やしての アサーション (エクスペクステーション) が特徴でしたが、やっぱり Kernel モジュールに勝手にオブジェクト足されるのは気持ち悪いよね、ということで古き良き Test::Unit を使い続ける人も一定数いるように思われます。

should と比較して、expect は RSpec コンテキスト (多分 ExampleGroup とか Example とかの中だろうけど特に読んではいません) の中に存在するメソッドなのでグローバル空間に副作用がありませんし、それでいて英語としても読み下しやすい DSL だと言えます。

そして expect といえば、JavaScript の BDD ライブラリでは結構ベタだったりします。
Jasmine もそうですし、Socket.IO の作者による expect.js というものもあります。
Espérance は expect.js を PHP に移植して作っています。

PHPSpec について

PHP はオープンクラスでないので、全てのオブジェクトに should メソッドを生やす、なんてことはできません。

そこで PHPSpec では $this->spec(“foo”) とすることで、値を Interceptor オブジェクトでラップし、Interceptor がマジカルな DSL を実現する、という作りになっています。

PHPSpec の Interceptor は独立したライブラリではありませんが、Speciphy でも拝借しており、独立性の高いモジュールでもあります。

PHPSpec の Interceptor は凄いんですが、正直言って頑張り過ぎなんではないかと思っています。

PHPSpec 本体の spec を見ていると $this->matcher->getDescription()->should->be(‘be equal to 1′); なんてことをしていたりします。

$this->expect($this->matcher->getDescription())->to->be(‘be equal to 1′); とかで充分やん、とか思ってしまうわけです。

とはいえ Espérance も __get とか __call とか使ってますし、マジカルであることに変わりはないのですが、作りは大分シンプルになっていると思います。
expect.js の実装を真似ているので、expect.js が凄いというだけの話でもあるのですが。

expect.js の DSL の実装について

ところで expect.js では expect(1).to.be(1) といったプロパティのチェーンを、Assertion オブジェクトを再帰的に生成して to プロパティに新しい Assertion を差し込む、なんてことをしており、一度 expect(1) としただけで 40 数回ぐらいコンストラクタが呼ばれるています。

PHP では __call でプロパティ読み出しをオーバーライドすることができるので、Espérance では再帰的なコンストラクタは行われません。

JavaScript でも最近の処理形だと Object.defineProperty() とかで似たようなことができるような気がするのですが、それをしないのはより幅広い環境での使えるようにということなんでしょうか。 (JavaScript あんまり知らないのでよくわかりません)

名前について

Google 翻訳に expectation と入力してフランス語として出てきた単語から適当に選びました。

フランス語を名前として使うアイディアは、最近話題になった React (通称 node.php) でも使われているイベントディスパッチャライブラリ Événement のパクリです。

今後について

Speciphy もそうですが、ボクはライブラリやフレームワークを作ってもメンテが続かないタイプの人間だということだけ書いておきます。

でもまぁこういう感じでアサーションだけがライブラリと独立しているのは割とありなんじゃないかなーとは思いますし、Packagist への登録ぐらいはやっておきたいと思っています。思っているだけですが。

というわけでレバ刺しを食べに行きます。

, ,

最近イチオシの php-buildphpenv について紹介してきました.

内容はほぼ最近のブログ記事をまとめた感じです.

一度ブログに書いたものを敢えて発表ネタにしたのにはいくつか理由があります.
(もちろん, 一度記事にまとめたネタなのでスライドに起こしやすい, というのもありますが…)

  • もっと色んな人に知って欲しい
  • もっといろんな人に使って欲しい
  • まだまだこなれていない部分があり, フィードバックが必要
  • フィードバックやパッチによりもっと改善されるはず

というわけで皆さんどんどん使いましょう.

類似のツールについて

php-build および phpenv と類似の機能をもったツールは他にもあります.
勉強会でも, 例えば phpall とはどう違うのか, という質問がありました.

結論から言うと, 私は他のツールは特に使ったことが無いのでよくわかりません.
ですが, もともと rbenv は Ruby コミュニティではかなり人気のツールですし, 便利な機能はいろいろと揃っており, 比較的失敗の無い選択肢だと考えています.

また, phpall の違いとして, phpall は各バージョンのバイナリをそれぞれ php-5.2.8 といった, バージョン番号付きのファイル名にしているようですが, phpenv は php コマンド自体を自由に切り替えます.
特定のコードを複数のバージョンで一気に実行する, というときには phpall が便利そうですが, ライブラリの開発等には phpenv の方が適していると思われます.
また, phpenv においては pyrus, phar, phpunit といったコマンドも各バージョンごとに試すことができる, というのも大きな利点です.

phpenv のプラグイン機能について

元々, これまでのブログ記事をまとめただけの発表にするつもりでしたが, ひとつだけおもしろい機能を見つけたので, そこだけ新ネタです.
phpenv の元になっている rbenv にはプラグインという機能があり, 専用のディレクトリにシェルスクリプトを配置するだけで簡単にサブコマンドを登録できる, というものです.
当然 phpenv でも使える機能です.

phpenv each によるコマンドの一斉実行

rbenv のプラグインとして公開されているものの中で, とりあえず rbenv-each というものをつかってみました.
これは rbnev (もしくは phpenv) の管理下にある全てのバージョンのバイナリで, 指定したコマンドを実行する, というものです.

とりあえず, php -v によるバージョンの確認を試してみました.

phpenv each によるバージョンの一斉確認

phpenv each によるバージョンの一斉確認

これ自体はあまり意味の無い例ですが, 例えば PHPUnit によるユニットテストを, 複数のバージョンの PHP で一斉に実行できるすれば, ライブラリ作者に取ってはかなり便利なのではないでしょうか.
それについてはいずれ調べて, また記事にしたいと思います.

このように, php-buildphpenv にはまだまだ便利な使い方がたくさんあるはずです.
利用者が増えることで, そういったノウハウが発掘され, 共有されることを心から願います.

最後になりますが, 今回の勉強会の主催の @gusagi さん, そして会場提供の株式会社 VOYAGE GROUP さん, 楽しい勉強会を本当にありがとうございました.

, ,