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して再起動がされるかの動作確認をすれば完了です。