死活監視ツールGodが暴走するときの対処方法
結論
DelayedJobのような長時間実行するプロセスに対するGod設定にはgrace
を設定しましょう。
God.watch do |w| ... w.grace = 5.seconds ... end
Godとは
Godとはプロセスの死活監視ツールの一つです。プロセスが死ねば再び命を与え、プロセスがコンピューティングリソースを浪費するような悪さをすれば罰を与えるという、まさに『神』の如きツールです。
Rubyで作られたツールであり、設定ファイルもRubyによるDSLを用いて柔軟に構成することができます。 下記は、いろいろなサイトで紹介されているよくある設定です。
RAILS_ROOT = "/path/to/rails_root" RAILS_ENV = "production" 3.times do |num| God.watch do |w| w.name = "DelayedJob-#{num}" w.pid_file = "#{RAILS_ROOT}/tmp/pids/delayed_job.#{num}.pid" w.start = "cd #{RAILS_ROOT} && script/delayed_job --environment=#{RAILS_ENV} --identifier=#{num} start" w.restart = "kill -TERM `cat #{RAILS_ROOT}/tmp/pids/delayed_job.#{num}.pid`" w.stop = "cd #{RAILS_ROOT} && script/delayed_job --environment=#{RAILS_ENV} --identifier=#{num} stop" w.log = File.join(RAILS_ROOT, "log/god_delayed_monitor.#{num}.log") w.behavior(:clean_pid_file) w.start_if do |start| start.condition(:process_running) do |c| c.interval = 5.seconds c.running = false end end w.restart_if do |restart| restart.condition(:memory_usage) do |c| c.above = 500.megabytes c.interval = 10.seconds end end end end
この設定によって以下のような挙動となります。
- DelayedJobプロセスを3つ稼働させる
- 5秒ごとにプロセスの存在をチェックし、存在していなければ起動させる
- 10秒ごとにプロセスの物理メモリ使用量をチェックし500MBを超過したら再起動させる
- 再起動には
kill
を使いプロセスにTERMシグナルを送信する
- 再起動には
こまりごと
前述の設定で死活監視としてはきちんと動作します。しかし、監視対象がDelayedJobの場合に困った事象が発生します。
DelayedJobはTERMシグナルを受け取ったときにジョブ実行中の場合、割り込み処理で内部のフラグを立てたのち元のジョブ処理に戻ります。そしてジョブ実行が完了した後にプロセスを終了するという、gracefulな終了をしてくれます。
一方Godは、監視対象プロセスがメモリ超過したことを検知し、TERMシグナルを送信しますが、DelayedJobプロセスはすぐには死なない。なので再びTERMシグナルを送信します。が、やっぱり死なない... このときのGodの挙動がヒドイ。鬼神の如くこれでもかってくらい殺しにかかる。
2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:15+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:16+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:16+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:16+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... 2015-09-18T18:40:16+0900: [Worker(delayed_job.1 host:BG.EXAMPLE.COM pid:31039)] Exiting... ...
その数、1秒間に約12回。TERMシグナルを送るためにbashプロセスとkillプロセスがそれぞれ生成されるし、シグナル受け取った側はその度に割り込み処理と復帰処理が発生。ジョブが1時間くらいかかるものだった場合、その間ずっとこれらの処理が繰り返されます。おかげで、サーバーのCPU負荷が上がりまくり。
我々は神の怒りをかってしまった。この世に救いはないのか...
神の怒りを沈める方法
Godのソース(ver. 0.13.6)を眺めていたら下記の記述がありました。
def action(a, c = nil) ... case a when :start call_action(c, :start) sleep(self.start_grace + self.grace) # ← ん? when :restart if self.restart call_action(c, :restart) else action(:stop, c) action(:start, c) end sleep(self.restart_grace + self.grace) # ← んん!? when :stop call_action(c, :stop) sleep(self.stop_grace + self.grace) # ← おおぉぉ!!!? end self end
(ノ゚ρ゚)ノ ォォォ・・ォ・・ォ・・・・ 神は我らに慈悲を与え給うた...
Godのwatch
ブロックにはgrace
およびstart_grace
, restart_grace
, stop_grace
が用意されていました。これを設定すると再起動等のアクションが実行された後、通常の監視状態に戻る前に指定時間だけsleepしてくれます。
God.watch do |w| ... w.grace = 5.seconds ... end
この設定をいれることによって、穏やかに再起動が実行されるようになりました。めでたし。
まとめ
GodでDelayedJobを監視したときに再起動処理が短時間に何度も繰り返される挙動を緩和する方法を紹介しました。
余談ですが、死活監視にGodというメタファーを用い、ウェイト時間にgrace(慈悲、寵愛)という名前を付けるセンスが羨ましい...