sidekiqとは、Rubyで非同期処理を行うためのライブラリです。
sidekiqを安全に再起動する構成の一例をここで紹介します。
課題
sidekiqで実行しているワーカーの更新には、sidekiqの再起動が必要になります。
capistrano等でデプロイを自動化している場合、sidekiqの再起動も同時に行いたいです。
しかし、それには問題があります。
sidekiqで実行するようなワーカーは、数十分、数時間かかるものもあるかもしれません。そのようなジョブが実行中にデプロイされることで再起動がかかり、また一から実行する、なんてことになってしまいます。
ですので、ワーカーが終了したかをチェックし、終了を確認してから再起動する必要があります。
capistrano-sidekiqでは、上記の課題を解決できませんでした。
なので独自で再起動の仕組みを実装します。
環境
ubuntu 18.04.2 LTS
rails 5.2.1
sidekiq 5.2.2 (ActiveJobと連携)
supervisord 3.3.1
ruby 2.5.1 (rbenvでインストール)
capistrano 3.11.0
目標
capistranoでデプロイしたときに、ワーカーが全て終了しているsidekiqプロセスはすぐに再起動、ワーカーが実行中のsidekiqプロセスはワーカーが全て終了したら再起動するようにする。
railsサーバは複数ホストの場合も想定。
supervisor
sidekiqのプロセスをsupervisorで管理します。
設定ファイルの例です。
# 通常のワーカー
[program:normal]
directory=(railsのrootディレクトリ)
environment=RAILS_ENV=production
command=/home/ubuntu/.rbenv/shims/bundle exec sidekiq --config config/sidekiq/process/normal.yml
process_name=normal
numprocs=1
autostart=true
user=ubuntu
redirect_stderr=true
stdout_logfile=/var/log/supervisor/sidekiq_normal.log
# sidekiqを再起動するためのワーカー
[program:restart]
directory=(railsのrootディレクトリ)
environment=RAILS_ENV=production
command=/home/ubuntu/.rbenv/shims/bundle exec sidekiq --config config/sidekiq/process/restart.yml
process_name=restart
numprocs=1
autostart=true
user=ubuntu
redirect_stderr=true
stdout_logfile=/var/log/supervisor/sidekiq_restart.log
sidekiqを再起動するためのワーカーを用意するのがポイントです。
sidekiqの設定
railsのsidekiq設定の例です。
:tag: normal
:verbose: true
:concurrency: 5
:queues:
- user
- purchase
- default
:tag: restart
:verbose: true
:concurrency: 1
:queues:
- <%= `hostname`.strip %>
:max_retries: 0
queuesにhostnameを設定するのがポイントです。
複数ホストの場合は、ワーカーがどのhostで実行されるかは、sidekiqが勝手に決めてしまいます。
デプロイしたホストで必ずsidekiq再起動ワーカーを実行してほしいので(supervisorで再起動するため)、このようなテクニックを使っています。
この方法は公式で紹介されています。
sidekiq再起動ワーカー
ワーカーを再起動させるワーカーを用意します。
class RestartSidekiqProcessWorker < ActiveJob::Base
def perform
# 新規キューの実行を受付けないようにする
target_processes.each(&:quiet!)
sleep 10
target_ids = target_processes.map {|ps| ps['identity']}
# 実行中ワーカーが完了するまで監視して安全に終了させる
while true
# 全てのワーカーが終了かつすでに再起動済みでないプロセスは再起動する
target_processes.select{|ps| ps['busy'] == 0 && target_ids.include?(ps['identity'])}.each do |ps|
restart(ps)
target_ids.delete(ps['identity'])
end
break if target_ids.count == 0
sleep 10
end
end
private
def target_processes
# 自分自身のプロセスは無視
target = Sidekiq::ProcessSet.new.to_a.delete_if{|ps| ps['tag'] == 'restart'}
# 自分のhost以外は無視
hostname = `hostname`.strip
target.delete_if{|ps| ps['hostname'] != hostname}
end
def restart(ps)
# supervisorで再起動
ps_name = ps['tag']
`sudo supervisorctl restart sidekiq:#{ps_name}`
end
end
ワーカーを実行するスクリプトを用意します。
queueにhostnameを指定することで、必ずこのスクリプトを実行したホストでワーカーが実行されます。
RestartSidekiqProcessWorker.set(queue: `hostname`.strip).perform_later
capistranoの設定
taskを用意し、deploy後に上記ワーカーを実行するように設定します。
namespace :sidekiq do
task :restart do
on roles(:app) do
with rails_env: fetch(:rails_env) do
within current_path do
execute :bundle, :exec, :rails, :runner, "script/sidekiq/restart.rb"
end
end
end
end
before 'deploy:published', 'sidekiq:restart'
end
以上の構成で、capistranoでデプロイした時に、自動で安全に再起動されます。
なお、sidekiq再起動ワーカーのプロセスは再起動されません。このワーカーに変更があった場合は、手動で再起動する必要があります。
ワーカーを再起動するワーカーを用意する、というややこしい構成になってしまいました。
もっとシンプルな方法があればいいのですが…。
参考
https://github.com/mperham/sidekiq/wiki/FAQ#how-do-i-ensure-a-job-processes-on-a-given-machine
http://n8.hatenablog.com/entry/2015/05/01/100424