LoginSignup
22
12

More than 5 years have passed since last update.

Sidekiqが謎の理由でスタックしたので状態監視して強制再起動させた

Last updated at Posted at 2016-12-25

Sidekiq、非常に便利でRailsでキューイングするとしたらSidekiq一択っていうくらい好きなんですが、とあるプロジェクトで実行中のキューがいつまでも終わらず、ワーカーが埋まってしまいキューがたまり続ける…という謎の現象に出くわしました。

サーバ負荷自体も特になく、ジョブ内容も変な処理などはないし、タイムアウトにすらならないという…。根本原因を探って直すのが良いのですが、再現性もなく突発的に起きるため、まずはサービス提供が継続できるようにSidekiqの状態を監視して、ワーカーが停止していたら強制再起動させて復帰させるというアプローチを取りました。

Sidekiqの状態取得

Sidekiqのワーカーを取得するにはSidekiq::Workers.newを呼びます。この中でワーカーがいつ実行開始されたのかがwork["run_at"]に記録されています。

workers = Sidekiq::Workers.new
workers.each do |process_id, thread_id, work|
  p "at: #{Time.at(work["run_at"])}"
end

こいつを監視して実行時間が想定以上になっていたらSidekiqを強制再起動させます。

Sidekiqの停止

停止のAPIも用意されています。

ps = Sidekiq::ProcessSet.new
ps.each(&:quiet!) #USR1
ps.each(&:stop!) #TERM

通常であれば、ゆるやかな停止(URS1)を選ぶところですが今回はワーカー自体が動かなくなってしまっているので強制停止(TERM)にします。

Sidekiqの起動

Rubyからシステムコールするにはバッククオートすれば簡単に呼べます。ただこれだけだと正常に起動したかどうかが分からないためプロセス管理ツールmonitを併用することにしました。

`bundle exec sidekiq`

monitでSidekiqを監視

yumなどでmonitをサクッとインストールします。

sudo yum install monit -y
sudo chkconfig monit on
sudo service monit start
sudo vi /etc/monit.d/sidekiq

Sidekiqの起動と停止コマンドを設定します。そして今回はSidekiqを強制停止した時にSlackなどに通知したかったのでif does not existの条件式を使っています。ここで、rakeタスクを指定して通知してあげます。

check process sidekiq with pidfile /var/www/rails/tmp/pids/sidekiq.pid
  every 2 cycles
  start program = "/bin/su - webmaster -c 'cd /var/www/rails/; bundle exec sidekiq -d -C config/sidekiq.yml -e production'"
  stop program = "/bin/su - webmaster -c 'cd /var/www/rails/; bundle exec sidekiqctl stop tmp/pids/sidekiq.pid'"
  if does not exist
    then exec "/bin/su - webmaster -c 'cd /var/www/rails/; RAILS_ENV=production bundle exec rake monitoring_sidekiq:stopped_sidekiq'"
    else if succeeded for 1 cycle then exec "/bin/su - webmaster -c 'cd /var/www/rails/; RAILS_ENV=production bundle exec rake monitoring_sidekiq:start_sidekiq'"

rakeタスクサンプル

30分以上実行中のワーカーがあったら強制終了させるタスクです。これをwheneverなどでcronに設定すればOKです。

namespace :monitoring_sidekiq do
  desc "Sidekiqのジョブが指定時間以上処理中の場合、強制的にSidekiqを終了させる"

  task :check_run_at => :environment do
    threshold_min = 30
    now = Time.now.to_i

    do_stop = false
    data = []

    workers = Sidekiq::Workers.new
    workers.each do |process_id, thread_id, work|
      data << work
      diff = now - work["run_at"]
      if diff >= threshold_min * 60
        do_stop = true
      end
    end

    if do_stop
      report_slack(format_message(data))
      ps = Sidekiq::ProcessSet.new
      ps.each(&:stop!)
    end
  end

  desc "Sidekiqの停止を検知したらSlackへ通知する"
  task :stopped_sidekiq => :environment do
    report_slack("sidekiqの停止を検知しました。再起動をします。")
    `bundle exec sidekiq -d -C config/sidekiq.yml -e #{Rails.env}`
  end

  desc "Sidekiqが起動したらSlackへ通知する"
  task :start_sidekiq => :environment do
    report_slack("sidekiqの再起動が完了しました")
  end

  def format_message(data)
    message = ""
    data.each do |val|
      message += <<-EOM
class: #{val["payload"]["class"]}
jid: #{val["payload"]["jid"]}
args: #{val["payload"]["args"]}
run_at: #{Time.at(val["run_at"])}
---
      EOM
    end
    message
  end

  def report_slack(data)
    Slack.chat_postMessage(
      text: data,
      username: 'slack_bot',
      channel: '#sidekiq'
    )
  end
end

今回の例だとstopped_sidekiqタスクの中でSidekiqを起動していますが、これはmonitでif does not existなどを使うとmonitのstart programが呼ばれず、自前のスクリプトにすべての処理を任せる(?)という仕様のためです。通知方法を標準のメールにするのであればrakeタスク内でsidekiqを起動せずとも全てmonitに任せることができます。

最後に手動でsidekiqのプロセスをkillして再起動がされるかの動作確認をすれば完了です。

22
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
12