これは何?
Sidekiqには同時実行数のパラメータがあるため、それをどのようにセットするべきか決める必要があります。
これは、Ruby on Rails 5, 6, 7でSidekiq v6, 7のパラメータのチューニングに必要な情報を事前調査してまとめたものです。
結論
- concurrencyを許容するジョブの同時実行数に合わせる
-
config/database.yml
のpoolをconcurrency+1
にしておく - 最終的には要求されるスループットのジョブを回してパフォーマンスを測定して決める
アーキテクチャ概要
キューとスレッドの関係は、Sidekiq v7から導入される Capsules (カプセル)機能によって、やや仕様変更が発生している可能性あり。検証中です。
プロセス:スレッド=1:N
プロセス:キュー=1:N
キュー:スレッド=N:N
ワーカー:キュー=1:1
プロセス:コネクション数=1:N
プロセス:構成ファイル=1:1
コンピューター:プロセス=1:N
キューとスレッドの関係は定義できない。Sidekiq単体では、キューとスレッドはそれぞれプロセスとの紐付けの定義のみ。
つまり、1キューでの並列度は指定できない。プロセス全体の並列度(concurrency)のみ指定可能。
→参考:Sidekiq の Server プロセス内でバックグラウンド処理をするケースの手法を見てみた
※紹介されているSidekiq-limit_fetch
gemがSidekiq v6.1.0に追従できていない、かつメンテも活発でないので代替案を探したほうが良いと思われます。
concurrency
- 公式の説明
- 1つのSidekiqプロセスで使用するスレッド数
- デフォルト値: 5 (v7), 10 (v6以下)
502 13124 775 0 7:10PM ttys008 0:02.79 sidekiq 5.2.1 application-name [0 of 10 busy]
- 推奨は50以下と書いてるが、結局はそれぞれの環境で検証してね。というスタンスっぽい。
- config/database.yml内のpool数をこれと合わせる必要がある
Sidekiqで言うところのスレッド
Sidekiqではジョブ(Woker)ごとに1スレッドを使用しているっぽい。
ソースコードレベルでは未確認だが、ジョブ起動する度にps
結果のbusyの数が増えていったため。
実際の並列処理はCPUコア数に依存。
Connection Pool
Sidekiqで使うコネクションプールはActiveRecordのものと無関係。
ActiveRecord
- ActiveRecordには自身のコネクションプールがある。
- Concurrency and Database Connections in Ruby with ActiveRecord (Heroku)
- ActiveJobでsidekiqを使う場合、connection_poolの値はconcurrency + 1以上にしよう
- Rails 7.1以下でmysqlを使うとデフォルトでこうなっている
ENV.fetch("RAILS_MAX_THREADS") { 5 }
- ソースコード
connection_pool gem
Sidekiq v7から導入される Capsules (カプセル)機能によって、やや仕様変更が発生している可能性あり。検証中です。
- このgemはSidekiqがRedisとのコネクションに使用。
- 自分で制御したい場合は、
config/initilizers/sidekiq.rb
にこう書く - コンストラクタに指定するsizeはプールするコネクション数。
- Sidekiq内で以下のように使用される(ソースコード)
module Sidekiq
class RedisConnection
class << self
def create(options = {})
# ...
ConnectionPool.new(timeout: pool_timeout, size: size) do
build_client(symbolized_options)
end
end
end
end
end
拡張性
拡張可能な変数としては以下のとおり。
- スレッド(concurrency)
- 1プロセスで50スレッド以下にした方がよいとの公式見解のため、これを超えるならばプロセスを増やす(スケールアウト)する
- もちろん、CPUコア数、CPUスレッド数を気にして実効性のある数を決める
- 起点としては、APMツールなどでI/O待ち時間の割合を測定し、アムダールの法則を使って効果が飽和する手前の並列度を見積もるなどの定量的な手法もある
- ワーカー
- 非機能要件からではなく、仕事内容に応じてワーカーを定義していく。1つのワーカーの責務が大きくなったら、ワークフローを再考してワーカーの分割を検討する
- キュー
- 待ち行列となる(待つことになる、FIFOになる)ことを考慮して、キューの分割を考慮する。例えば、頻度が少ないが来たらすぐに処理したいワーカーには専用のキューを用意し、その優先度を上げる
- 平均処理時間の異なる2つのワーカーは別キューに分ける。なぜなら、同じ優先度の異なるキューに入れば、それぞれから1つずつ処理してくれることになり、短時間タスクが長時間タスクによって待たされることが減る。
- なお、優先キューを作ることはそのワーカーの仕事内容によっては許容されないケースもあるかも知れないので注意。例えば、1日の中では順序非依存だが、日付の前後が入れ替わる処理順では困る場合(意外と簡単に既存仕様を破壊しかねないので気を付けたい)
- プロセス
- キューごとの並列度、言い換えればワーカーごとの並列度はSidekiq単体では指定できない。concurrencyを共有するため。よって、別々の並列度を指定したい場合は、別プロセスに分割することも検討する。
- concurrencyが上限に近づいたらプロセスをスケールアウトする
- 最近はコンテナ単価も安いので、concurrency頑張るより水平スケーリングさせるのがいいかも
- Sidekiqコネクションプール数
- concurrencyよりも多く準備する
- Redis側の実効性のあるコネクション数も気にしておく