Mastodon のジョブキューを Sidekiq から SolidQueue に置き換える

これは Ruby on Rails - Qiita Advent Calendar 2024 - Qiita の17日目の記事です。

Mastodon のジョブキューを Sidekiq から SolidQueue に(部分的に)置き換えている話をします。

Mastodon とは

github.com

Mastodon は、ActivityPub をベースにした、Ruby on Rails 製のオープンソースの分散型SNSソフトウェアです。 誰でも自由に自分のサーバーを運用することができ、他のサーバーと相互にやり取りすることができます。

ActivityPub とジョブキュー

ActivityPub の特性上、Mastodon は大量の非同期ジョブを持っています。

  • 投稿を作成したとき、それをフォロワーのサーバーに配送するジョブ
  • 投稿を Like したとき、それを投稿元のサーバーに配送するジョブ
  • 他のサーバーから投稿が配送されたとき、それを RDBMS や Redis などに保存するジョブ
  • 他のサーバーから Like が配送されたとき、それを RDBMS に保存するジョブ

また、SNS というサービスの特性上からも、様々なジョブが必要となります。

  • URL を投稿したとき、あるいは URL を含む投稿が他のサーバーから配送されたとき、OGP を取得して RDBMS にキャッシュを保存するジョブ
  • 様々なイベントに応じてユーザーにプッシュ通知を送信するジョブ

その他にも、サーバーの負荷を一定に保ちつつユーザーに高速な応答をするために、様々な処理が非同期ジョブ化されています。

レポジトリの中にあるジョブを数えてみたところ、78個ほどのジョブが定義されているようです。

$ ls app/workers/**/*_worker.rb | wc -l
      78

Mastodon と Sidekiq、およびその問題点

上にあげた非同期ジョブは、全て Sidekiq によって実現されています。

github.com

Sidekiq は長い歴史のある非同期ジョブのミドルウェアであり、多くのRuby on Rails のサービスで利用されています。 無料で利用することもできますが、商用サービスにおいては Pro 版あるいは Enterprise 版を購入することが推奨されています。

sidekiq.org

無料版の Sidekiq の欠点の一つは、信頼性に欠けるところです。 より具体的には、ジョブの実行中にエラーが発生してジョブがクラッシュした場合、そのジョブは実行されずに虚空へ消え去ります。

この問題は Pro 版を購入することで回避できますが*1、そのためには $995/year の費用を支払う必要があります。 OSSである Mastodon では当然ながら無料版の Sidekiq を利用しているため、ジョブの信頼性は低いです。

実際、以下のような issue が起票されています。

Sidekiq Worker Crashing will cause it to forget all its in-progress jobs · Issue #32600 · mastodon/mastodon · GitHub

この issue では、Sidekiq の代替として、GoodJobSolidQueue といった ActiveJob backend が提案されていますが、影響が非常に大きいことやパフォーマンスへの懸念から、採用には至っていないようです。

self-hosted mastodon に SolidQueue を導入してみる

Sidekiq の代替として提案されている SolidQueue については Kaigi on Rails 2024 でも言及があった通り、今後の Rails のジョブキューのデファクトスタンダードの座を Sidekiq と争っていく可能性があります。

Sidekiq vs Solid Queue | Kaigi on Rails 2024

ちょうど仕事の方でも SolidQueue の採用を検討しており、また僕が運用している self-hosted mastodon ( https://social.genya0407.link )程度の負荷であればまず問題なく捌けるであろうというところから、実際に SolidQueue を試してみることにしました。

導入方法の検討

前述の通り、mastodon には 78 個ものジョブが定義されており、その全てを一度に SolidQueue に移行するのは労力的に厳しいですし、問題発生時のリスクも高いです。 そのため、Sidekiq と SolidQueue を共存させつつ、以下のような順序で SolidQueue を導入することにしました。

  • まず LinkCrawlWorker を SolidQueue に移行して、問題を洗い出す
    • LinkCrawlWorker は OGP のデータを取得するジョブで、これが全く動かなくなったとしても問題は少ない
  • その後 ProcessingWorkerDistributionWorker といった負荷の高いジョブを SolidQueue に移行する
    • この辺りのエンキュー件数の多いジョブを一通り移行すれば、主なジョブを SolidQueue に移行したと言えるだろう

SolidQueue の素振りと検証が目的であることから、全てのジョブを移行することは目指しませんでした。

パッチの内容

最終的に、SolidQueue 移行のパッチは以下のようなものになりました。 なお、これは差分を見やすくまとめたものであり、実際に動かしているコードとは異なります。

Tmp/solid queue diff by genya0407 · Pull Request #58 · genya0407/mastodon · GitHub

ポイントとしては以下のようなところです。

config/recurring.yml で色々する

mastodon/config/recurring.yml at 0cd8b5708a0bb2156bf7c26936a56f6683a3ee49 · genya0407/mastodon · GitHub

SolidQueue には recurring という仕組みがあり、定期的に実行すべきジョブを定義することができます。 ここには以下の二つの処理を仕込んでいます。

いずれも、SolidQueue の足りない機能を補うという感じの設定です。

spec/rails_helper.rb にヘルパーを仕込む

Gradual Shift from Sidekiq to SolidQueue by genya0407 · Pull Request #58 · genya0407/mastodon · GitHub

Sidekiq には Sidekiq::Testing.inline! という機能があって、これを使うとテストにおいて非同期ジョブが同期的に動くようになります。 SolidQueue や ActiveJob にはこれ相当の機能がないようだったので、雑に自作しました。

これにより、ジョブが同期的に動くことを前提としているテストケースがパスするようになります。

config/queue.yml のチューニング

mastodon/config/queue.yml at 0cd8b5708a0bb2156bf7c26936a56f6683a3ee49 · genya0407/mastodon · GitHub

config/queue.yml は SolidQueue の設定を司るファイルですが、ここでワーカースレッドの数を制御することができます。

紆余曲折を経て、mastodon において SolidQueue を利用するとき、ワーカースレッドの数は MAX_THREADS - 2 とするべきということがわかりました。

これは以下の理由によります:

この点については SolidQueue に PR を送り、README.md に注意点を追記してもらいました。

github.com

MAX_THREADS に設定する値は、各々のサーバーの負荷に応じて決定すれば良いでしょう。 今のところ、僕のサーバーでは MAX_THREADS=10 でかなり余裕を持ちつつジョブを捌けているようです。

around_perform で諸々の後始末をする

ApplicationJob には以下のような処理を仕込んでおく必要があります。

mastodon/app/jobs/application_job.rb at 0cd8b5708a0bb2156bf7c26936a56f6683a3ee49 · genya0407/mastodon · GitHub

これがないと、Redis のコネクションプールが速攻で枯渇してしまうということが実験により明らかになっています。 (コネクションの返却をこのミドルウェアに依存しているジョブがあるため)

その他所感など

  • SolidQueue 普通に使える
    • 共存させる方法についても README.md に記載があり、Sidekiq など他のジョブキューからの移行を促進したい意向がありそうに感じました
    • お仕事 Rails プロダクトでも、あまり重要でないジョブから徐々に SolidQueue に移していく、という戦略が取れそうです
  • SolidQueue まだちょっと足りない
    • 利用者側で recurring.yml に少し処理を追加してあげないと不要なデータが消えてくれない、ダッシュボードがデフォルトで入らないなど、Sidekiq に比べると痒いところに手が届ききってないなと思うシーンがたまにありました
    • 今の段階でも十分実用的だと思いますが、もう少し待ってみるとさらに良い体験が得られるようになるかもしれません
  • PostgreSQL のメモリ使用量が顕著に増えた
    • PostgreSQL はコネクションの数だけプロセスを fork するという設計*2なので、SolidQueue が建立するスレッドの数だけ追加で fork するはず。その影響でメモリ使用量が増えているのかもしれない。
    • PostgreSQL 側の設定を見直せばどうにかなるかもしれないが...
  • OpenTelemetry は便利
    • OpenTelemetry でトレースを吐いて Grafana Cloud で検索・可視化できるようにしているのだが、ジョブキュー移行においてかなり便利だった。
    • 特別なログを仕込んだりしなくても、ジョブキューの処理状況を簡易的に確認できるダッシュボードをサクッと作れたのは結構感動的だった

まとめ

僕が運用している mastodon のサーバーにおいて、各種ジョブキューを SolidQueue に置き換える試みを行いました。

いくつかのハマりどころがありましたが都度解決し、現在では安定して SolidQueue を運用できています。

既存の Rails アプリに少しづつ導入していくということもできるので、皆さんも試してみてはいかがでしょうか?