GitHub製Resqueを使用したRubyでのバックグラウンド処理(バッチ処理)

18th Jan, 2010 | ruby resque

そこそこの規模のWebシステムになってくるとバックグランド処理(batch処理)は欠かせないものになってくる。メールの送信、データの日次、月次、年次処理、削除(フラグ)データのpurgeやバックアップ、等々いろいろな物が出てくる。

現在はBackgrounDRbを使っているが、いろいろといまいちなので今回Resqueを評価してみた。ちょっと触った段階での第一印象をメモ。

まず、バッチ処理系で評価のポイントになってくる部分はなんだろうかと考えてみると、なんと言っても見通しのよさと異常系の処理だと思う。画面系と違い、バッチ処理は「見えにくい」ところで実行されるので、その二つが特に大事になってくる。「知らないうちに止まっていました」では困るのがバッチ処理。

たとえば、

  • 異常時の処理
    • 無視?
    • 管理者に通知?
    • リトライ?
  • 復旧処理
    • タスクの削除
    • (問題を修復後)リトライ
  • 状態の監視
    • いくつのJobが残っているか
    • 失敗したJobの数はいくつか
     

そして重要なのは、上記の処理を任意の粒度で実行でき、それぞれが独立して動くこと。一つのタスクが失敗した場合に、色々な他のタスクもコケる、では困る。

これに加えて複数サーバでの並列処理や、二重化、管理機能、Queueの扱いやすさ(優先度や並列実行)など、Batch処理管理としての基本機能が考慮ポイントになる。それと、今回の自分のニーズとしてはRuby on Railsとの親和性の高さも大事になってくる。これらを踏まえて評価してみた。

さて、実際にResqueを触ったみた印象だが、「これ系のソフトウェアは使い方が難しい」という前提で、気合を入れて調査を始めたのだが、Resqueは非常に簡単だった。シンプルに物を考え、ソフトウェアを作るって大切だな、と改めて強く思った。(注: シンプルに考える != 機能を削りまくる)

まず、Resque でのJob管理がどういう構成になるのかを自分の理解したところを書いてみる。(ResqueのREADMEをまとめただけ)

Resqueの構成例:

単純にこれだけだ。この例では、Worker 1はQueue Aを専門で処理。Worker 2はQueue Bを優先的に処理し、Queue BのJobがなくなったら、Queue C, Queue Aの順で実行する。

また、Jobは以下のルールで処理されていく:

  • workerは自分が担当するqueueの中から優先度の高いものから優先的に処理する。
  • queueに登録されたjobは登録された順にworkerで実行される。
  • 1つのworkerは1つずつjobを実行する。
  • workerが実際のrubyプロセスで、Queueの中から一つずつjobを実行する。
  • queueはrubyのコードで定義され、jobはredisに格納される。
  • 複数のworkerが同じqueueを担当することができる。この場合でも当然1つのjobは全体で一回しか実行されない。
  • 複数の物理サーバでworkerを動かすことができる。
  • workerの実行は任意のマシンでコマンドを実行するだけ。複数台に跨っていようが、1台で何回に分けてworkerを生成してもいい。
  • workerの追加(実行)、削除(停止)は動的に好きなタイミングでできる。
  • queueの作成はrubyコードで定義する。
  • jobの作成とqueueへの登録もrubyコードで行う。
ほぼすべてのことがRubyのコードだけで定義できるのがかなりの魅力。別途設定ファイルを書いたり、Redisをいじったりする必要がないのがとてもいい。

実際に少し動かしてみる

まずシンプルにRails抜きで、Resque単体で見てみよう。

Resqueはqueueを保存するためにバックエンドにRedisを使う。今回は、ResqueとRuby関連はローカルのMac上に置き、redisはVMWareのFreeBSD上に置いた(参照: 仮想環境の勧め)。

redisサーバのインストールとサーバの起動

FreeBSDではパッケージ化されているので、databases/redisをインストールするだけでOK。他の*NIX系システムでもだいたい同じだろう。

redisはリクエストベースで自動的にデータベースが作られるようで、事前に何か作成しておく必要はないようだ。ただ起動するだけOK。実際に運用に入る前にはもう少しRedisのことも知っておくべきなので後で調査する予定。

関連するgemをインストール

$ gem install redis redis-namespace yajl-ruby rack resque

resqueのgitレポジトリの中にexampleが入っているのでそれを使う

$ git clone git://github.com/defunkt/resque.git

デモは、examples/demoの下に入っている。

Resqueはデフォルトではローカルホスト上にあるredisを使うことになっているので、変更したい場合には、

Resque.redis = 'redis-server.example.com:6379'

と定義する。今回のexampleでは、job.rb のrequire文の後ぐらいに書いておけばいいだろう。もちろん実際の運用では設定ファイルとかに行くべきだろう。

Resqueには、管理Webアプリが付属してるので、それを起動しておく。当然、このWebアプリは管理用なので動かさなくてもいい。

$ cd examples/demo
$ rackup config.ru

ブラウザで http://localhost:9292/ とかで管理画面が開ける。

さて、exampleのjob.rbを見てみよう。

require 'resque'
Resque.redis = 'redis.madoro.org:6379' # Redisサーバの設定

module Demo
  module Job
    @queue = :default

    def self.perform(params)
      sleep 1
      puts "Processed a job!"
    end
  end
end

@queue でこのJobがどのqueueを使うか、というのを定義している。:default というのがそれにあたり、これは任意の物を設定できる。(この例では:defaultだが、:defaultというもの自体には特別な意味はない)

self.performの中が実際にJobで実行される処理の本体。引数は任意の物が設定できる。ここではparamsとなっているが、任意のものを任意の個数設定できる。

このJobを作成しqueueに登録してみる。

$ irb
>> require 'job'
=> true
>>Resque.enqueue(Demo::Job, 'aaa')
=> "OK"

これで、登録が完了した。ただし、まだ1つもworkerを起動していないので、このJobはpending状態として実行はされない。

管理画面で見ると、

こんな感じになる。

次に、このQueueのためのworkerを作成し、このJobを実行してみる。

$ COUNT=1 VVERBOSE=true QUEUE=default rake resque:workers
(in /Users/masatomo/workspaces/resque/examples/demo)
(in /Users/masatomo/workspaces/resque/examples/demo)
*** Starting worker unknown-00-1f-5b-f3-c0-9e.home:85131:default
** [06:50:11 2010-01-18] 85131: Registered signals
** [06:50:11 2010-01-18] 85131: Checking default
** [06:50:11 2010-01-18] 85131: Found job on default
** [06:50:11 2010-01-18] 85131: got: (Job{default} | Demo::Job | ["aaa"])
** [06:50:11 2010-01-18] 85131: resque: Forked 85133 at 1263797411
** [06:50:11 2010-01-18] 85133: resque: Processing default since 1263797411
Processed a job!
** [06:50:12 2010-01-18] 85133: done: (Job{default} | Demo::Job | ["aaa"])
** [06:50:12 2010-01-18] 85131: Checking default
** [06:50:12 2010-01-18] 85131: Sleeping for 5
^C** [06:50:15 2010-01-18] 85131: Exiting...

COUNT=で起動するworkerの数、QUEUE=でこのworker(s)で実行するQUEUEを指定する。QUEUEはカンマ区切りで指定でき、書いた順がそのまま優先順位になる。

複数のサーバでworkerを実行したければ、このコマンドを複数のサーバで実行するだけ。とっても簡単に冗長化や処理分散ができる。

ちょっと、長くなってしまったので今回はこの辺で。次回はエラー処理と、Railsの連携とかを書く。


記事の内容についての質問、苦情、間違いの指摘等なんでもtwitterでどうぞ。