Sidekiq v6チューニングのためのconcurrencyパラメータ整理

# これは何？
Sidekiqには同時実行数のパラメータがあるため、それをどのようにセットするべきか決める必要があります。
これは、Ruby on Rails 5, 6, 7でSidekiq v6のconcurrencyのチューニングに必要な情報を事前調査してまとめたものです。

Sidekiq v7+向けはこちら => [Sidekiq v7+チューニングのためのconcurrencyパラメータ整理](https://qiita.com/HrsUed/items/94da131f2c32f7657653)


:::note warn
本記事はSidekiq v6を前提としています。v7からは一部デフォルト値が変更され、Capsules（カプセル）機能によっても、やや仕様変更が発生しているため、[公式ドキュメント](https://github.com/sidekiq/sidekiq/wiki)をご確認ください。
:::


# 結論
- `Active Recordのpool値 - 1 = concurrency <= 50`とします
  - `config/database.yml`のpoolを`concurrency + 1`にします
- 最初から水平スケーリング（プロセス数増加）できるアーキテクチャとしておきたいです
- 要求されるスループットのジョブを回してパフォーマンスを測定して決めます。理論的には[アムダールの法則](https://ja.m.wikipedia.org/wiki/アムダールの法則)や[Universal Scalability Law](http://www.perfdynamics.com/Manifesto/USLscalability.html)を使って効果が飽和する手前の並列度を見積もるなどの定量的な手法もあります。

# アーキテクチャ概要
![アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249168/47611232-a3ae-f263-26ea-55c8b2fe92ce.png)

プロセス：スレッド＝1:N
プロセス：キュー＝1:N
キュー：スレッド＝N:N
ワーカー：キュー＝1:1
プロセス：コネクション数＝1:N
プロセス：構成ファイル＝1:1
コンピューター：プロセス＝1:N

キューとスレッドの関係は定義できない。Sidekiq単体では、キューとスレッドはそれぞれプロセスとの紐付けの定義のみ。
つまり、1キューでの並列度は指定できない。プロセス全体の並列度（concurrency）のみ指定可能。
もう少し柔軟な設定とするには、[sidekiq-throttled](https://github.com/ixti/sidekiq-throttled)の利用を検討ください。

# concurrency
- [公式の説明](https://github.com/mperham/sidekiq/wiki/Advanced-Options#concurrency)
- 1つのSidekiqプロセスで使用するスレッド数
- デフォルト値: 10 (v6以下)
  - [v6ソースコード](https://github.com/mperham/sidekiq/blob/v6.1.3/lib/sidekiq.rb#L21)
  - `sidekiq -c N`で実行時に値を指定することも可能
  - 以下の`0 of 10 busy`の10はconcurrencyの値

```console:ps出力結果
502 13124   775   0  7:10PM ttys008    0:02.79 sidekiq 5.2.1 application-name [0 of 10 busy]
```
- [推奨は50以下と書いてる](https://github.com/mperham/sidekiq/wiki/Advanced-Options#concurrency)が、結局はそれぞれの環境で検証してね。というスタンスっぽい。
  - [Github/issue: Is concurrency > 50 stable?](https://github.com/mperham/sidekiq/issues/3211)
  - [StackOverflow: sidekiq - Is concurrency > 50 stable?](https://stackoverflow.com/questions/40314588/sidekiq-is-concurrency-50-stable)
- 各SidekiqスレッドでDB接続することを想定して、Active Recordコネクションプールサイズ（config/database.yml内のpoolの値）とセットでチューニングが必要です。、「[ActiveJobでsidekiqを使う場合、connection_poolの値はconcurrency + 1以上にしよう](https://repl.info/archives/659/)」によれば、`conccurency >= Active Recordコネクションプールサイズ + 1`としておいた方が安心です。

# スレッド
Sidekiqではジョブ処理ごとに1スレッドを使用している。
ソースコードレベルでは未確認だが、ジョブ起動する度に`ps`結果のbusyの数が増えていったため。
実際の並列処理はCPUスペックに依存します。

# コネクションプール
Sidekiqの文脈で言うコネクションプールはRedisコネクションプールのことで、Active Recordのものではありません。
ただし、Sidekiqプロセス内でActive Recordを利用すると、SidekiqプロセスからDB接続するためのActive Recordコネクションプールを利用します。なので、Sidekiqのconcurrencyとそれらのプールサイズは密接な関係があります。

## Redisコネクションプールと[connection_pool gem](https://github.com/mperham/connection_pool)
- このgemはSidekiqがRedisのコネクションプーリングするために使用する。
- 自分で制御したい場合は、`config/initilizers/sidekiq.rb`に[こう](https://github.com/mperham/connection_pool/blob/v2.4.1/lib/connection_pool.rb#L37)書く
- コンストラクタに指定するsizeはプールするコネクション数。
  - [ソースコード](https://github.com/mperham/connection_pool/blob/v2.4.1/lib/connection_pool.rb#L37)
  - connection_pool gem v2.4.1におけるデフォルトは5。
  - Sidekiqにおけるデフォルトは、serverなら`concurrency + 5`。clientなら`ENV['RAILS_MAX_THREADS']`。
     - [ソースコード](https://github.com/mperham/sidekiq/blob/v6.1.3/lib/sidekiq/redis_connection.rb#L17-L27)
     - serverだと何で+5なのかというと、[ここに記載のとおり](https://github.com/mperham/sidekiq/blob/v6.1.3/lib/sidekiq/redis_connection.rb#L43-L48)ですが、通常版（=OSS版、Pro/Enterprise以外）だと+2で最低限動くらしい。
     - 実際、[この部分のバリデーションコード](https://github.com/sidekiq/sidekiq/blob/49b5d3351f95b21d8b6cf25e18bf94cd7dd8622a/lib/sidekiq/cli.rb#L80-L82)のとおり、指定する並列度+2以上が必要な実装になっています。
- Sidekiq内で以下のように使用される（[ソースコード](https://github.com/mperham/sidekiq/blob/v6.1.3/lib/sidekiq/redis_connection.rb#L34)）

```ruby:sidekiq/lib/sidekiq/redis_connection.rb
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
```

## Active Recordコネクションプール
- [Active Recordには自身のコネクションプールがある](https://railsguides.jp/configuring.html#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E6%8E%A5%E7%B6%9A%E3%82%92%E3%83%97%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)。
- [Concurrency and Database Connections in Ruby with ActiveRecord (Heroku)](https://devcenter.heroku.com/articles/concurrency-and-database-connections)
- [ActiveJobでsidekiqを使う場合、connection_poolの値はconcurrency + 1以上にしよう](https://repl.info/archives/659/)
- Rails 7.1以下でmysqlを使うとデフォルトでこうなっている
  - `ENV.fetch("RAILS_MAX_THREADS") { 5 }`
  - [ソースコード](https://github.com/rails/rails/blob/7-1-stable/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt#L15)

# 拡張性
拡張可能な変数としては以下のとおり。

- スレッド（concurrency）
- ワーカー
- キュー
- プロセス
- Redisコネクションプールサイズ

## スレッド（concurrency）
  - 1プロセスで50スレッド以下にした方がよいとの公式見解のため、少し余裕をもった値にしつつ、50に近づいたらプロセスを増やします（スケールアウトします）
  - 個人的には50も多過ぎるので、ビタビタに攻めるよりは最初から水平スケーリングできるようにしたいです
  - もちろん、CPUスペックやDB負荷を気にして効率の良い数を決めます
  - 起点としては、APMツールなどでI/O待ち時間の割合を測定し[アムダールの法則](https://ja.m.wikipedia.org/wiki/アムダールの法則)を適用したり、異なる並列度での実験値を[Universal Scalability Law](http://www.perfdynamics.com/Manifesto/USLscalability.html)を使ってフィッティングするなどして、効果が飽和する手前の並列度を見積もるなどの定量的な手法もあります
## ワーカー
  - まずは非機能要件からではなく、仕事内容に応じてワーカーを定義していきます。1つのワーカーの責務が大きくなったら、処理ワークフローを再考してワーカーの分割を検討します。
  - 仕事内容は、引数で受け取るリソース、リトライ範囲、順序や一貫性を保証したい範囲など、非機能要件側からも境界を決めていきます。
  - 非同期なワーカーが増える場合、ワーカー同士の依存関係を主としてシステムの複雑度が上がってないかチェックしたいです。また、業務自体が複雑化しているシグナルと捉え、業務見直しのチェックポイントを最初から考慮しておくと拡張しやすくなります。
## キュー
  - 待ち行列となる（待つことになる、FIFOになる）ことを考慮して、キューの分割を考慮します。例えば、頻度が少ないが来たらすぐに処理したいワーカーには専用のキューを用意し、その優先度を上げるようにします。
  - 平均処理時間の異なる2つのワーカーは別キューに分けます。なぜなら、同じ優先度の異なるキューに入れば、それぞれから1つずつ処理してくれることになり、短時間タスクが長時間タスクによって待たされることが減るからです。
  - なお、優先キューを作ることはそのワーカーの仕事内容によっては許容されないケースもあるかも知れないので注意です。例えば、1日の中では順序非依存でも、日付の前後が入れ替わる処理順では困る場合です。意外と簡単に既存仕様を破壊しかねないので気を付けたいです。
## プロセス
  - キューごとの並列度、言い換えればワーカーごとの並列度はSidekiq単体では指定できません。concurrencyを共有するためです。よって、キューごとに別々の並列度を指定したい場合は、別プロセスに分割するか、[sidekiq-throttled](https://github.com/ixti/sidekiq-throttled)を導入することも検討します。
  - concurrencyが上限に近づくか、レイテンシーの頭打ちとなったらプロセスをスケールアウトします。
  - 最近はコンテナ単価も安いので、concurrency頑張るより水平スケーリングさせるのがいいです。
  - プロセスを増やすとDBコネクション含めて外部リソース利用も増えるため、限界値を見積もって将来に備えます。
## Redisコネクションプールサイズ
  - 特別チューニングする必要はありません。
  - concurrencyよりも多く準備する必要があるが、デフォルトでconcurrencyと同数のコネクションが確保されるようになっています。

# 参考
- [Sidekiq Wiki](https://github.com/mperham/sidekiq/wiki)
- [Github/issue: Is concurrency > 50 stable?](https://github.com/mperham/sidekiq/issues/3211)
-  [Concurrency and Database Connections in Ruby with ActiveRecord (Heroku)](https://devcenter.heroku.com/articles/concurrency-and-database-connections)
-  [Railsガイド](https://railsguides.jp/configuring.html#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E6%8E%A5%E7%B6%9A%E3%82%92%E3%83%97%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)
- [ActiveRecordのコネクションプールの理解を深める](https://qiita.com/katsuyuki/items/42b3c69bcd76c44ad64a)
- [How to Quantify Scalability - The Universal Scalability Law (USL)](http://www.perfdynamics.com/Manifesto/USLscalability.html)