ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Amazon ECS+FargateでRailsを動かす際の最適なパラメーターを考えてみる

こんにちは、最近野菜が高いのでもやしばかり食べているWebエンジニアの本間です。 そろそろレタスが食べたい...。

さて、ここ1、2年、ユニファではAmazon ECS+AWS Fargateを使用して、Railsアプリケーションを本番運用することが増えてきました。 stagingでテストしたDockerイメージがそのまま本番で使えて安心だったり、オートスケールが簡単だったりとメリットが多く、大変便利だと感じています。

ただ、そのような環境を構築する中で、vCPU数やメモリ量、およびPumaの並行性に関するパラメーターをどうしようか毎回悩んでいたため、この辺で自分の中で整理しておこうと思います。

前提

下記を使用したWebアプリケーションを前提にします。

  • Amazon ECS
  • AWS Fargate
  • Puma
  • Ruby on Rails
  • MRI(CRuby)

このようなシステム構成において、以下のパラメーターに何を設定すれば良いか考えてみます。

  • ECSのタスク定義 vCPU数
  • ECSのタスク定義 メモリ量
  • Puma プロセス数( puma.rbでの workers )
  • Puma 1プロセスあたりのスレッド数( puma.rbでの threads )

今回、Pumaを前提に考えますが、他のアプリケーションサーバーやワーカーライブラリに対しても応用が効く考え方だと思います。

パラメーターの考察

vCPU数

AWS Fargateの場合、vCPUの値は以下の中から選択することになります。

  • 0.25
  • 0.5
  • 1
  • 2
  • 4

自分としては、「1」vCPUから始めるのが良いと思っています。 本番運用をしばらく続けてみて、CPUが常に余る状態であれば、0.5, 0.25と下げていくのがよさそうです。

一方、CPUリソースが足りていない場合、vCPUを2, 4と上げていくよりも、2台, 3台とコンテナを追加してスケールアウトで対応する方式をオススメします。 vCPUを増やす場合、合わせて他のパラメーターも調整しないといけないため手間がかかります。 また、オートスケールの設定を行い負荷に応じてコンテナ数を増減させることで、1つの強いコンテナを常時稼働させるより低コストで運用できるメリットもあります。

メモリ量

Amazon Fargateでは、vCPU数に応じて、選択できるメモリ量が変化します。 1vCPUの場合、「2GB」〜「8GB」の間で1GB単位で選択できます。

自分としては、最小の「2GB」から始めることをオススメします。 アプリケーションの規模や、後述するプロセス数の設定で必要なメモリ量は増減しますが、多くの小・中規模なRailsアプリケーションであれば「2GB」もあれば十分なことが多いです。

ただし、メモリ量が足りなくなると深刻な問題が発生するため、足りなくなる傾向が見えたら増やす必要があります。 また、メモリ使用量は起動時から徐々に増えていき、最終的には起動時の2倍、3倍ぐらいになっていることもあるため、実際にしばらく運用して確認する必要があります。 具体的には、本番運用中にメモリ使用率が80%を超えるようであれば、「1GB」ずつ追加していくと良いと思います。 メモリの追加はvCPUの追加に比べてコストが低いので、悩むぐらいであればやってしまいましょう。

プロセス数

まず、「プロセス数」と「スレッド数」を検討するにあたって、下記ブログ(翻訳記事)を大いに参考にさせていただきました。

Rails: Puma/Unicorn/Passengerの効率を最大化する設定(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

大前提として「プロセス数 ≧ vCPU数」とする必要があります。 プロセス数がCPUコア数より少ない場合、MRI(CRuby)のGVLにより、CPUが100%使いきれません。

その前提の上でですが、自分としては puma.rb上の worker が「0」、つまりシングルプロセスモードでPumaを動かすことをオススメします。 「プロセス数(1) ≧ vCPU数(1)」となっており、前提条件を満たしています。 また、1プロセスだけにすることでメモリ量を抑えることができます。 何よりシングルプロセスモードでPumaを動かすことでコンテナ内のプロセス構成をシンプルにすることができます。

なお、参考にしたブログでは、Pumaのworker数は3以上を推奨、と記載があります。 ブログによると、Pumaのクラスタモードは、各workerプロセスの負荷のかかり具合を見てリクエストを流してくれるらしいです。 これはロードバランサーで行う負荷分散の方式である「ラウンドロビン」や「ランダム配信」と比較して、より効率的に素早くリクエストを処理できます。

このような機能がPumaに備わっているのであれば、使用メモリ量が増えたとしても積極的に利用したくなります。 しかし、Amazon ECS+Fargateでは、非常に簡単にコンテナのオートスケールイン/アウトが実現できます。 オートスケールの設定ができていれば、ある程度効率が悪くても負荷には容易に対応できると思い、今回は採用を見送りました。

スレッド数

「プロセス数」を1としたため、「スレッド数」=「並列実行可能数」になります。 スレッド数を適切に設定しないと、CPUが100%使いきれなかったり、あるいは不要にメモリを使用したりするため重要です。

MRI(CRuby)では、IO関連の処理のみ、複数スレッドの処理を並列実行できます。 そのため、1リクエスト中のIO関連の処理がどれだけ含まれるかで、並列に実行できる数が決まります。 参考にしたブログによると、IO関連の処理は意外と少なくスレッド数は5,6程度が適切だと記載がありました。 ただし、ユニファの場合、DBのみのアクセスだけでなく、いくつかのマイクロサービスにもアクセスするためIO関連の処理はもう少し増えると思います。

そのため初期値としては、CPU処理が多いアプリケーションでは「5」前後、IO処理が多いアプリケーションでは「10」前後にするのが良いと思います。 この状態で負荷テストを行い、CPU使用率が100%近くになることを確認できればOKです。 もし、負荷テストでCPU使用率が100%近くに行かない場合、IO処理が多くてまだサーバーに余裕がありますのでスレッド数を増やす対応を行います。

あと細かいですが、以下の設定も見直しておきます。

  • 最小スレッド数( min_threads_count )と最大スレッド数( max_threads_count )は同じ値で良いと思います。閑散時にスレッド数を減らすことでメモリ使用量が減るかもしれませんが、それによってコストが減ったりはしないためです。
  • データベースコネクションのプール数( pool )を「プロセス数」×「スレッド数」に変更することを忘れないようにしましょう。

Puma以外への応用

Puma以外への応用も考えてみます。 例えば、バックグラウンド処理に sidekiq を採用して、ECSで動かすことを考えてみます。 各パラメーターの初期値は、以下のようになります。

  • vCPU数 -> 1
  • メモリ量 -> 2GB
  • プロセス数 -> 1( sidekiq はシングルプロセスのため )
  • スレッド数 -> ★要検討

という感じになり、「スレッド数」のみを検討すればよいことになります。

sidekiqで処理するバックグラウンド処理は、webに比べてIO処理が多い傾向があります。 そのため、webより大きめの「スレッド数」を初期値としてセットして良いと思います。具体的には「20」あたりでしょうか。

そして、やはりこちらも負荷テストを行い、全スレッドが動いている状態でCPU使用率が100%近くになるかを確認します。 もし、CPU使用率に余裕があるようであれば、まだ並列実行できる余裕があるので「スレッド数」を増やします。 あとは本番運用してみて「メモリ量」を調整すればOKです。

シングルプロセスの場合、ほとんど同じ流れで決定できると思います。 マルチプロセスの場合、プロセス数に応じてメモリ使用量が増えるため、「メモリ量」に余裕を持たせる必要があります。

まとめ

まとめると以下のようになります。

  • 推奨する初期値
    • vCPU数 -> 1
    • メモリ量 -> 2GB
    • プロセス数 -> 1 (puma.rbでは0)
    • スレッド数 -> 5〜10 (サーバーがCPUを使う処理がメインであれば5、IO処理がメインであれば10)
  • 負荷テストを行い、CPU使用率が100%近くになるか確認。ならない場合、「スレッド数」を増やす。
  • 本番環境の運用状況に合わせて「vCPU数」を減らす、「メモリ量」を増やす。
  • 本番環境の負荷が高いようであればオートスケールの設定を行い、負荷に応じてコンテナ数を増やす設定を追加

ある程度の規模のアプリケーションまでは、この方針で問題なくないようできるはずです、きっと...。

以上になります。最後までご覧いただき、ありがとうございました。