SmartHR Tech Blog

SmartHR 開発者ブログ

Sidekiqを拡張するgemが競合することがあるぞ

こんにちは。SmartHRでRails顧問業をしているwillnetです。最近肩こりが酷くて、少しでもこりを軽減するために分割キーボードを使うようになりました。SmartHRのテックブログにエントリを書くのはだいたい二年ぶりになります。最近もうちょっと書く頻度を増やしたい気持ちになっているので頑張っていこうと思います。

今日は最近SmartHR社内で遭遇した、Sidekiqの構成に関するちょっとしたハマりどころを紹介します。Sidekiqは公式が提供しているSidekiq ProSidekiq Enterpriseの他にも、サードパーティのgemが数多くあります。うまく使うことで要件を満たす機能を簡単に追加できて便利ですが、場合によっては既存の機能と競合してしまうことがあります。

Sidekiq公式とサードパーティgemの機能が競合するケース

以前、SmartHR社内のプロジェクトのひとつで sidekiq-limit_fetchを使っているのを見つけました*1。このgemは名前のとおり、特定のキューのフェッチを制限してジョブの同時実行数を一定以下に抑えるものです。同時接続数が限られている外部APIを叩くときや負荷の高いジョブを実行するときなどに便利です。

しかし、sidekiq-limit_fetchを利用するとSidekiq Proのsuper_fetchと競合してしまいます。Sidekiq Proのsuper_fetchは、SidekiqプロセスがOOM Killerなどの要因で突然の死を迎えても実行中のジョブを失わずに再実行できるようになる機能です。Sidekiq Pro以上を採用しているプロジェクトの大半はこの機能が欲しいのでPro(もしくはEnterprise)を利用していると言っても過言ではないでしょう。

Sidekiq Proのsuper_fetch実装について

Sidekiqはジョブの情報をRedisから取得するための機構をFetcherと呼ばれるコンポーネントで管理しており、このFetcherを切り替えることでジョブの取得方法を変更することができます。デフォルト値はここで設定されています。コードを見るとわかるように、デフォルトはSidekiq::BasicFetchが使われます。

Sidekiq Proでconfig.super_fetch!とするとFetcherはSidekiq::BasicFetchからSidekiq::Pro::SuperFetchに切り替わります。このSidekiq::Pro::SuperFetchでジョブの情報を取得する際「実行中のジョブを管理するキュー」にジョブ情報を移動させてからジョブを実行します。この仕組みによってSidekiqプロセスが突然死してもジョブを再実行できるようになっています。Sidekiq Proはコードが公開されていないのでリンクは貼れないのですが、もし会社でSidekiq Pro以上を契約しているのであれば利用しているプロジェクトのディレクトリ上でVISUAL="code --wait" bundle open sidekiq-proなどとしてlib/sidekiq/pro/super_fetch.rbを開くとSuperFetchのコードを確認することができます。 さらにlib/sidekiq-pro.rbを見るとsuper_fetch!メソッド実行時にFetcherがSidekiq::Pro::SuperFetchに切り替えられているのを確認できます。

このように、Sidekiq Proでは専用のFetcherを通じてsuper_fetchを実現しています。

Sidekiq Proとsidekiq-limit_fetchを併用したときの挙動

一方、sidekiq-limit_fetchもSidekiq::LimitFetchという専用のFetcherを持っていて、これにより特定キューの同時実行数を制限しています。Sidekiq::LimitFetchの具体的な振る舞いについては触れないので気になる方はコードを読んでください。

Sidekiq Proとsidekiq-limit_fetchを併用すると次のような挙動になります。

  • Sidekiq ProはRails起動時にconfig.super_fetch!を実行したタイミングでSidekiq::Pro::SuperFetchをFetcherとして設定する
  • その後、Sidekiqがワーカを起動してジョブを処理し始めるタイミングでFetcherをSidekiq::LimitFetchに設定する

Fetcherの設定は後勝ちなので、sidekiq-limit_fetchの機能が有効になりsuper_fetchは無効になります。併用しても特にwarningはありませんしSidekiqプロセスの突然死はそう頻繁には起こらないのでなかなかsuper_fetchが無効になっていることに気づくことはできません。

Sidekiq Proを使って同時実行数の制限をしたいときはどうしたらいいんですか?

ここまでの話でSidekiq Proとsidekiq-limit_fetchは併用できないことがわかりました。ではSidekiq Proのsuper_fetchを利用しつつ同時実行数を制限したい場合はどうしたら良いでしょうか?

例えばSidekiqを1プロセスで利用している場合は、Sidekiq 7.0から導入されたcapsuleを使うと近いことができます。次のように設定したとします。

Sidekiq.configure_server do |config|
  config.queues = %w[critical default low]
  config.concurrency = 5

  config.capsule("unsafe") do |cap|
    cap.concurrency = 1
    cap.queues = %w[unsafe]
  end
end

こうするとcritical、default、lowのキューを5スレッドで、unsafeのキューを1スレッドで、合計6スレッドで動作するようになります。

複数のプロセスで利用している場合は次のようにSidekiq用の設定を複数作り、1プロセスだけsidekiq_unsafe.ymlを使うようにするとよいでしょう。

# sidekiq_default.yml
:concurrency: 5
:queues:
  - critical
  - default
  - low
# sidekiq_unsafe.yml
:concurrency: 1
:queues:
  - unsafe

しかし今紹介したやり方だと、どちらのやり方でもunsafeキューの専用スレッドができてしまいます。これを他のものと共有して使いたい場合は標準の機能で実現するのは難しいです。

まだ試せてはいないのですが、sidekiq-throttledというgemのコードを読むとFetcherを差し替えずにsidekiq-limit_fetcherと似たようなことを実現しているのでこれの利用を検討するのが良いかもしれません。

その他Fetcherを差し替えるgemの例

ざっと調べた限りでは次の2つのgemがFetcherを差し替えています。

sidekiq-rate-limiterはsidekiq-limit_fetcherのようにジョブの取得を制限する目的のgemです。一方sidekiq-power-fetchの方はsuper_fetchと同じようにプロセスの突然死からジョブを守るのを目的として作られているようです。いろいろなgemがありますね。

まとめ

Sidekiq公式とサードパーティのgemの機能が競合するケースについて、内部の実装まで含めて解説しました。sidekiq-limit_fetchのREADMEの表示を変えてsuper_fetchと共存できないことをわかりやすくしたので新規でSidekiq Proと併用を試みる人は減ると思いますが、既存のプロジェクトではすでに併用して使っているところもあるかもしれません。ぜひ一度担当しているRailsアプリケーションのGemfileを確認してみることをおすすめします。

We Are Hiring!

SmartHR では一緒に SmartHR を作りあげていく仲間を募集中です!

Sidekiqが好きな人もそうでない人もお待ちしています。 少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!

hello-world.smarthr.co.jp

*1:今は利用をやめています。