HerokuのWorker再起動問題を考える

HerokuのWorker再起動問題を考える

こんにちは。

9月にこんな記事を書きました。

JavaでHerokuのWorkerを作成する

Heroku上のWebアプリをPlayframeworkで作るなら、WorkerもPlayで作るのが楽だよね!という話だったんですが。。。
実はこれ、ちょっと微妙な罠がありました。(--

 

★すべてのDynoは1日に一回再起動する

Herokuを運用しているとわりとすぐに気が付くと思いますが、WebDynoはだいたい24時間サイクルで再起動しています。
このあたりの挙動は以下のドキュメントに記載されています。

https://devcenter.heroku.com/articles/dynos#automatic-restarts

1日に一回サイクルすると書いてありますね。
僕はWebDynoについてはこの挙動はアリだと思っています。
WebDynoが仕事中であるか否かはRouterは把握しているはずなので、Routerが適切にWebリクエストをせきとめてくれるなら少なくとも原理上はリクエストの取りこぼしは発生しませんし、耐障害性を考えてもこのアーキテクチャは優れていると思うからです。

(prebootが早く標準になってほしいというのはありますけど。気になる人は「heroku labs」と叩いてみましょう。(^^v)

しかしWorkerDynoまで同じサイクルで再起動するのはどうなんでしょうね???
ちなみに再起動の方法は以下のドキュメント

https://devcenter.heroku.com/articles/ps#graceful-shutdown-with-sigterm

最初にSIGTERMを送って10秒待っても終了しなかった場合はSIGKILLで殺すっていう感じですね。
Workerが仕事中であるかどうかは外部からは知りようがないので夜間バッチなどの長時間かかるタスクを実行中であっても問答無用で再起動がかかりそうです。。。(--

 

★Workerの動作を観察する

んで、実際にWorker( on Playframework1.2.5)を数日間動かしてみて以下のような観測結果を得ました。

  • 再起動間隔は約24時間だが厳密ではなくかなり前後する
  • 予想通り長時間のJobを実行中であっても問答無用で再起動はかかる
  • SIGTERMはRuntime#addShutdownHookで捕まえることができるが、フックしたところでできることがほとんどない

さてさて困ったものです。(--
再起動間隔はざっと見た感じ24時間よりも若干長い傾向があるので仮に最初に午前中にWorkerを開始したとしても時間の経過とともに、夜間バッチの時間帯に再起動がかかるということはありえます。
ていうか、そもそも「Workerは夜間バッチの時間から遠く離れた時間に起動すること」みたいな謎の運用ルールはアウトでしょう。

つまりここまでの結論としては、Workerは

  • 実行に長時間かかる中断不能な処理
  • 処理が途中で中断された場合にやり直しのきかない処理

を扱うには不適ということになってしまいます。。。
マジでかーーー!!!(--

 

★Schedulerを併用する

さて。気を取り直して、どうにかしてこの問題に対処しなければならないわけですが、最初に考えたのはSchedulerでherokuコマンドを実行するというものです。
Jobを動かす時に「heroku ps:scale worker=1」として不要になったら「heroku ps:scale worker=0」とするという作戦です。
この方法にはWorkerのdyno時間を節約できるというメリットもあります。

Schedulerで実行できるコマンドは「heroku run bash」で確認できるのでとりあえず試してみます。

 

>heroku run bash
Running `bash` attached to terminal... up, run.1
~ $ heroku ps
bash: heroku: command not found
~ $

 

あ~。。。(--
heroku環境にherokuコマンドはないようです。。。。
時間帯によるWebDynoの増減とか割と用途はある気がするんだけどなぁ。。???(--

ちなみにJavaのHerokuAPIを入れるとそれはちゃんと動きました。
ひょっとするとSSHの鍵から入れないといけないんじゃないかと憂鬱だったけどそれは必要ないらしい。(gitは入っているので、そこで必要なんだと思う。)

でもAPIキーをどっかから取ってきて、HerokuAPIを実行するプログラムを書いて、とちょっと面倒くさい。。。(--

□□□□

次にやったのはplayコマンドをworkerとして起動していたframeworkIDを指定して実行するという方法。

 

play run --http.port=$PORT --%worker

 

当たり前だけどこっちはちゃんと動きます。
これでWorkerとして定義したJobは実行できるようになったけど、Schedulerは永続的なプロセスではないのでJobが終了したらPlayをシャットダウンするように修正しないといけません。もうこの際Jobが終了したらそこでstopしてしまえば良いでしょう。

実際にテストで使用したクラスはこれ

 

import play.jobs.Job;
import play.jobs.OnApplicationStart;
import play.Play;
import java.util.Date;

@OnApplicationStart
public class HerokuTest extends Job {
	
	public void doJob() {
		if ("worker".equals(Play.id)) {
			System.out.println("framework id: " + Play.id);
			System.out.println("Job start: " + new Date() + ", " + Play.id);
			try {
				Thread.sleep(5 * 60 * 1000);
			} catch (InterruptedException e) {
				System.out.println("Job Interrupted: " + new Date() + ", " + Play.id);
			}
			System.out.println("Job end: " + new Date() + ", " + Play.id);
			new Thread() {
				public void run() {
					Play.stop();
					System.exit(0);
				}
			}.start();
		}
	}

}

 

stopするのに新たにスレッドを作っているのはOnApplicationのJobはPlay#start内で動くのでそこを抜けてから終了したいからです。(Play#startとstopはいずれもsynchronizedメソッドなのでstartを抜けるまでstopはブロックします。)

スケジューラのドキュメントによると数分を越えるような長い処理はWorkerを使えとありますが、たいていの場合はこれで十分でしょう。
ていうかWorkerが使えないからこんなことやってるんだっちゅーの(--

 

★まとめ

herokuのWorkerは良い仕組みだと思いますが、ちょっと惜しい感じです。
多くの場合Workerでは再起動のタイミングまでコントロールしたい気がします。(HerokuAPIを使ってアプリの中から制御するということもできそうですけど)

あとSchedulerでherokuコマンドは是非使えるようになってほしい!
いつだかのミートアップであった夜間はDynoの数を減らしたいという要件はこれができれば解決という気がします。

コメント(4)