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の連携とかを書く。
そっちはどちらかと言うと共用サーバでの話。今回は自分自身が開発したり、ソフトウェアを評価したりするときにも仮想環境(特にJail)は便利だよ、という話。
普段、自分の開発環境はMac OS X(iMac / Macbook)なんだけど、両方ともVMWare Fusionを入れ、その上でFreeBSDを動かしている。そしてさらにそのFreeBSDの中にさらに複数の仮想環境であるJailが動いている。(ちょっとややこしい)
例えば、自宅のiMacの中のFreeBSDには以下のJailが入っている。
% ezjail-admin list
STA JID IP Hostname Root Directory
--- ----- --------------- --------------------- -------------------------
DR 1 192.168.252.204 rems.madoro.org /usr/local/jails/rems.madoro.org
DR 2 192.168.252.205 rems3.madoro.org /usr/local/jails/rems3.madoro.org
DR 3 192.168.252.207 puppets.madoro.org /usr/local/jails/puppets.madoro.org
DR 4 192.168.252.206 puppetc.madoro.org /usr/local/jails/puppetc.madoro.org
DR 5 192.168.252.208 puppetc2.madoro.org /usr/local/jails/puppetc2.madoro.org
DR 6 192.168.252.203 pgsql84.madoro.org /usr/local/jails/pgsql84.madoro.org
DR 7 192.168.252.202 pgsql82.madoro.org /usr/local/jails/pgsql82.madoro.org
DR 8 192.168.252.211 mongodb.madoro.org /usr/local/jails/mongodb.madoro.org
DR 9 192.168.252.209 mogilefs.madoro.org /usr/local/jails/mogilefs.madoro.org
DR 10 192.168.252.212 gerrit.madoro.org /usr/local/jails/gerrit.madoro.org
DR 11 192.168.252.201 couchdb.madoro.org /usr/local/jails/couchdb.madoro.org
DR 12 192.168.252.210 couchdb2.madoro.org /usr/local/jails/couchdb2.madoro.org
DR 15 192.168.252.213 redis.madoro.org /usr/local/jails/redis.madoro.org
これだけの仮想環境がVMWare上のFreeBSDに入っている。使わなくなった仮想環境は消しているので、上にあるのは現役で使っている仮想環境のみ。オーバヘッドが大きそうだなあ、と思う人もいると思うがVMWare的に割り振っているメモリは、かなり余裕を持たせて1GB。少し前までは512MBだったがDatabase系がメモリ食いなので増やした。最近はノート型PCでも4GBとかいけるので、1GBくらいなら許容範囲だと思う。CPUやディスク負荷的には余裕。もちろん激しいことをしたら全体が遅くなるが、そういうことが必要になった場合(たとえば負荷テストとか、大規模データ処理とか)には別の環境を用意すればいいだけの話。
デスクトップ用途のマシンにサーバソフトウェアは入れない
デスクトップ用途のMac上に色々なサーバソフトウェアを入れるのは好きではない。Macに限らず、Windowsでもなんでも「デスクトップ用」として使ってる環境にサーバ系ソフトを色々入れるのはよろしくない。とは言え、開発環境としてはオールインワンで開発に必要なすべての物が物理的な一台に入っていると便利なことも多いので(ちょっと外出先でプログラミングとかね)、自分は仮想環境を使っている。
仮想環境のすばらしいところ
で、仮想環境のいいところは、たとえば、たった今 redis.madoro.org をResqueの評価用に作った。現在評価中なんだが、その後「やっぱりResque使わない」となったとしよう。この場合ばっさりその仮想環境を消せば、すべてがなかったことになる。他の環境がまったく汚れない。もっとすばらしいのは、独立した仮想環境にインストールすることによって、「Redisをインストールしたら、PostgreSQLが動かなくなった!」(例です)とかアホな事態が避けられること。また、仮想環境ひとつずつが独立したIP addressで動くので、port 80がかぶるから、こっちは8080で動かそう、とか本質でないことを考える時間も節約できる。アホなことで時間を使わない、のが生産性向上の第一歩。
なぜ、FreeBSDのJailなのか
こういうことをするのには、もちろんどんな仮想環境でもいいんだけど、FreeBSDのJailを勧めたいのは、軽くて早いから、早いというのは、特に、インストールやアンインストールにかける時間の面がでかい。人間的な手間も少ないし、実際に作成にかかる時間も少ない。慣れれば1分とかで新しい環境ができる。Jail以外の仮想環境も以前は使っていたけど、怠け者の自分には色々手がかかりすぎで使わなくなってしまった。たとえば、「ちょっと空いた時間にResqueの評価がしたくなった。でも環境の設定に1時間かかる」とかだと一気に冷めてしまう(自分は短気なので5分でも冷めるかも)。
なんか、だらだらと書いてしまったけど、このFreeBSD/Jailを使った環境は、自分のこの10年の中でも革新的なことだったので、つい熱くなってしまう。紹介してくれた同僚の某氏ありがとう!
ただ、見てわかるようにほぼ静的なコンテンツのサイトなので、アクセス毎にアプリケーションサーバを走らせる意味がない。また、このVPSの一番安いコースにおいているので、あまり贅沢に資源を使いたくない。と言ったことから生成したhtmlをキャッシュして2度目のアクセスからはアプリケーションサーバやデータベースにアクセスしないようにしている。
Webシステムによっては、アプリケーションサーバで静的なhtmlファイルを作成し負荷の軽減をしたりするが、キャッシュファイルを自前で扱うのはvalidation等色々だるいので、このブログシステムではnginxに任せている。
今回はその設定の紹介
基本的にnginxは設定が簡単なので超オススメ。キャッシュの設定はたったこれだけ。全体の設定ファイルはこっちに置いとく。
http {
proxy_cache_path /usr/local/nginx/cache levels=1:2 keys_zone=cache-space:4m max_size=50m inactive=120m;
proxy_temp_path /usr/local/nginx/tmp;
server {
location / {
proxy_cache cache-space;
proxy_cache_valid 200 302 60m;
proxy_cache_valid 404 20m;
}
}
}
簡単に説明すると、まず、最初のproxy_cache_pathとproxy_temp_pathでキャッシュが格納されるディレクトリとサイズ等を指定する。詳しくはこの辺を見た方が早いが、この設定では、キャッシュ用に、メモリを4M使う、ディスクの最大サイズは50M、120分間アクセスがないキャッシュファイルは削除、の意味。
localtion / 内で、実際にどういうものをキャッシュするか決める。proxy_cache_valid には、httpのレスポンスコードと、そのレスポンスコードを返したときのコンテンツをどのくらいの時間キャッシュするかを書く。
この場合、正常処理である200または302を返した場合、そのコンテンツをnginxが60分間保持し、その後60分間、同じURLでアクセスがあった場合、アプリケーションサーバ(ここの場合はSinatra)に問い合わせずキャッシュをそのまま返す。
追記1:
Twitterにて: 「なぜ404は20分にしているのか」というのがあったが、正直なところあまり深い意味はないが、設定したときは以下のことを考えていた。
- 404を返すだけなら、アプリケーションサーバの負荷はたいしたことなさそう。
- 404はエラーなので(相手側のエラーの場合と、実際にファイルがない場合)、何か直す必要がある場合にはすぐ反映されるように。
追記2:
Twitterにて: 「データ更新をしても最大60分間は実際の更新が行われないということか」というのがあった、そのままだとそうなる。でも自分は更新時の処理でCacheをクリアしている。ただし、NginxにはCacheを消すという正式な方法がまだ用意されていないのでアンオフィシャルな方法である「単純にcacheディレクトリの下のファイルをすべて消す」というので運用している(cacheディレクトリの下のディレクトリは消してはダメ)。今のところそれで問題は起きてない。追記: 部分的にcacheをpurgeする方法を書いた。
abを使って簡単なベンチマークを取ってみた
nginxのキャッシュなしの場合:
% ab -n 100 -c 20 http://blog.madoro.org/mn/14
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking blog.madoro.org (be patient).....done
Server Software: nginx/0.8.31
Server Hostname: blog.madoro.org
Server Port: 80
Document Path: /mn/14
Document Length: 5844 bytes
Concurrency Level: 20
Time taken for tests: 9.049 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 605444 bytes
HTML transferred: 588577 bytes
Requests per second: 11.05 [#/sec] (mean)
Time per request: 1809.781 [ms] (mean)
Time per request: 90.489 [ms] (mean, across all concurrent requests)
Transfer rate: 65.34 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 161 174 9.2 173 202
Processing: 450 1522 301.2 1456 2268
Waiting: 224 1090 286.8 1059 1893
Total: 613 1696 306.1 1620 2454
Percentage of the requests served within a certain time (ms)
50% 1620
66% 1789
75% 1808
80% 1822
90% 1853
95% 2439
98% 2449
99% 2454
100% 2454 (longest request)
nginxのキャッシュありの場合
% ab -n 100 -c 20 http://blog.madoro.org/mn/14
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking blog.madoro.org (be patient).....done
Server Software: nginx/0.8.31
Server Hostname: blog.madoro.org
Server Port: 80
Document Path: /mn/14
Document Length: 5844 bytes
Concurrency Level: 20
Time taken for tests: 3.236 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 656124 bytes
HTML transferred: 636251 bytes
Requests per second: 30.90 [#/sec] (mean)
Time per request: 647.157 [ms] (mean)
Time per request: 32.358 [ms] (mean, across all concurrent requests)
Transfer rate: 198.02 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 162 177 10.1 174 201
Processing: 329 389 74.3 351 791
Waiting: 164 176 16.7 167 234
Total: 494 566 75.5 527 957
Percentage of the requests served within a certain time (ms)
50% 527
66% 599
75% 617
80% 628
90% 652
95% 667
98% 817
99% 957
100% 957 (longest request)
格段にレスポンスがよくなっているのがわかる。
このような並列のアクセスだけなく、単体のアクセスのレスポンスも早くなるので、負荷が高いサイトだけでなく(ここのような)零細なサイトでも恩恵を得ることができる。最近のコンテンツは動的な部分にJavascriptを使うことが多くなっているので、このようなキャッシュがしやすくてよい。