cronと比較して
*nix系のシステムだと、伝統的に、何かを定期的に実行したいcronを使うのだけど、アプリケーションからcronを使う場合、いくつかの使いづらい点がある。
- アプリケーション本体から離れた場所でcronの動きを管理しないといけない。通常、/etc/crontab 等をメンテナンスしないといけないのだけど、そこはアプリケーションの外側なので、メンテナンスするときにアプリケーション側で使っている方法を使いづらい(SCMもそうだし、deployとかも別で考えないといけない)。
- cronから起動する場合、起動する環境について考えないといけない。環境変数が代表例。アプリケーションと同じような環境になるように注意しないといけない。
resque-schedulerを使うと、cronでするようなことをアプリケーション内で完結することできる。crontabのようなものをアプリケーション内に置けるので、アプリケーション本体を管理するSCMを使うこともできるし、deployもアプリケーションと常に同じ方法でできるようになる。
ただ、基本的にはcronの動きを踏襲しているので、cronの以下の(場合によっては)負の特徴も引き継いでいる。
- cronと同じように、指定した時間にresque-schedulerが立ち上がってない場合、その処理は行われない。
- cronと同じように、同一のタスクが同時に走ってしまう場合がある。例えば、毎分実行するような設定を書いた場合、その処理に1分以上かかってしまった場合、前の処理が終わる前に次の処理が走ってしまう。それが致命的な場合、自分でロック処理を書く必要がある。
- 複数のresque-schedulerを走らせることが考えられていない。複数のresque-schedulerを実行すると、同じ時刻に同じタスクが複数回走ってしまう。つまり、「冗長化のために、2台のアプリケーションサーバ上でresque-schedulerをそれぞれ動かす」ということができずに、冗長化した構成が取れない。
最後のは、嫌なので、複数のところで走らせても同一のタスクが複数回走らないようなパッチを書いて作者に投げてある。しかし、「冗長化構成が取れるように」ということには賛同を得たのだけど、そのパッチについてはいくつか課題がありそうなので保留になっている。気が向いたらもうちょっと頑張るつもり。
使い方
resque-scheduler を見るのが早いが、crontabで定義するようことをyamlで定義する。そのドキュメントの例を使わせてもらうと、
queue_documents_for_indexing:
cron: "0 0 * * *"
class: QueueDocuments
args:
description: "This job queues all content for indexing in solr"
こんな感じになる。この場合、毎日0時0分に、QueueDocumentsというResqueのJobがQueueに入ることになる。ただし、ResqueのQueueに入るだけで、resque-schdulerは実際の実行はしない、ということに注意。Queueを処理するのはResque自体のWorkerの仕事になる。
deployについて
resqueと同じように、resque-scheduler自身には、起動スクリプトみたいなものがついていないので、godやmonitのようなもので管理するのがいいっぽい。
自分はmonitを使って、こんな感じのwrapperを書いて、monitではそのwrapper経由で、起動、停止をしている。
これとこれの続き。この後、もう少し調査して、Resqueを実際のシステムの一部で使い始めてみたのでその感想とメモ。
前回までのあらすじ
Resqueはバックグラウンドでジョブの実行をするもので、かなりの大規模サイトでかつ更新系の処理が多そうなシステムであるGithubで開発され使われている。よくある使い方としては、「Web UIを軽く見せるため、処理の依頼だけを受け付け、実際の処理はバックグラウンドで実行」「バッチ処理などで、大量のJobをQueueに突っ込んでおいて、(複数の)workerで並列で効率よく処理」などがある。
不安なところ
Resqueの大きな特徴は、QueueをRDBMSではなくRedis上に作るところにある。Redisは、Memcacheのようにシンプルに使え、すべてのデータはメモリ上に展開されるのでとても速く、データはディスク上にも永続化されるので、何かあったときにもMemcacheと違いデータが揮発しないのが特徴のkey-value型のデータベース。
今回Resqueを導入したWebシステムは、よくあるRDBMSを使ったものなので、Resqueのために新たに別な種類のデータベースを導入することへの躊躇いが多少あった。サーバ設定の手間も増えるし、各開発者の開発環境のセットアップの手間も増える。また、常時使うデータベースのようなものが一つふえると言うことはメンテナンスや障害時の手間も増える。また、RDBMSならすぐ中を確認できるが、Queueの状況が見づらいのではないか、と言った心配もある。
正直なところ、上記の不安はまだ全部それなりに残っている。が、魅力も見えてきたのでとりあえず使ってみることにした。実際に使ってみないとわからないこともあるしね。
良さそうなところ
上記のような不安がある中で、導入に踏み切った大きな理由として、QueueをRDBMSに置かずにRedisに置く、というのが実際の運用ではかなり良さそうというのがある。
メインで使っているRDBMSと同じところにQueueを置くと、RDBMSの影響をモロに受けてしまう。せっかく重い処理をバックグラウンドでするためにResqueを使っても、そのQueueの登録部分が重くなってしまっては意味がない。Queueの部分が別DB(Redis)だとこの辺のことに影響されず「とりあえずQueueに入れる」「後はサーバの状況に合わせQueueを順番に処理する」といったことができるようになる。
後は、Resque自体の作りが非常に良さそう。例えば、 convention over configuration な感じがよくできていて、新しいQueueやJobを作るときにあまり難しいことを考えずに、Rubyのコードを書くだけで済む。
Deployに関して
Resqueには、起動スクリプトのようなものが添付されていないので、何か自分で起動させる方法を考えないといけない。Githubではgodを使っているようだ。今回うちでは普段使っているCapistranoとMonitを組み合わせてDeploy時に(再)起動とプロセスの監視をすることにした。また、Resque(のworker)は、自分のプロセスとしてRailsの環境を読み込みので、アプリケーションのコードが変わった際には再起動の必要がある。
まず、Resqueはpidを保存してくれたりしないので、monitのFAQに従い、簡単な起動用のWrapperを書きそれをmonitに監視させている。Capistranoでのdeployプロセスでは、monitのrestartコマンドを呼ぶだけでResqueのworkerも再起動してくれるようになった。
ちょっとこの辺が最初だけとは言えめんどくさい。
監視に関して
ここがまだ弱いのだけど、とりあえずResqueに添付されているWebアプリケーションを目視するという弱い方法で行っている。ここは改善予定。待て次号!
とりあえず、こんなところ。
前々回、前回に引き続き、もうちょっとResqueの練習。試しに、このブログの更新情報をTwitterに流す仕組を作ってみた。twitterfeed的なことをする感じで、ブログに投稿したら、Twitterにリンクをポスト、またN時間後にポスト、という感じで。うざそうなので実際には使わないかも。
追記: resque-schdulerのcron的な使い方について別のエントリで書いた。
このN時間後に投稿の部分にresque-schedulerを使ってみた。
Twitterへ投稿するライブラリは、grackleを使ってみた。特に深い理由はない。単純にポストするだけだから何でもいいかな、という感じで、簡単そうに使えそうなのを選んだ。
とりあえずインストール。
gem install grackle
gem install resque
gem install resque-scheduler
Twitterにポストするためのクラスを作成する。このクラスは、 performメソッドを実装する。その中でtwitterに投稿するコードを書く。performの引数としてPostしたい文字列を渡すことにする。
class MyTwitter
@queue = :normal
def self.perform(status)
options = Sinatra::Application.options
client = Grackle::Client.new(:auth=>
{:type=>:basic,
:username=>options.twitter_username,
:password=>options.twitter_password})
client.statuses.update! :status=>status
end
end
Rakefileにschedulerのタスクを追加:
require 'resque/tasks'
require 'resque_scheduler/tasks'
これで、準備完了。
scheulderを起動する。
rake resque:scheduler
後はTwitterに投稿したいところで、
# すぐに投稿したい場合 (resque標準)
Resque.enqueue(MyTwitter, "post post post")
# 10時間後に (resuque-scheduler)
Resque.enqueue_in(10.hours, MyTwitter, "post post post")
# 投稿したい日付が決まっている (resuque-scheduler)
Resque.enqueue_at(Time.local(2010, 1, 31, 10, 30), MyTwitter, "post post post")
と言った感じ。とても簡単。
正確には、実行される時間ではなくて、Queueに入る時間なので実際には少し後に実行される。
Railsとの話の前に、前回書き忘れてしまったのだけど、resqueには、1日1回実行する、と言ったスケジューリングの機能はない。スケジュール機能は別のそういう機能を持ったソフトウェアに任せる(代表例: cron)か、自分で作る必要がある。また、resque-scheduler といresqueのプラグインタイプの物もある。現在どの方法が良さそうかか評価中なのでそのうち書く。
さて、Railsとの連携だが、resque自体がそもそもGitHubのRailsシステム用に作られたという経緯から、もちろん非常に親和性が高い。たとえば、worker毎にRailsのEnvironmentが一回ロードされるだけなので余計な資源を食わなかったり、RailsアプリのWeb UIから非同期な処理の扱いなども簡単にできる。
さて、インストール
./script/plugin install git://github.com/defunkt/resque
gemでインストールする方法ある。詳しくは http://github.com/defunkt/resque 参照。
設定
これも特に難しいところはない。前回も書いたように必要になるのは Redis サーバを指定するところのみ。作るファイルは3つ。全部上記READMEに書いてある。
- config/initializers/resque.rb
- lib/tasks/resque.rake
- config/resque.yml
Workerの起動
インストールが完了するとRailsのrake taskに以下の二つが追加される。
- rake resque:work # Start a Resque worker
- rake resque:workers # Start multiple Resque workers
このコマンド経由で実行するとRailsの環境を読み込んだWorkerが起動し、Jobの中でRailsとまったく同じ環境でプログラミングすることができる。
optionとかは単体での起動と同じ。
例: QUEUE=default rake resque:work
非同期処理
Web系アプリではレスポンスの速さがとても大事なので、処理に時間がかかる部分は、とりあえずレスポンスだけユーザに返してしまって、残りの処理は裏でやるということがよく行われる。resqueを使うとこの処理を簡単に行うことができる。と言ってもresque側で何か特別な非同期用の仕組が用意されているわけではなく、Web側のアプリでは、単に Resque.enqueue を呼ぶだけでの話。そして、Jobの中で再度元のクラスのmethodを呼ぶということで簡単に実現できる。この辺、変に凝った仕組ではなくとても簡単でいい。
まとめ
なんか色々なことが簡単にできてしまって、何かを見落としているのではないかという気分になる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コードで行う。
実際に少し動かしてみる
まずシンプルに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の連携とかを書く。