「キュー」は何しにLaravelへ?

タグ: Laravel4   Laravel5  

追記:2014年12月31日より、iron.ioの料金体系が変更されます。この記事で説明しているエラーキューやリトライ機能は、有料プランでしか使用できなくなります。無料プランのAPI利用回数は10Mから1Mへ引き下げられます。

当サイトでもキューについて、既に過去記事で軽く触れています。Web上には、いくつかLaravelのキューについて解説している記事もあります。

実際の導入方法は、これらの記事にお任せして、この記事では、キューの基本的な概念と、これを使って何が嬉しいのかを解説します。

キューとは?

キュー(queue)とは、コンピューターの世界では、待ち行列を指します。混んでいるときは何かを待たせておき、最初に行列に入れた要素が最初に取り出され、処理される仕組みのことです。

まさに、行列のできる店を考えてもらえばよいでしょう。日本人は真面目ですから、ちゃんと行列の後ろに並び、空席ができると列の最初の人から、入店します。それをコンピューターで行う仕組みのことです。

ワークキューとは?

ドキュメント中でも使われている「ワーク(work)キュー」とは何かですが、この記事のために明確な定義があるのか調べました。しかし、見つけることが出来ませんでした。Linuxが持っている仕組みの名前にワークキューが使われています。多分、推測ですが、昔からOSが扱う実行の単位はジョブ(job)と言っており、この新しい仕組みを表すために、Jobをworkに変え、そう名づけたのでしょう。

これ以外に、通信の仕組みにもワークキューという名前が使われていました。また、メッセージキューとの対比として、優先順位付けができたり、グルーピングができる特徴を持つものをワークキューと呼んでいる場合もありました。更に、一定の作業を順番通りに自動的に行う仕組みをワークキューと呼んでいるソフトウェア・アプリもあります。

このように様々な状況で、違いのあるものを同じ言葉で呼ぶのは、特にコンピューター業界には多いですね。そして、混乱を招き、何が正しいのか要らぬ議論を呼びます。

ここでの理解を進めるために、Laravelでのワークキューが何かを定義づけるとしたら、「処理すべきデーターと、どのクラス/メソッドで処理するかを配列として待ち行列に預け、後ほど順番に処理する仕組み、もしくはこの機能を持った(ファサード)クラス」として理解しておきましょう。

もっと、大雑把に言えば、「ある仕事の単位をワークとし、それを先入れ先出し方式で待たせ、順番に処理する仕組み」です。このくらい大雑把にすると、ただのキューの定義と違いはさほどありません。処理単位をワークと呼んでいるというポイントだけ押さえておきましょう。

なお、一般的にはキューに投入する単位はジョブと呼びますが、このポストに限り「ワーク」と呼びたいと思います。一般的な言い方ではありませんので、このポストを元に別の記事を書こうなんて思っている方は、注意してください。さらに、キューに溜まっているワークを処理するプログラムを「ワーカー」と呼ぶことにします。こちらの用語はサーバー動作させるソフトウェアで頻繁に利用されます。

何が嬉しいの?

Webアプリの利用者、大抵の場合はネットの向こうでページを見ている閲覧者を時間のかかる処理で待たせることが少なくなることです。ユーザーエクスペリエンスの向上です。

一番理解しやすく、使われる場合が多いのはメールです。そのため、Laravelのメールでは、わざわざキューに一度入れてから送信する専用メソッド、Mail::queue()が用意されているほどです。

ユーザー登録を考えてみましょう。情報を入力してもらい、「登録」ボタンを押してもらいます。メールアドレスが実際に存在するものか、確認メールを送りましょう。キューを使わなければ、内部で作成したメール文章を即時送信するため、メールサーバーとやりとりを開始します。このやりとりは環境により多少時間がかかります。例えばGmailのSMTPサーバーを使用する場合、場所が遠いですし、たくさんのトラフィックを処理しているサービスですので、送信終了まで数秒、ときには10秒程度かかることもあります。その間、ユーザーをただ待たせることになります。

「時間のかかる仕事は後回し」にするため、「この新しいユーザーに確認メールを送る」仕事(ワーク)をキューに登録しましょう。キューへの登録は、通常とてもすばやく行えます。登録し終えたら、すぐにユーザーに対し、「登録メールが届きます。確認作業をしてください。」と表示できます。ユーザーはメールが多少遅れることがあることは経験上理解し、納得してくれるでしょう。メールが届くまで、多少は待ってくれるでしょう。

登録ボタンを押してから、次のページが表示されるまでの間に待たされると「遅いサイト」とか「できが悪いサービス」と罵られますが、ページがすぐに表示されれば、メールが多少遅れても「そんなものだろう」と理解してもらえます。たとえユーザー登録にかかる、全体の時間が変わらないとしてもです。

他の場合にも使えます。例えば、ユーザーからプロフィール画像を登録してもらい、サイトの表示に合うように拡大・縮小する必要があるとしましょう。一覧で表示したりする場合に使えるサムネイル画像の生成も必要だとします。

ユーザーはアップロードに時間が多少かかるのは経験から知っています。しかし、アップロード後に「加工が終わるまで」待たせられれば、どうして反応が無いのか訝しむでしょう。

例えばサイトが大当たりして、一度に何人もプロフィール画像を登録したとしましょう。同時に画像加工が行われたら、何が起きるでしょうか?節約のため、サーバーにCPUが1つか2つしかないVPSを使っていたら?

画像の加工は比較的CPUとメモリを多く使用します。そのため、こうしたリソースが画像加工に食いつぶされてしまいます。実行プロセスのプライオリティ付けでワーカーの優先度を下げていないと、Webサーバーの反応は遅くなり、レスポンスが帰ってこなくなります。せっかくサイトがブームしたのに、レスポンスがスローダウン、最悪停止し、ユーザーを逃がすことになります。

キューにワークを登録するとはどういうことでしょうか。簡単に言えば2つの優位性があります。一つは、実行するタイミングをずらせること。例えば、サイトのアクセスが増えても、世界的な大ヒットでもない限り、深夜から朝方にかけてはアクセスが落ちるものです。その時間は、サーバーのリソースに余裕があります。「重たい」処理をまとめて行うにはピッタリです。サムネイルが翌日まで表示されなくても、たいていのサイトでは支障はないでしょう。

もうひとつの利点は、そのワークに利用できるリソースを制限できることです。キューはワークを順番に溜めていきます。それを処理する側のワーカーを1つ用意すれば一つずつ、2つ用意すれば同時に2つずつ処理されます。

例えば実働サーバーで、画像の加工が1プロセスとメール送信2プロセスまでなら同時に処理が発生しても、Webサーバーのレスポンスには影響が少ないと判明したとしましょう。それならば、画像の加工とメール送信のワークキューに分けてワークを登録します。そして、それを処理するワーカーを画像加工には1つ、メール送信には2つ用意します。これで、ユーザーが少ない夜中まで待たなくても、常時決めた数だけ起動しているワーカーにより、ワークが順番に逐次処理されます。「画像加工を夜中のバッチ処理で行うなんて、時代遅れだよな」などとブツブツ言う、「よく知っている」うるさ型のユーザーの口もこれでふさげますね。

どうです?Laravelで簡単にキューが使えるのは、喜ばしいことだと感じ始めてもらえましたか?

機能の分散

ワークキューを使用するように設計・開発すれば、ワークを投げる側と、それを処理する側で機能を分離できます。Laravelの場合、ワークにはデーターだけでなく、クロージャーが登録されます。コードが含まれるのですから、設計や使い方によっては分離されたり、されなかったりすることになるでしょう。

開発にLaravelを使いはじめる当初は、将来の拡張性を考慮して、機能の分離などを細かく設計し、開発する使い方は少ないかと思います。そこまで詳細に設計するのであれば、LaravelのQueueクラス使用は最善手ではないかも知れません。使用するキューサービスやライブラリーに直接合わせて設計するほうが良いでしょう。現状、Laravelがまだまだ規模の大きな開発よりは、小規模での開発に多く使われている状況ですので、単にキューを「なんだか便利そうだから使ってみるか」と言う程度の使われ方が多いだろうと想像できます。

小さなアプリでは、ワークの依頼側と処理側は同じLaravelプロジェクトとして開発し、同じサーバーで依頼と処理を行うことになるでしょう。もちろん、依頼側と処理側を別々のLaravelプロジェクトとして、独立させ開発することもできます。ワークの処理側をLaravelを使わずに、開発することさえ可能です。例えば、とてもたくさんのワーカー処理を行う必要があるとしましょう。Laravelは比較的起動時の準備に時間がかかるフレームワークです。キューで依頼されたワークを処理するワーカー側をできるだけ効率的に作成したいという方針で、生PHPを直接使用することにし、「軽い」ワーカーを作成することもできます。(起動時の負荷を減らす方法として、Artisanでワーカーをデーモンモードで動作させる方法もあります。詳細は後ほど。)

キューサービスにアクセスできるのであれば、ワークの依頼側とワーカーを別々のサーバーに載せることも可能です。別々のサーバーに、Web UIとワーカー処理の両方を持つ、同じ一つのLaravelプロジェクトを乗せて、片方にはWeb UIを担当させ、もうひとつにはワーカーの処理を担当させます。もしくは、初めからWeb UI担当のプロジェクトと、ワーカー担当のプロジェクトとして、完全に機能を分離し、それぞれを別のサーバーに乗せ、負荷対策にすることも考えられます。

最初にそこまで考える必要がない場合でも、想定以上にサイトがブームし、改善が必要になる場合のことも考え、「重い」処理を何らかの形で分離できるようにするためにキューを使用しておくのは、いざという時の保険にもなります。

キューサービス

Laravelではデーターベースエンジンを選択するのと同じように、ドライバーを切り替え、キューサービスを選択できます。つまり、キューサービス自体は、Laravelに含まれていません。外部のキューサービスを使います。

キューサービスは特定のサーバーに自分で用意することもできますし、既存のWebサービスを利用することもできます。syncとRedis以外のドライバーを利用する場合、使用するサービスに合わせて、ドキュメントに記載されているパッケージをcomposer経由でインストールする必要があります。

beanstalkd

beanstalkdはワークキューを提供するオープンソースのプログラムです。大抵のLinuxディストリビューションでは、標準リポジトリーに含まれており、簡単にインストールできます。リポジトリに含まれていなくても、ソースを取得し、簡単にmakeできます。

利用も簡単です。(日本語情報が少なく、プロセスの起動を永続化させる方法の説明がされているため、面倒なように思えますが、beanstalkd自身は単純です。)気をつける点は一つで、デフォルトではキューはメモリ上に存在しており、何らかの原因でbeanstalkdが停止した場合、キューの内容が消失してしまうことです。それを防ぐため、キューをファイルに書き出しておく機能も持っています。-b起動オプションでディレクトリーを指定するだけです。保存しておくディレクトリーは予め作成しておいてください。存在しないとエラーになります。

RabbitMQ

RabbitMQは名前が示す通り、メッセージキュー(Message Queue)です。とは言え、キューシステムとしてはbeanstalkdより人気があるようです。プラグインの機能で拡張すれば、プライオリティーを付けたり、グルーピングしたりと、ワークキュー同様に動かせるようです。とはいえ、Laravelのキューは、ワークキューの機能とされる、こうした機能はサポートしていませんので、RabbitMQでも使用できるようです。

デフォルトでドライバーはLaravelに含まれていません。有志の手により開発されたドライバーが公開されており、composerでインストールできます。

このドライバーがLaravelのキュー機能を100%サポートしているかは不明です。

iron.io

iron.ioはキューサービスを主体として提供しています。一月に1000万回のIPAアクセスまで無料で使用できます。(1キューを処理するには、だいたい5APIを使用します。)

特筆すべきは、PUSH型のキューを提供していることです。通常のキューはPULL型です。つまり、ワーカーがキューサービスに対して、登録されているワークが存在しているかを尋ねるのですが、PUSH型の場合、キューサービス側から通知してくれます。つまり、メールやhttp(s)アクセスを通して、登録されたワークの内容が届きます。

何が嬉しいのかといえば、このあとに詳しく説明しますが、ワーカーを設置したり、起動したりするPUSH型で必要な手間をかける必要がないのです。指定したURLにワークの内容が届けられます。それを処理しますが、Queueファサードに専門のメソッドが用意されており、決め打ちのコードだけで処理は済んでしまいます。

これも以降で説明しますが、PULL型を利用する場合、ワーカーの起動をおこなうArtisanコマンドのオプションにより、リトライ回数や間隔を指定します。このPULL型を使用する場合は、iron.ioの設定画面で指定することになります。

LaravelでPUSHキューを使用する場合、多少まごつく点は、Artisanコマンドのqueue:subscribeを実行する前に、iron.ioの操作パネルにより、使用するキューを予め作成しておくことです。このコマンドは1キューに付き、一回実行すればOKです。正しく実行すれば、iron.ioの操作パネル上で、当該キューの"Push Queue"タブに"Active"アイコンが付き、"Subscribers"に登録したURLが表示されます。一度Activeになれば、URLの変更や追加は、この操作パネルで指定できます。"Subscribers"の左側、"Push information"でリトライ回数や間隔を指定します。

PUSHキューの場合、HTTPコードでLaravelからiron.io側へ、処理の成否を知らせます。200なら成功です。それ以外は失敗とみなされ、指定したリトライ間隔ごとに、リトライ回数まで実行されます。指定したリトライ回数繰り返しても200が帰ってこない場合は、失敗したワークとしてみなされ、"Push information"の"Error Queue Name"で指定したiron.ioの別のキューに結果が通知されます。

サーバー側でbeanstalkdや、RabbitMQを自由に起動できない、一般の共有サーバーや制限の多いSaaPなど、もしくは手数をかけずにお手軽にキューを実現したい場合、このiron.ioのPUSH型一択になるでしょう。iron.ioは通常のPULL型サービスも提供しています。

SQS(AWS)

SQSはシンプルなキューサービスです。100万回のAPI利用まで無料で利用できます。東京にもリージョンがありますから、遅延少なくキューサービスが利用できます。問題点となり得るのは、AWSの場合、キューに投げられた順番で必ずしもワーカーに渡されるとは限らないことです。「できるだけ順番通りに渡します」というベストエフォート方式です。

厳密に順番通りに実行したい場合は、別のサービスを利用するか、一工夫が必要です。

Sync(同期)

ドライバーにはsyncも指定できます。これはキューサービスを使用するドライバーではありません。キューへの依頼はLaravel内部で即時に処理されるため、ワークのロジックが終わるまで待たされます。それが「同期」という意味です。機能的にこれを使用して、なにか優位なことがあるという意味合いはありません。

このドライバーの存在意義は、ローカル環境でキューの設定ができなかったり、面倒であったりする場合でも、キュー機能を利用した開発が行えることです。本番環境で実際にキューサービスを利用する時点で、同期・非同期処理の違いにより不具合が出てくることもありますが、それでもこのドライバーを使用すれば、コードのパスを一通り開発環境で通してみることができます。

Redis

新しく追加されたばかりです。今のところどうやって使用するのか、情報を集めていません。

ワークの正体

例えば、メールのキュー送信を使うとしましょう。以下のような実装を考えます。

Mail::queue('emails.queue', [], function($m)
    {
        $m->to('hiro.soft@gmail.com', 'Hirohisa Kawase')
            ->from('hirokws@gmail.com', '川瀬')
            ->subject('キューーー!');
    });
});

メールのビューである、emails.queueはどんなものでもかまいません。

上記のMail::queueメソッドが実行されると、キューにワーカーが一つ保存されます。そのワーカーの中身は、次のようになります。

{
    "job":"mailer@handleQueuedMessage",
    "data":{
        "view":"emails.queue",
        "data":[

        ],
        "callback":"C:38:\\"Illuminate\\\\Support\\\\SerializableClosure\\":169:{
            a:2:{
                i:0;s:140:\\"function ($m) {
                    \\n    $m->to(\'hiro.soft@gmail.com\',
                     \'Hirohisa Kawase\')->from(\'hirokws@gmail.com\',
                     \'\\u5ddd\\u702c\')->subject(\'\\u30ad\\u30e5\\u30fc\\u30fc\\u30fc\\uff01\');\\n
                };\\";i:1;a:0:{

                }
            }
        }"
    }
}

ご覧の通り、シリアライズされたクロージャーとデーターです。もう一つ、見てみましょう。今度はクラスを使ってワークを登録してみます。

class Work {
    public function calc( $job, $data ) {
        $shiharai = $this->keisan($data['tanka'], $data['ninzu'], $data['zei']);
        $user = User::FindOrNew($data['id']);
        $user->shiharai = $shiharai;
        $user->save();

        $job->delete();
    }

    private function keisan($tanka, $ninzu, $zei)
    {
        return $tanka * $ninzu * $zei;
    }
}

ワークをキューに登録します。

Queue::push('Work@cals', ['tanka'=>2000, 'ninzu'=>35, 'zei' => .10]);

このワークの実態はどんなものでしょう。

{
    "job":"Work@cals",
    "data":{
        "tanka":2000,
        "ninzu":35,
        "zei":0.1
    }
}

渡された$dataの内容が、そのデータを処理するクラス@メソッドと共に、シリアライズされています。つまり、これは何を表しているのでしょうか。

公式ドキュメントのキューのページに記載されている、クロージャーに対する記述に注目してください。

注目: useを通じてキューするクロージャーでオブジェクトを使用できるようにする代わりに、主キーを渡し、キュージョブの中で関連するモデルを再取得することを考慮してください。これにより、予期しないシリアライズの影響を防ぐことができます。

実際には、クロージャーに限った話ではありません。

例えば、Eloquentモデルのインスタンスは、複雑で大きなものです。上記で分かるように、記号などはエスケープされ、日本語もエンコードされてサイズが大きくなっています。もし、元々大きなEloquentモデルインスタンスを渡したら、シリアライズされ、さらに大きくなってしまいます。

そして、使用するキューサービスごとに、利用可能な一つ当りの最大ワークサイズが決まっています。もし、それを超えてしまえば当然エラーになり、キューに投入できません。

基本的にワークとして登録される情報は、コードも含め、できるだけ小さくするように注意しましょう。

実際の話、この注釈が付け加えられた理由は、使用しているシリアライズライブラリーのバグで、一部のデーターに不具合が出たことがあったからです。そこまで責任取れないので、できるだけ文字列等は避け、整数などを渡し、ワーカー側で処理してねというのが、元々の意図だと思われます。

それと、Webを通してのアクセス単位である「セッション」に依存する情報をクロージャー内で取得することはできません。例えば、InputファサードやCookieファサードの取得系メソッドは、クロージャー内で利用できません。処理されるのは、その「セッション」の生存期間ではないからです。Mail::queueメソッドは、クロージャーを利用する方法であることに注意を払いましょう。

クロージャーを使わない場合でも、例えばサーバーが複数に分かれており、ファイルシステムが共有されておらず、ワーカーが別々のサーバー上で動作するなら、読み書きする先のファイルは、ワーカーごとにそれぞれ別になるでしょう。

また、もしキューへの登録時間がワーカーで必要になるのであれば、ワークに情報として渡しておく必要があります。ワーカー側でシステムタイムを取得しても、それはワーカーが起動しているその時のもので、当然、ワーク登録時の時間ではありません。

ワーカーが動作する環境と時間は、ワーカーが作成され、登録された時点とは異なることを意識しましょう。また、複数ワーカーが動作する場合、それぞれのマシンごとの環境は当然異なるでしょう。意図した通りに動作させられるように、設計・計画しましょう。

最後に確認を。ワーカーの中から、リターンで値を返しても無駄になります。(誰が受け取るのでしょう?)処理の結果は必要に応じ、永続性のあるストレージに保存するのが一般的です。データベースのテーブルに保存したり、ファイルに書き込んだりしましょう。

ワーカーコマンドの使い方

キューにワークを登録しても、それだけでは何も起きません。使用するキューサービスにワークが溜まる一方です。

手動でも自動でもかまわないので、キューに溜まっているワークを実際に行ってくれる、ワーカーを起動する必要があります。ワーカーをどんな言語や(クロージャー内に依存する部分を含まない限り)どんなフレームワークで開発しても構いません。フレームワークやライブラリーを使用せず、直接使用する言語で作成することもできます。

しかし、Laravelでキューに投げた仕事の処理は、Laravelで行うのが簡単です。ワーカーを制御するArtisanコマンドも用意されています。

一番簡単なワーカーの起動コマンドは、ドキュメントのキューのページ、「キューリスナーの実行」のセクションに書かれている方法です。

php artisan queue:listen

このコマンドで行われていることは:

    1. ワークが存在されているか、キューサービスに問い合わせる(ポーリング)
    • 1.1 存在していれば、登録されている一番古いワークを一つ処理する
      • 1へ戻る
    • 1.2 存在していなければ、1へ戻る

情報の存在を問い合わせることを、コンピューター用語ではポーリングと言い、特に通信の分野ではよく使われます。

この場合、ワークの処理後、もしくはポーリングの結果存在していない場合、どちらもすぐに1のポーリングに戻ります。つまり、このArtisanコマンド(PHPスクリプト)は休まずに動き続けます。

この使い方をすると、ワーカーは1CPUの数パーセントを使けるでしょう。キューに新しいワークが登録されると即時に処理が開始されるのは利点ですが、CPUを無駄に使い続けるのはマイナスです。

iron.ioでは1000万API、SQSでは100万APIを無料で使えますが、この状態では1秒間に何回もポーリングのためにAPIを連続して叩くため、APIの無駄な消費が多くなります。つまり、この状況では無料枠はあっという間に無くなってしまうでしょう。もし、1秒間に10回ポーリングしており、一回のポーリングに1API使用するとしても、一日で84万4千回です。(もしかしたら、ポーリングで1API以上使用するかも知れません。)100万APIで2日も持ちません。

そこで、ひと工夫です。オプションを付けます。

php artisan queue:listen --sleep=10

この場合の動作です。

    1. ワークが存在されているか、キューサービスに問い合わせる(ポーリング)
    • 1.1 存在していれば、登録されている一番古いワークを一つ処理する
      • 1へ戻る
    • 1.2 存在していなければ、10秒スリープする
      • 1へ戻る

見つからない場合に、10秒間の猶予を与えます。ワーカーがスリープしている間、コンピューターの他のプロセスに実行時間を譲ります。CPUの使用状況を確認してみれば、キューにワークが溜まっていない時、たまにCPUを数パーセント使用するが、たいていの時間全く使用しないPHPのスクリプトが見つかります。これで大分CPUとAPI呼び出しは大分節約できます。

もちろんスリープ時間は、長いほどCPUの節約にはなります。しかし、スリープ時間は、登録されたワークが起動を待たされる最大時間でもあります。どの程度が最適なのかは、処理する内容と重要度により決めましょう。

タイムアウト時間を指定し、実行時間を制限することもできます。--timeoutで各ワークごとの実行時間を制限します。(確か)時間を費やしてしまうと、例外が発生します。(記録があやふやです。タイムアウトの場合は例外が発生し、試行回数が1進み、ポーリングに戻ったと思います。皆さんで確認してください。私も後ほど機械があれば調べ直し、書き直しておきます。)コードは読んでいませんが、このタイムアウト時間は、たぶんPHPのコード実行時間だと思われます。

ドキュメントでは後ろの方に記述があるのですが、queue:listenコマンドは「失敗したジョブ」を取り扱う機能と一緒に使うのが一般的でしょう。この機能は、試行回数を指定し、その回数試行しても成功しない(削除されない)ワークをデータベーステーブルに登録しておき、管理しやすくするものです。

この機能を使うには、テーブルを用意する必要があります。どのデータベース接続、テーブルを使用するのかは、queue.php設定ファイルで指定します。

'failed' => array(
        'database' => 'mysql', 'table' => 'failed_jobs',
    ),

続いて、上記で指定した管理テーブルを作成するマイグレーションを生成し、実行しましょう。

php artisan queue:failed-table
php artisan migrate

管理テーブルが作成されました。リトライ回数を指定し、ワーカーを起動します。

php artisan queue:listen --sleep=10 --tries=3

ワーカーを3回試しても、そのワークが削除されなければ失敗管理テーブルに登録される仕組みです。(初心者の方のため、際確認しましょう。PULL型ではジョブの実行が成功したら、そのワークの中で、そのワーク自身を削除します。)まずは、失敗したワークをリスト表示します。

php artisan queue:failed

表示される情報の一番左が、ジョブIDです。この番号を指定し、失敗したワークを操作します。再度、実行キューに乗せ直すのであれば:

php artisan queue:retry ジョブIDの整数

削除するのであれば、同じくIDを指定します。

php artisan queue:forget ジョブIDの整数

多分、一番多用することになるのは、次のコマンドです。失敗した全ワークを失敗管理テーブルから削除します。

php artisan queue:flush

時間のかかる仕事を一つのキューに任せ、順番に行うこともできますが、より組織立てて使用することもできます。

例えば、キューにbeanstalkdを使用し、3つのキューの使用を計画します。設定ファイルを以下のように指定します。

<?php
return array(
    'default' => 'powerjob',
    'connections' => array(
        'powerjob' => array(
            'driver' => 'beanstalkd',
            'host'   => 'localhost',
            'queue'  => 'default',
            'ttr'    => 60,
        ),
        'mail-low' => array(
            'driver' => 'beanstalkd',
            'host'   => 'localhost',
            'queue'  => 'lowmail',
            'ttr'    => 60,
        ),
        'mail-high' => array(
            'driver' => 'beanstalkd',
            'host'   => 'localhost',
            'queue'  => 'highmail',
            'ttr'    => 60,
        ),
    ),
    'failed' => array(
        'database' => 'sqlite', 'table' => 'failed_jobs',
    ),
);

上記の場合、プロモメールや一斉送信するお知らせメールなど送信にある程度時間がかかっても良いメールは、mail-lowのコネクションを使用し、問い合わせや個別の連絡など即時性が求められるメールには、mail-highを使います。メール以外の時間やCPUを喰う仕事は、デフォルトのpowerjobに任せると設計したとしましょう。

powerjobに投げられるワークは、CPUを喰う可能性があり、そのため一度に複数実行されては困ります。ですから、常にワーカーは一つだけ建てておきましょう。

php artisan queue:listen --sleep=60 --tries=2

一方のメールでは、2段階のプライオリティーに分けて、キューに登録します。もちろん、mail-highに登録したメールをmail-lowに登録したものより優先して送信します。こんな場合は、優先順序を指定して、ワーカーを立てます。

php artisan queue:listen --sleep=60 --tries=3 --queue=mail-high,mail-low

同じワーカーを一つだけしか建てられないわけでありません。例えば上記の場合、もしメールサーバーの能力が高く、たくさんのメールを捌けるのでしたら、2つでも3つでも同時に立てて、ワークの処理スピードを向上させることが可能です。実際には、メールには一日あたりや1時間あたりに送信できる制限が、プロバイダーにより決められていることが多いですから、滅多矢鱈に送信するのは止めましょう。

では、処理するワークの数に制限を付けたいときはどうしましょうか。残念ながら、Artisanコマンド側では面倒を見てくれません。(でも、いいアイデアですよね。オプションで指定できるようにするのは。)

実行回数の制御をシェルスクリプト側で組めば(もしくは、他のコマンドを呼び出せますので、新しいArtisanコマンドを作成することもできます)、一度に一つだけのワークを処理する、queue:workコマンドが役に立ちます。

php artisan queue:work --tries=3 --queue=mail-high,mail-low

注意: 実際にこのようなオプションの使い方ができるのか調べてはいません。つまり、このコマンドでtriesオプションや、sleepオプションがどのような振る舞いをするのか、把握していません。今回は3回まで試行するという意味合いで付けましたが、もしかしたら、queue:workコマンドでは、一回失敗したらすぐに処理を抜けてくるのかも知れません。

いずれにせよ、通常は1ワークの実行後に制御が戻ってきます。このコマンドの実行回数を数えれば、それがワークの実行回数です。決められた回数だけ実行するか、時間が過ぎたら実行を停止するシェルスクリプトを書いておき、それを一定時間ごとにcronで回せば、時間毎の回数制限が実現できるでしょう。

デーモンモードの使い方

このqueue:workコマンドにはデーモンモードが用意されています。対比のため、まずqueue:listenの動作を理解しましょう。

queue:listenでは、ワークを処理するごとにLaravelの環境が整えられます。これには利点と欠点があります。利点は環境がフレッシュであることです。開放し忘れたリソースがたまり、サーバーの動作を危険に晒したり、コマンドが途中で中断したりするなどのリスクを避けることができます。欠点は、実行ごとにLaravelの準備コードを整えるのですから、実行時間も遅くなり、CPU時間も消費します。

ドキュメントで暗示されている通り、サーバーの資源を浪費してしまうリスクはあるが、複数のワークを処理するにあたり、フレームワークの環境を毎回捨てずに、連続してワークを処理することで、起動にかかる時間分を節約しようというのが、デーモンモードです。ドキュメントには、サーバーをメンテナンスモードにする方法が書かれていますが、これは必ずそうしなくてはならないわけでなく、そうしたほうが良いですよというお勧めです。

php artisan queue:work --daemon --tries=3

注意: キューサービスへ安定してコネクションできない時(特にBeanstalkd、多分Redisを使用する場合も同様)、デーモンモードを使うのは止めましょう。動作するCPUを100%使用してしまい、制御が戻ってこなくなります。コネクションが不安定であれば、いざという時に停止できるように、ワーカープロセスのプライオリティーを下げておくことを考えましょう。

これは、デーモンモードがその名の通り「デーモン」として動作する場合も考え、メッセージを表示しないために起きる現象だと考えられます。OSを通じ、メッセージを出力するには、OSへ制御を一度渡すことになります。それにより、OSはCPU時間を必要としている他のプロセスに適切に制御を渡すことができるため、ワーカーがCPUを独占することはありません。ところがデーモンモードではメッセージを出力しないため、OSに制御を戻すことはなく、CPUを専有したままキューサービスとコネクションを貼ろうと繰り返し、結果独占状態になるのでしょう。(たぶん、バグとして扱われるべきです。)

ちなみにsleepを付けておけば避けられそうな感じがしますが、このオプションはキューが空になった時点で、次のポーリングまでの間隔を指定するものなので、コネクション自身が確立できない場合には役に立ちません。

基本的にqueue:workは「一つだけ処理する」ためのコマンドですが、daemonスイッチにより、「ワークがキューに存在する限り処理する」モードになるため、queue:listenと似た動作になります。違いは前述の通りです。

たぶん、queue:listenではなく、queue:workをデーモンモードで使用する場合は、「仕事を休みなくどんどんこなす」のが目的になるでしょうから、sleepを指定しないのが通常でしょう。(もちろん、何かの資源がビジー状態になる可能性があり、再実行まで少し時間を取ることが有効であれば、指定する意味はあります。)

もう一つの特徴は、queue:restartコマンドにより、queue:workデーモンモードのプロセスは終了することです。これは、作業のワークフローとして考えると理解しやすです。先ず、こんなシェルスクリプトを考えてください。(言葉に馴染みのある方なら「夜間バッチ」のイメージです。)

昼間の時間、ユーザーから依頼されていた処理をまとめて実行するシステムがあります。深夜になれば自動的に、もしくはオペレーターさんが手動で、スクリプトを実行します。

#!/bin/bash

# インストールディレクトリーへ移動
cd /var/www/htdoc/laravel

# サイトをメンテナンスモードへ
php artisan down

# 処理能力を上げるため、バックグラウンドでワーカーを3つ立てる
php artisan queue:work --daemon --tries=3 &
php artisan queue:work --daemon --tries=3 &
php artisan queue:work --daemon --tries=3 &

3つのワーカーがどんどん溜まっていた処理をこなしていきます。さて、まだキューにはやり残したワークがありますが、そろそろサイトを立ち上げる時間になりました。別の起動スクリプトを実行します。

#!/bin/bash

# インストールディレクトリーへ移動
cd /var/www/htdoc/laravel

# ワーカーを停止する
php artisan queue:restart

# ここで、1ワークの処理時間程度スリープするほうがより良い

# メンテナンスモードを終了し、サイトを動作させる
php artisan up

queue:restartコマンドが実行されると、ワーカーへシグナルが送られます。もし、ワーカーがワークを処理中の場合、そのワークを完了した時点で、コマンドが終了します。ワークを処理中でなければ、即座に終了します。ドキュメントには再起動させると書かれていますが、実際にはコマンドを終了させます。これらのコマンドが死活管理され、コマンド(プロセス)が死んだ場合に外部から起動がかけられる仕組みになっていれば、ドキュメント通りに再起動されることになるでしょう。

注目: デーモンモードでは、ワークを一度処理し終えると、再度キューにワークが投入されても処理されません。プロセスが自動で停止することもありません。この振る舞いは、サイトのメンテナンスモードと関連しているかも知れませんが、そこまで今回調べていません。

ここまでが、キューサービスにポーリングを行う、PULL型のワーカー利用でした。Laravelでは、iron.ioが提供するPUSH型の通知を簡単に利用できる仕組みも用意されています。

PUSHキューの使い方

現在のところ、iron.ioにベンダーロックされてしまいますが、ワーカーを実働環境に用意し、それを死活管理し、必要に応じ再起動させる手間を省くため、PUSH型のキューを利用できます。ポーリングしませんのでワーカーは必要ありませんし、APIの利用数も押さえることができます。

PUSHキューを使用するために必要な前準備です。

  1. iron.ioにアカウントを作成する。
  2. 指示に従い、最初のプロジェクトを作成する。
  3. プロジェクトにキューを作成する。
  4. APIのキーを管理画面からプロジェクトIDとトークンを確認する。
  5. 確認したプロジェクトIDとトークンをqueue.php設定ファイルに指定する。

注意: ドキュメントから落ちていますが、2014年1月22日のプルリクエストで、queue.php設定ファイルのironドライバー用の設定項目に、encryptが追加されています。'encrypt'=>true,がデフォルトです。存在しないとエラーになります。2月19日はhostが追加されており、管理画面から指定している、AWSのリージョンかRackspaceのサーバーロケーションを指定します。

これで、PULL型のキューとして利用できるようになりました。PUSH型を利用するにはひと手間必要です。

php artisan queue:subscribe queue_name http://通知先ドメイン/通知先URI

このArtisanコマンドは、iron.ioのAPIを叩き、キューにワークが登録された時点で、それを通知するURLを登録します。

一度通知URLを登録すれば、iron.ioの管理画面から、削除したり、追加したりが可能になります。ただし、全部削除してしまうと、登録できなくなります。その場合、再度このコマンドを実行します。(追加ボタンをクリックしても、何もエラーが表示されないため、非常に分かりづらいです。最初の一つは、APIを叩いて登録する必要があり、そのためにコマンドが用意されているわけです。)

続いて、同じプロジェクトにこのPUSHキューが処理できなかった場合の情報を保存しておく、エラー用のキューを別に作成しましょう。そのキューの名前をこのPUSHキューで指定します。PULL型の場合は、Laravelがテーブルに「失敗したジョブ」として保存していました。PUSH型の場合は、iron.ioの別のキューで保存しておくわけです。ただし、再度実行キューに投入することはできません。管理画面で状況やデータを確認する程度の使い方になります。(もちろん、このエラーキューをPULL型で使用することもできますが…それなら、初めから全部PULL型でやったほうが早いでしょう。)

通知URLの処理は、ほぼ定形コードになります。コントローラーアクションの一例です。

public function handleSendMail( $token )
{
    # クエリーストリングを使用するなら、$token = Input::get('token')になるだろう。
    if( $token != '300dkdclkdj9DKJF+dd' )
    {
        Log::notice('異なったトークンによる、handleSendMailへのアクセスがありました。');
        return Response::make( '', 404 );
    }

    return Queue::marshal();
}

Queue::marshal()がワーク処理の面倒を見てくれます。このメソッドだけ呼び出すのは、セキュリティーホールを作成することになるため、避けましょう。キューに登録されたワークの通知を受け取るURLは一般に公開してはいけません。万が一のため、確認のためのトークンも一緒に送ることを考えてください。PUSHキューの管理画面から、各登録URLごとにヘッダー情報を追加できますので、よりセキュアにするために、ヘッダー情報を確認することもできます。

Queue::marshal()はワークが正常終了すると、レスポンスに200を返します。それを受け取ったiron.ioは、当該ワークを完了にします。200以外であればエラー扱いになり、管理画面で設定した回数リトライします。リトライしても200が返されなければ、登録したエラーキューに情報を通知し、ワークを削除します。

iron.ioのPUSHキューは設定のしかたを覚えてしまえば、お手軽に使用できます。ポーリングしませんので、APIもさほど消費しません。ただ、URLへアクセスできなくてはならないため、外部からのhttpアクセスを受けられない開発環境では、テストできないのが欠点です。

個人で開発・使用するサイトであれば、キューを楽に使用したい場合、これをまずおすすめします。