まえおき
Gearmanというジョブキューサーバを使って時間のかかる処理や言語間の連携をやってみようというお話
Express(node.js)上のWebアプリからExcelファイルをごにょごにょして返したいという要望が出たとき、使い慣れたPHPExcelでやれたら便利だったりします
node.jsからPHPのプログラムをキックして結果を受け取る方法はパイプを使うとか色々あるのですがWebアプリなので結果を書き出すまでに時間がかかってタイムアウトなんてことは避けたいわけです
そこで昔Webブラウザのボタン押下をトリガにしてメールを大量に配信するときに使っていたGearman使えるじゃんというイージーな方法を思いついたのでした
GearmanはもともとCで書かれたジョブキューサーバです
私は前述のメール配信や画像処理を書いていた2008年頃から使いはじめてた気がします
歴史があるおかげかJava/node.js/PHP/Python/Ruby/Perl/.NET/Go/Lispといろんな言語のバインディングがあるので助かります
ここでは、node.jsのクライアントからPHPで書かれたジョブを実行して結果を受け取る、というようなものをサンプルで書いておきます
逆にPHPからnode.jsで書かれたジョブを実行したり、同じ言語でも時間のかかる処理やブラックボックス化しておきたい処理があるときなども本体のプログラムからは分離して専用のジョブにしてみるなど色々と使い手がある方法だと思います
インストール
まずはGearmanのインストールですが、CentOSの場合はepelにあるのでyumで
sudo yum install gearmand
sudo service start gearmand
これでデフォルトだとlocalhostの4730番ポートでListenするサービスが立ち上がります
各言語のクライアントをインストール
まずPHPの場合はremiから入れてる場合は
sudo yum --enablerepo=remi,remi-php56 install php-pecl-gearman
で入ってくれるはず・・・PHPのバージョンが違う場合は注意してください
node.jsの場合は
npm install node-gearman
実際はpackage.jsonに追加することになると思います
実際のコード
まずはクライアントコードを
var Gearman = require('node-gearman');
var gearman = new Gearman('localhost', 4730);
var job = gearman.submitJob('example', JSON.stringify({message: 'Hello'}));
var chunks= [];
job.on('data', function(chunk){
// データは断片的に来る可能性がある
chunks.push(chunk);
});
job.on('error', function(err){
console.error(err);
});
job.on('end', function(){
// ↓は文字列の場合
// バイナリとかの場合はまた別途考える必要がある
var buffer = Buffer.concat(chunks);
console.log(JSON.parse(buffer.toString('utf-8')));
});
次にサーバ側は渡されたデータの後ろにWorld!をくっつけて返すという単純なものにしますが、これを「面倒くさい」「この言語に優れたライブラリがあるのでそれでやったほうが書きやすい」「時間のかかる処理」と置き換えればOK
<?php
$worker = new GearmanWorker();
// デフォルトだとlocalhost:4730につなぎにいく
// ちゃんと指定する方法もあるよ
$worker->addServer();
// exampleという名前のジョブを無名関数で定義
$worker->addFunction('example', function($job){
// 基本パラメータはJSONであっても一度シリアライズした状態でやりとりすることになります
// json_decodeの第二引数をtrueにすると$paramsは配列になる
// デフォルトだとfalseでstdObjectなオブジェクト形式になる
$params = json_decode($job->workload(), true);
// 今回は単純だけどこの辺は色々面倒くさい処理を書いてね!
$params['message'] = sprintf("%s World!", $params['message']);
// データを投げて
$job->sendData(json_encode($params));
// 終わり
// 途中経過を%d/%dで送れる sendStatus()メソッドや、失敗の場合はsendFail('エラーメッセージ')なんてのもあります
$job->sendComplete('');
// このreturn文、sendXX系のメソッドが出る前の遺物だと思うのですが必要なのかしら
return GEARMAN_SUCCESS;
});
// これがないと定義しただけで終わってしまう
while($worker->work());
あとはターミナルその1で
php worker.php
で起動してる状態にしといて、別のターミナルで
node client.js
で結果が確認できます
非同期のメリットを生かす
さて、node.js側は非同期になってるのでexpressなどのWebアプリの場合はsubmitJob()を送った時点でブラウザには「処理は依頼した、結果は追って知らせる」的レスポンスを返しておき、workerの中やクライアントのjob.on('*')なメソッド中でメールなりWebSocket経由で結果を通知するという方法もとれます
ブラウザからサーバ側での動画のエンコードなど時間のかかるバッチ処理なんかをキックさせたい場合は、XHRを使ってもHTTPでやってる以上タイムアウトは発生するので・・・
ジョブの永続化
デフォルトだとgearmandは投げられたジョブを自分で確保しているメモリストレージに格納しています
なので、うっかり
service gearmand restart
なんてするとたまっているジョブをすっかりド忘れしてくれるお茶目さんです(どうやってもエラーしか出なかった「黒歴史」ジョブも忘却してくれるのでデバッグ中はこっちのほうがありがたいのですが)
キューの永続化にmemcachedやMySQLなどの各種DB、libdrizzleを経由する方法もあるので実運用にはそちらをお勧めします
またGearman自体に簡単なHTTPサーバを持たせてHTTPでジョブを投げちゃえという拡張もあるので公式サイトのマニュアルを確認してください
ちなみに永続化すると、キューを共有した上で複数のサーバで同じワーカーを動かして分散処理なんてこともできます
余談
node.js専用だと思ってたpm2ですが、.phpなファイルはphpコマンドで実行してくれることが発覚(マニュアル嫁って話なんですが)
おかげでworker.phpなんかの起動も任せられるので色々捗ってます