Railsでpumaやsidekiqのスレッド数とコネクションプールの数ってどうやって決めるんですか | Dev Driven 開発・デザインチーム Railsでpumaやsidekiqのスレッド数とコネクションプールの数ってどうやって決めるんですか | 働くひとと組織の健康を創る iCARE

BLOG

Railsでpumaやsidekiqのスレッド数とコネクションプールの数ってどうやって決めるんですか

2022/12/24

この記事はiCARE Dev Advent Calendar 2022 第1レーン24日目の記事です。

Railsの基本原則の一つに「メニューはおまかせ」があり、デフォルトで設定を良い感じにしてくれています。しかし、本当に自分のユースケースでも問題ない設定だと自信を持って言うためには、なぜこの設定になっているのかの背景知識が必要になります。例えばrails newをするとpumaのスレッド数はデフォルト5、データベースのコネクションプール数も5になっています。これは自分のユースケースで適切な値なのでしょうか?どういうときにいくつに設定するのが正しいのでしょうか?

pumaのスレッド数をどうやって決めるのか

pumaはRailsのデフォルトのアプリケーションサーバであり、複数プロセス、複数スレッドで動くアプリケーションサーバです。この記事を執筆している時点で最も利用率の高いアプリケーションサーバでもあります。

先程も書いたように、デフォルトのpumaは5スレッドで動くようになっています。5スレッドで動くということは、CPUのコア数や他に動いているスレッドにもよりますが、最大5並列で動く事ができるということになります。しかし、MRI(CRuby)にはGIL(global interpreter lock)があり、複数スレッドを動かしていても同時に動けるのは1スレッドです。

ただしIO待ちのときにはロックを開放するので別のスレッドが動くことができます。IO待ちとは、例えばDBアクセス、別のサービスのHTTP APIを叩く、ログファイルに書き込みを行うなどをしている最中に生じる待ち時間を指します。

つまりMRIにおける複数スレッド環境で全体のうちどれくらい並列に実行できるかの割合は、どれくらいIO待ちが生じているかに依存することになります。例えばpumaがリクエストを処理する時間のうち40%の時間をIO待ちで使っているとしたら、40%が並列実行可能ということになります。

40%IO待ちをする、といっても実際にはIO待ちが多い時間帯や少ない時間帯があるはずです。であればたくさんIO待ちをするタイミングに合わせて予めスレッドをたくさん作っておこうと思うかもしれませんが、それは現実的ではありません。スレッドを作るほどpumaのメモリ消費量は増える傾向にあるからです。

つまりスレッドを複数作ると、IO待ちが多ければ並列にリクエストを処理できて効率的だけどその分メモリ消費量も多くなる、ということになります。ということは並列に処理できる度合い(IO待ちの比率)とメモリの消費量を見てコスパの良いところを選ぶのが良いですね。

ここでアムダールの法則という法則を紹介します。以下wikipediaからの引用。

アムダールの法則(アムダールのほうそく、英語: Amdahl's law)は、ある計算機システムとその対象とする計算についてのモデルにおいて、その計算機の並列度を上げた場合に、並列化できない部分の存在、特にその割合が「ボトルネック」となることを示した法則である。

例えば仮にシステムのうち並列化できる箇所が50%しかないとすると、50%は普通に直列で実行せざるを得ないので、並列数を無限に増やせたとしても速度は最大2倍にしかならないですよね。システム中の並列化できる度合いによって、並列数(x軸)と速度向上率(y軸)の関係が変わることをグラフにしたものをwikipediaから引用します。

英語版ウィキペディアDaniels220さん, CC 表示-継承 3.0, リンクによる

これをRailsのアプリケーションに置き換えると、たとえば75%I/O待ちしているとすると、アムダールの法則的には8コア以上のCPUを使っていたら8スレッドで約3倍速くなるけど、それ以上スレッド増やしてもほとんど速くならない、という事になります。

次に気になるのは、アプリケーションがIO待ちしてる割合ってどうやって調べるのか、ということですよね。これはNewRelicなりDatadogなり、お使いのAPMサービスで測定することができます。例えばDatadogは全リクエスト中のDBが占める時間を"% of Time Spent"グラフで見ることができるため、それを見るとよいでしょう。

大抵のアプリケーションサーバのIOは50%未満であるので、5スレッドは悪くない設定なように思えます。sidekiqなどのバックグラウンドワーカーはアプリケーションサーバよりはIO比率が高いので、5スレッド以上にする余地がありそうです。

コネクションプール数はプロセスで利用するスレッド数と同じにする

Railsでは、DB接続はコネクションプール経由で行います。これはクエリを投げるたびに毎回DBに接続して処理が終わったら切断するより、一度接続したコネクションを使いまわすほうが効率が良いためです。

コネクションは1スレッドごとに1つ割り当てられます。コネクションプールの数はデフォルトだと環境変数RAILS_MAX_THREADS、もしくは5になっています。これはpumaのスレッド数の設定と共通です。なので、大抵のケースではデフォルトのままにしておけば大丈夫です。プールが確保しているコネクションより多くのコネクションを要求してエラーになることはありません。ただし、pumaのスレッド数を増やすときはコネクションプールの数も合わせて増やす必要があります。

Railsでスレッドを利用しているのがpumaだけならこれで終わりなのですが、puma以外でスレッドを使うものがあったらそれも考慮に入れる必要があります。もしアプリケーションコード中で手動でスレッドを生成して、その中でDB接続するような箇所があったらその分コネクションプールの数を足しましょう。

sidekiqを利用していたら、sidekiqのconcurrencyの設定を見ましょう。これが6以上ならDBのpool数もそれに合わせて増やさないと、6以上のワーカスレッドがDB接続するタイミングでActiveRecord::ConnectionTimeoutErrorになる可能性があります。

また、ActionCableをアプリケーションと併用(1プロセスでHTTPもwebsocketも対応する)する設定にしている場合はActionCable用のワーカスレッドの分もプール数を積み増す必要があります。デフォルトでは4つのワーカスレッドでwebsocketの処理を行っているので、pumaとActionCableの設定がデフォルトであればコネクションプールは9にする必要があります。

まとめ

おそらくあまりまとまった情報のないスレッドとデータベースのコネクションプールの値をどう設定するか、についてまとめてみました。参考になれば幸いです!

付録情報1: capistranoでデプロイしているときの注意事項

capistrano-pumaを利用している場合、執筆時点の最新安定版である5.2.0まではRailsのconfig/puma.rbではなく独自に作ったpumaの設定ファイルを利用する、という仕様になっています。そしてそのデフォルトが最大16スレッドになっているので注意が必要です(リリース予定の6.0からはconfig/puma.rbを利用する模様)

付録情報2: 昔のRails(5.2未満)を使っているときに気をつけること

昔のRails(< 5.2)では、最大のスレッド数からさらに+1した値をコネクションプールとして設定しておく、というTipsがありました。なぜかというと、次のようにアプリケーションの起動時にDBアクセスするような作りになっていると、ワーカスレッド以外のスレッドがDB接続を追加で1つ持ってしまうためです。

# app/models/user.rb
class User < ApplicationRecord
  # user.rbをrequireしただけでPrefectureに対してクエリが走る
  validates :prefercture, inclusion: { in: Prefecture.pluck(:name) }
end

Rails5.2以降は、アプリケーションの起動直後に一回DBの接続を切る、というコードが入っているのでこれは考慮しなくて良くなりました。

参考

https://us11.campaign-archive.com/?u=1aa0f43522f6d9ef96d1c5d6f&id=997fbd1c2c