はじめに
Sidekiqはバックグラウンドジョブ処理を制御するためのライブラリです。
Railsで重たい処理をバッチなどでキューに追加して、別のスレッドで実行するというのがSidekiqを用いると簡単にできます。
今の仕事でこれを使っており、調べて試してみたのでそれを記事にしました。
ここでは、ポケモンデータベースで有名なpokedexを題材にして、Sidekiqの必要性、ワークフロー、実装の仕方をざっくりやってみます。
つくってみるもの
ここに807匹のポケモンの情報を列挙したcsvファイルがあります。
これを元に、807個のPokemonクラスのインスタンスを生成して、保存して、テーブルで表示するWebアプリケーションを作るとします。
その場合、普通に書いたらpokemons_controllerは次のようになると思います。
class PokemonsController < ApplicationController
require 'csv'
def index
@pokemons = Pokemon.all
end
def upload
csv_path = File.join Rails.root, 'db', 'pokemon.csv'
CSV.foreach((csv_path), headers: true) do |pokemon|
Pokemon.create(
species_id: pokemon[2],
name: pokemon[1],
height: pokemon[3],
weight: pokemon[4])
end
flash[:notice] = "All 807 Pokemons added to db."
redirect_to pokemons_path
end
def destroy_all
Pokemon.destroy_all
flash[:notice] = "All 807 Pokemons deleted from db."
redirect_to pokemons_path
end
end
このコードは正常に動作しますが、例えばuploadが呼び出されると、同期的に処理されるため、807個のモデルオブジェクトの保存が終わるまでブラウザが読み込み中のまま固まってしまします。
さらにデータが増えれば、サーバの環境によっては408 Request Timeout
が返されることもあるため、解消すべき問題です。
そこで、Sidekiqによるマルチスレッド処理を実装します。
メインのWebアプリケーションのスレッドで時間のかかるPokemonインスタンスの生成や削除、保存をするのではなく、Sidekiqにこれらを任せることができます。
処理の流れは次の図のようになります。
モデルオブジェクトの保存が全て完了してから結果を画面に表示するのではなく、Redisにこのタスクをキューイングします。
Webアプリケーションは、タスクが実行中であることを、フラッシュメッセージでユーザーに伝えることができます。
その裏ではSidekiqによる処理が実行されています。
チュートリアル
Setup
SidekiqはキューをRedisに書き込むことでジョブ管理します。
Redisがインストールされていない場合は、インストールしてください。
$ brew install redis
同期的な処理のアプリケーション
まずは同期的な処理のPokemonアプリケーションを普通に作ってみます。
rails generateでサクッと実装します。
$ rails new sidekiq_pokemon
$ cd sidekiq_pokemon
$ rails g model Pokemon name:string species_id:integer height:integer weight:integer
$ bundle exec rake db:migrate
$ rails g controller pokemons index upload destroy_all --skip-template-engine # viewファイルの生成をスキップするオプション
$ mkdir app/views/pokemons
$ touch app/views/pokemons/index.html.erb
$ wget -O db/pokemon.csv https://raw.githubusercontent.com/veekun/pokedex/master/pokedex/data/csv/pokemon.csv # ポケモンのcsvをダウンロード
次に、routes.rb
を以下のように編集します。
Rails.application.routes.draw do
resources :pokemon, only: [:index]
post 'pokemons/upload', to: 'pokemons#upload'
post 'pokemons/destroy_all', to: 'pokemons#destroy_all'
end
pokemons_controller.rb
とviews/pokemons/index.html.erb
も以下のように編集します。
class PokemonsController < ApplicationController
require 'csv'
def index
@pokemons = Pokemon.all
end
def upload
csv_path = File.join Rails.root, 'db', 'pokemon.csv'
CSV.foreach((csv_path), headers: true) do |pokemon|
Pokemon.create(
species_id: pokemon[2],
name: pokemon[1],
height: pokemon[3],
weight: pokemon[4])
end
flash[:notice] = "All 807 Pokemons added to db."
redirect_to pokemons_path
end
def destroy_all
Pokemon.destroy_all
flash[:notice] = "All 807 Pokemons deleted from db."
redirect_to pokemons_path
end
end
<h1>Pokemons</h1>
<% if flash[:notice] %>
<div class="notice"><%= flash[:notice] %></div>
<% end %>
<%#= link_to "See sidekiq stats", sidekiq_web_path %>
<%= form_tag pokemons_upload_path do %>
<%= submit_tag "Import Pokemons" %>
<% end %>
<%= form_tag pokemons_destroy_all_path do %>
<%= submit_tag "Delete Pokemons" %>
<% end %>
<table>
<tr>
<th>ID</th>
<th>Pokemon</th>
<th>Height</th>
<th>Weight</th>
</tr>
<% @pokemons.each do |pokemon| %>
<tr>
<td><%=pokemon.species_id%></td>
<td><%=pokemon.name%></td>
<td><%=pokemon.height%></td>
<td><%=pokemon.weight%></td>
</tr>
<%end%>
</table>
準備が整ったのでrails s
でサーバを起動し、http://localhost:3000/pokemons にアクセスします。
Import Pokemons
ボタンを押すと、807個のインスタンスを生成して保存するuploadアクションが開始され、最後にリダイレクトされて結果が表示されます。
Delete Pokemons
はその逆です。
普通の実装ではどれくらいの時間を要するのか、実際にやってみます。
upload
destroy_all
localhostでサーバを起動しているにもかかわらず結構かかっています。
herokuなどのサーバにデプロイした場合、これよりかなり時間がかかることが予想できます。
Sidekiqによる非同期処理の実装
これまでのコードを、Sidekiqによるマルチスレッド処理に対応させるためにリファクタリングします。
Gemfileにgem 'sidekiq'
を追記して、bundle install
してください。
Sidekiqの処理を各workerファイルも作成します。
$ rails g sidekiq:worker PokemonAdd # workerファイルとそのテストファイルが生成される
$ rails g sidekiq:worker PokemonRemove # 一度に複数は作れないっぽい?
pokemon_add_worker.rb
とpokemon_remove_worker.rb
は次のように実装します。
class PokemonAddWorker
include Sidekiq::Worker
sidekiq_options retry: false
require 'csv'
def perform(csv_path)
CSV.foreach((csv_path), headers: true) do |pokemon|
Pokemon.create(
species_id: pokemon[2],
name: pokemon[1],
height: pokemon[3],
weight: pokemon[4]
)
end
end
end
class PokemonRemoveWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform
Pokemon.destroy_all
end
end
このように、Sidekiqのクラスを宣言するには、Sidekiq::Worker
をincludeします。
また、sidekiq_options
を特に指定しなければ、Sidekiqは処理に失敗した場合、そのメソッドを再試行しますが、例えば決済処理のような再試行してほしくないケースでは、上記のようにretry: false
とすることでこれを防ぐことができます。
そして、外出ししたい処理はperform
メソッドに記述します。
ボトルネックになるからメインのWebアプリケーションのスレッドで処理してほしくない部分は、ここに記述してSidekiqに委譲できます。
これで主なロジックは実装できたので。pokemons_controller.rb
から次のように呼び出せるようリファクタリングします。
class PokemonsController < ApplicationController
def index
@pokemons = Pokemon.all
end
def upload
csv_path = File.join Rails.root, 'db', 'pokemon.csv'
PokemonAddWorker.perform_async(csv_path)
flash[:notice] = "Pokemons getting added to db"
redirect_to pokemons_path
end
def destroy_all
PokemonRemoveWorker.perform_async
flash[:notice] = "Pokemons getting removed from db"
redirect_to pokemons_path
end
end
workerメソッドは。上記のようにWorkerClassName.perform_async(args)
で呼び出すことができます。
これですべての準備ができたので、ローカルで実行してみます。
以下のコマンドでredis-serverとsidekiqを起動してください。
$ redis-server /usr/local/etc/redis.conf
$ bunde exec sidekiq
rails serverを起動して、実際にバックグラウンドジョブとしてインスタンス生成/削除が生成できているかを確認してみます。
(GIFにカーソルを表示するようにするの忘れちゃった...)
upload
destroy_all
Sidekiqの実装前と比べて、リダイレクトまでの時間が非常に速いです。
どちらも、モデルオブジェクトの保存が完了する前にリダイレクトされていることがわかると思います。
リダイレクトとモデルオブジェクト保存の処理が別のスレッドで実行されており、ボトルネックだったジョブはSidekiqによりバックグラウンドで処理されています。
おわり
以上、Sidekiqの使い方の簡易的な説明でした。
もっと詳しく全体的な情報を知りたいという方は、こちらの記事が参考になると思います。
例えば、Sidekiqは強力なタスク監視のダッシュボードがビルトインされており、それの設定の仕方などが丁寧に書かれています。