Webアプリと同じプロセスにworker入れてお金が節約できる。


Webアプリは “リクエスト来る→サーバーで処理→レスポンス返す” というのを繰り返すわけだが、サーバーでの処理に時間がかかる場合にそこを別のプロセスに任せて、先にレスポンスを返しておいて、あとで結果は取りに来てよ、という実装をする事がある。


時間がかかる処理は2つに大別できる。

  1. 動画をエンコードするとか。CPU負荷が高くて時間がかかるのでWebサーバーとは別の場所で動かしたい
  2. Twitter APIを10回ぐらい使った結果をまとめて返すとか。CPU負荷は低いけどIO待ちが長い
2の方について、HerokuのRuby環境で安く上げる方法をまとめる。


手法

HerokuのcedarスタックでRuby使う時はwebサーバーとしてThinが起動する。
ThinはEventMachineの中で動いてるので、EM::defer等が使える。
Herokuは1プロセス目は無料、2プロセス目を起動させると課金されるが、EventMachineでworkerをWebアプリのプロセス内に同居させればお金がかからなくなる。


非同期処理

例えば、ユーザー登録されたらメール返す処理の場合
post '/regist_user' do
mail_addr = params[:mail]
send_mail(mail_addr, 'hello!!') ## メール送信する処理
redirect '/' ## トップページに戻す
end
これだとメールが送信されるまでレスポンスを返せないので、ブラウザが固まる。
また、Sinatra+Thinを1プロセスしか起動していない場合、1リクエスト/レスポンス処理するまで次のリクエストが処理できないので、
メール送信されるまで他の人からのアクセスを全員待たせる事になる。


重い処理をEM::deferで囲むだけで、そこは別スレッドで処理される。すぐにレスポンスが返るので、みんな待たずに済む。
post '/regist_user' do
mail_addr = params[:mail]
EM::defer do
send_mail(mail_addr, 'hello!!') ## メール送信する処理
end
redirect '/' ## すぐレスポンス返す
end


たとえばTwitter APIを連続使用する時は1秒間隔を空けろとか指定があるけど、そういうのをSinatraのプロセス内で行うならEM::deferでやってしまうのが良いと思う。
お金払ってdelayed_jobを使わなくても、EM::deferで囲むだけで非同期になるので便利。
CPUに負荷がかかるタイプの処理は素直にお金払って別のプロセス起動したほうがいいと思う。


ジョブキュー

処理時間が長い仕事が大量にある時、仕事のリストを作っておいて、順番に処理していくという手法がある。
普通はDB等に仕事を保存しておいて、Webアプリとは別のプロセスが順番に処理し、結果をDBに入れておくとかするけど
これもWebアプリに内蔵させられる。

config.ru でSinatraアプリを起動した後にEM::deferを書いておくと、Webアプリとは別スレッドでずっと動き続けるループが作れる。
require 'sinatra'
require 'eventmachine'
## (略)

run Sinatra::Application

EM::defer do
loop do
sleep 5
next if @@jobs.empty?
job = @@jobs.shift ## ジョブ1つ取り出す
## job処理する
end
end


起動しといたスレッドにジョブ追加する
post '/add_job' do
@@jobs.push '仕事'
end
キューにはDBとかgearmanとか使ったほうが良いと思う。


注意点

しばらくアクセスが無いと、プロセスがkillされる。クリティカルな処理は大量にキューに入れて処理しない方が良い。
メモリ使用量も注意するべき。Dyno(プロセス)ひとつあたり512MB割り当てられていて、1.5GB超えたら再起動すると書かれている。

実際はアクセスが無いと長くても2,3時間以内にはkillされてる気がするので、この方法あんまりアテにしない方が良いと思う。


あと、この「Sinatraにワーカー埋め込む」というのはEventMachine内で動くWebサーバー(ThinやWebrick)だけで使える方法なので、
自分のサーバーでUnicornやApache+Passengerで運用する場合は動くコードなのか知らない。調べてない。