読者です 読者をやめる 読者になる 読者になる

devひよこのあしあと

いつでもひよっこな気持ちで学びと挑戦を

死活監視ツールGodが暴走するときの対処方法

God Ruby DelayedJob

結論

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(慈悲、寵愛)という名前を付けるセンスが羨ましい...