こんにちは。adingoでプログラマをしている真幡です。

アプリケーションの評価指標の一つにレスポンス性能があります。どれほど素晴らしいアプリケーションでも、レスポンスを返すまでに時間がかかるシステム(=重いシステム)は敬遠されがちです。今回はGearmanというジョブキュー管理ソフトウェアを使い、ウェブアプリケーションのレスポンス性能を向上させる方法を紹介します。

ジョブキューとは何か

ジョブキューとはジョブをキューで管理するものです。これでは説明になっていませんね。キューとはFIFO(First In First Out)を実現するデータ構造です。キューに登録されたモノは、キューに登録した順に処理されます。ジョブキューにおいては、キューに登録するモノはジョブなので、キューに登録した順にジョブが処理されることになります。ジョブキューに登録するジョブの粒度は大小を問いません。したがって、ユーザから重たいジョブを処理するようにリクエストされたときに、ジョブを逐次的に処理せず、ジョブキューに登録することで「ジョブの受付をしました」とユーザにレスポンスを返すことができます。「リクエストを受け付ける=>リクエストを処理する=>レスポンスを返す」という従来の流れから「リクエストを受け付ける=>(とりあえず)レスポンスを返す=>リクエストを処理する」という流れを実現することができます。レスポンスの早さはユーザ体験の向上につながるので、これはうまいやり方であると言えます。

次の図はジョブキューの全体像です。

ジョブキュー

ジョブサーバーが持つキューに注目してください。ジョブサーバーから出ている矢印を見ると、ジョブをキューの上側から詰めていることが分かります。一方、ジョブキューから出ている矢印を見ると、ジョブをキューの下側から取っていることが分かります。先ほどの「キューに登録した順にジョブが処理される」という説明を図で表現すると、このようになります。

より具体的なケースとして、ブログサービスにおける「ブログ記事の投稿」について考えてみましょう。このブログサービスでは、記事が投稿されると次のような処理を行うとします。

  • 記事内容をデータベースに追加
  • 添付画像を(モバイル用の)サムネイル画像に変換
  • RSSファイルの更新
  • 記事投稿の完了通知をメールで送信
この一連の処理において、添付画像の処理が重たい処理であると仮定します(例えば5秒程度の処理時間がかかるとします)。この場合、とりあえず記事内容をデータベースに追加し、それから先の処理をジョブキューに登録し、先にユーザにレスポンスを返すという実装方法が考えられます。こうすることで、ユーザのブラウザを5秒程度フリーズさせる部分を解消することができます。この代償として、添付画像の反映、RSSファイルの更新、メールの送信が5秒程度遅れることになりますが、サービスの性質を考えると、それほど大きな問題ではないと思います。

ところで、同じようなことをcronを使って実現することも可能です。短い間隔でcronを実行し、その中で添付画像の処理、RSSファイルの更新、メールの送信をすれば同様の結果を得られます。では、cronではなくジョブキューを使うメリットは何でしょうか。後述しますが、ジョブキューではワーカーと呼ばれるプロセスが常駐することで、プロセス起動のコストを削減することができます。cronが起動するプロセスがライブラリなどを大量に読み込む必要がある場合、プロセス起動のコストは高くつきます。そのような場合は、cronで都度プロセスを立ち上げるより、ジョブキューの常駐プロセス(ワーカー)を使う方が適切でしょう。

以上のことをまとめると、次のような処理を行うケースではジョブキューを使うメリットがあると言えます。
  • 即時性を求められない処理
  • プロセス起動のコストが高い処理
ジョブキューを効果的に使うことによって、アプリケーションの体感速度を向上し、システムの負荷を下げることも可能であると納得できたでしょうか。以下では、Gearmanというジョブキュー管理システムの使い方について説明します。

Gearmanのインストール

Gearmanのインストール方法は次の通りです。インストール環境は「MacOSX Snow Leopard」を想定しています。

$ sudo port selfupdate
$ sudo port install gearmand
$ sudo pecl install gearman channel://pecl.php.net/gearman-0.7.0

最後に、php.iniに"extension=gearman.so"を追記してください。

Gearmanの構成要素

Gearmanの構成要素には次の3種類があります。

  • ワーカー(またはコンシューマー): 要求に応答する役割
  • クライアント(またはプロデューサー): 要求を生成する役割
  • ジョブサーバー(またはエージェント): クライアントとワーカーの仲介を行う役割

Gearmanデーモンの起動

gearmand(Gearmanデーモン)を起動します。次のようにコマンドを打ちます(前述のようにGearmanをインストールした場合)。

$ /opt/local/sbin/gearmand -d

"-d"オプションはバックグラウンドで起動するためのオプションです。次のようにgearmandプロセスが残れば問題ありません。

$ ps xu | grep gearmand | grep -v "grep"
y-mahata 1314 0.0 0.0 2435700 320 ?? Ss 1:35PM 0:00.00 /opt/local/sbin/gearmand -d

クライアントとワーカーを動かす

ワーカーとしてworker.phpを用意します。

<?php
// worker.php
// ワーカー(コンシューマー)
$worker = new GearmanWorker();
$worker->addServer();
$worker->addFunction('log', 'my_log');
while ($worker->work());

function my_log($job)
{
sleep(60);
file_put_contents('/tmp/foo/' . $job->workload(), $job->workload());
}

ワーカーをバックグラウンドで動作させるため、実行するときには"&"を付けます。

$ php worker.php &

worker.phpについて説明します。これは、GearmanWorkerオブジェクトを生成し、addServer関数でジョブサーバーを追加します。引数なしでaddServer関数を呼ぶと127.0.0.1がジョブサーバーとして追加されます。その後、ワーカーとして提供する関数をaddFunction関数で追加します。この例では、60秒のスリープの後に/tmp/foo以下にログを吐き出すmy_log関数を"log"という名前で(クライアントに)提供します。

次に、クライアントとしてclient.phpを用意します。

<?php
// client.php
// クライアント(プロデューサー)
$client = new GearmanClient();
$client->addServer();
for ($i = 0; $i < 10; $i++) {
// $client->do('log', $i);
$client->doBackground('log', $i);
}

こちらは、何も考えずに実行することができます。

$ php client.php

client.phpについて説明します。これは、GearmanClientオブジェクトを生成し、addServer関数でジョブサーバーを追加します。GearmanWorkerオブジェクトと同様に、引数なしでaddServer関数を呼ぶと127.0.0.1がジョブサーバーとして追加されます。GearmanClientオブジェクトのdoBackground関数は、ジョブキューにジョブを追加し、すぐに終了します。この関数は第一引数にワーカーの関数名を、第二引数にワークロードを取ります。ワークロードとは、ワーカー側のworkload関数で取り出すことのできる値です。do関数はdoBackground関数と同様の引数を取りますが、結果を受け取るまで待ちに入る点が異なります。この他にも、GearmanClientオブジェクトには、ジョブを追加し最後にまとめて実行する関数なども存在します。詳細についてはPHPマニュアルのGearmanの項を参照してください。

client.phpを実行した後、/tmp/fooディレクトリを見ると次のようになります。

$ ls -l /tmp/foo/
total 80
drwxr-xr-x 12 y-mahata wheel 408 6 7 17:05 ./
drwxrwxrwt 22 root wheel 748 6 7 17:05 ../
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:56 0
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:57 1
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:58 2
-rw-r--r-- 1 y-mahata wheel 1 6 7 16:59 3
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:00 4
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:01 5
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:02 6
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:03 7
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:04 8
-rw-r--r-- 1 y-mahata wheel 1 6 7 17:05 9

client.phpはすぐに終了したはずですが、ファイル生成はきっかり60秒間隔で行なわれたことが確認できます。

注意するべきポイント

doBackground関数やdo関数の第二引数であるワークロードは文字列を受け取ります。したがって、文字列以外のデータ(例えば配列など)をワーカーに渡すときにはシリアライズをする必要があります。また、doBackground関数やdo関数は第三引数としてID(となる文字列)を渡すことができます。この値を指定すると、同一のIDで登録したジョブは一度しか実行されないので、注意してください。

一歩進んだトピック

Gearmanはデフォルトでメモリ上でキューを管理します。もしキューの管理をMySQLやDrizzleで行いたければlibdrizzleを使うことができます。また、ジョブキュー管理ソフトウェアはGearmanの他にも存在します。TheSchwartzQ4Mなどと比較することで、Gearmanの長所と短所をより理解することができるかもしれません。

参考資料