JavaでHerokuのWorkerを作成する
こんにちは。
このブログで何度も取り上げていることからもわかると思いますが、FLECTでHerokuアプリを作成する場合の第一選択は Playframework 1.2.xです。
これは当社にJavaの技術者が多いことが最大の理由ですが、Playの生産性は非常に高くパフォーマンスも良いので Webアプリを作る分には何の問題もありません。
その一方でHerokuのWorkerをJavaで作る場合はどうするのが良いんだろう?ということがずっと自分の中で課題として残っていました。
自前でmainを持つサーバーアプリケーションを作ってしまえば良いんですけど毎回それはさすがに面倒ですもんね。
そんな折ちょっとした示唆があってWorkerもWebアプリと一緒にPlayで作っちゃえば良いじゃん!ということに気がつきました。
以下はその方法です。
★PlayでJobを作成する
Playframeworkを使ったことのある人ならご存知かとは思いますが、PlayにはJobという強力なスケジューリング機構があります。
- 何秒おき、何時間おきといった指定間隔でのインターバル実行
- 毎日何時に実行
といったスケジューリングがAnnotationの宣言だけでできるのです。
この機能を使えばWebDynoを作る際にバックエンドJobも同時に組み込むことができますが、実のところそれはあんまりうれしくありません。
何故ならWebDynoは負荷によってスケールさせたいので何台起動するかがわからないからです。WebDynoが5台あがっていたらバックエンドJobも5回動作することになりますが、多くの場合これは意図するところではないでしょう。
なので定義したJobはWebDynoでは動かしたくありません。あくまでWorkerとしてだけ動かしたいわけです。
実はこれ簡単にできます。
以下にサンプルを示します。
import play.Play; import play.jobs.Job; import play.jobs.Every; import java.util.Date; //@Every("10s") @Every("${jobtest.every}") public class JobTest extends Job { public void doJob() { System.out.println("Job: " + new Date() + ", " + Play.id); } }
上記は日付とPlayのフレームワークIDをコンソールに出力するだけの単純なJobです。
ポイントは@Everyの宣言でパラメータを使用していることです。
@Everyまたは@Onの引数に「${xxx}」というパラメータを使用した場合、その値はapplication.confから読み込まれます。
★application.confでスケジュールを定義
あとはフレームワークIDごとのスケジュールをapplication.confに追加してやればOKです。
デフォルトの設定で使っている場合は、
- ローカル開発環境ではフレームワークIDなし
- 本番(Heroku)環境では「%prod」
としている人が多いと思いますが、その場合の設定は以下のようになります。
jobtest.every=10s
%prod.jobtest.every=never
%worker.jobtest.every=5s
%worker.application.mode=prod
「%prod」「%worker」はそれぞれWebDyno用、WorkerDyno用のフレームワークIDです。
application.confでは同じキーの設定が複数回出現した場合は後に記述されたものが有効になるので、それぞれの環境毎の設定が行えます。
設定値を「never」とした場合そのJobは実行されなくなります。
最後の「%worker.application.mode=prod」はWorkerDynoでのPlayをProductionモードで動作させるための設定です。
DEVモードでは最初のHTTPリクエストが届くまでPlayは動作しないので、これがないとWorkerDynoはうまく動きません。
★Procfileの作成
最後にWorkerでもPlayを動かすようにProcfileを定義します。
web: play run --http.port=$PORT $PLAY_OPTS
worker: play run --http.port=$PORT --%worker
上の例ではWebDynoのフレームワークIDはHeroku環境変数「PLAY_OPTS」で、WorkerDynoのフレームワークIDは直接指定していますがこの辺はお好きなように。
WorkerDynoでもportにbindしてHTTPリクエストを待ちうけていますが、HerokuのルーターはWebDynoにしかリクエストをforwardしないのでここにHTTPリクエストが来ることはありません。
★メリット、デメリットを考える
いかがでしょう?(^^;
この方式ではWebDynoとWorkerDynoを一つのアプリケーション内で同時に作成できるのが最大のメリットです。
ローカル環境で開発/テスト中はWebとWorkerを1プロセスの中で同時に動かすことができます。
WebDynoでは使用しないJobのインスタンスがひとつ余計に作られることになりますが、その程度ではアプリの動作に何の影響もありません。
WorkerDynoはまったく使用しないWebアプリをまるまる抱えることになり、それはまぁちょっと気になるところではありますが。。。。
おそらくWorkerDynoのスペックもWebDynoと同等と思われるので気にすることはないでしょう。
WorkerDynoが待ちうけているportは多分外からアクセスできないと思うのでこれも問題ないはず。
ここにリクエストが飛んでくるようならそれはHerokuのセキュリティホールです。(^^;
Heroku上にステージング環境を持つ場合でもフレームワークIDでどうにでも対応できるので柔軟性にも問題なし。
他に何か考慮すべきことあるかな???
ん~、圧倒的にメリットの方が多い気がします。
こうして考えるとPlayでHerokuアプリを作る場合Workerはこれがスタンダードでも良い気がしますね。(^^v
@Every, @On の中身ですが、 cron.xxxx とすれば、${}で囲まなくても読まれますよ。
投稿: ikeike443 | 2012年9月 7日 (金) 13:16
ソース読んで発見した書式だったので知ってますよ。(^^;
なんとなくですが知らない人が見た時に「cron.xxx」だとなんじゃそら?と思いそうだけど、「${xxx}」だと設定ファイルかな?と思いそうなのでこちらにしました。
投稿: konishi | 2012年9月 7日 (金) 13:30
%worker.jobtest.every=5s
の様に
%worker.jobtest.every=0 0 12 ∗ ∗ ?
と書くとParseExceptionが発生するのですが、解決法等ご存知ですか?
投稿: hotchemi | 2012年10月29日 (月) 05:56
everyではcron書式は使えないので@Onですよね?
hotchemiさんが書かれたものをコピーしてみたら確かにエラーになったのでなんでだろうと思って調べてみたら「*」がU+2217になってますよ。(^^;
ASCIIの「*(U+002A)」を使えば多分大丈夫ではないかと。(^^;;;
投稿: konishi | 2012年10月29日 (月) 10:10
あ、なるほど…。本家チュートリアルをコピーしたら無事にいけました。
@Onの話でした!言葉足らずで申し訳ありません。
投稿: hotchemi | 2012年11月 1日 (木) 22:38