デプロイ用パフォーマンスチューニング

このガイドでは、Ruby on Railsアプリケーションをproduction環境にデプロイする際のパフォーマンスとコンカレンシー(並行処理)の設定について説明します。

このガイドの内容:

  • Puma(Railsデフォルトのアプリケーションサーバー)を利用すべきかどうか
  • Pumaの重要なパフォーマンス項目の設定方法
  • アプリケーション設定のパフォーマンステストを始める方法

このガイドは、多くのWebアプリケーションにおいてパフォーマンスに敏感なコンポーネントであるWebサーバーに焦点を当てています。バックグラウンドジョブやWebSocketsなどの他のコンポーネントもチューニング可能ですが、このガイドでは扱いません。

アプリケーションの設定方法について詳しくは、Rails アプリケーションの設定項目ガイドを参照してください。

このガイドでは、Ruby言語の標準実装であるMRI(CRubyとも呼ばれます)を実行していることを前提とします。JRubyやTruffleRubyなど他のRuby実装を利用する場合、このガイドのほとんどは適用外です。必要に応じて、それらのRuby実装に固有の情報源を確認してください。

1 アプリケーションサーバーを選定する

Pumaは、Railsのデフォルトのアプリケーションサーバーであり、コミュニティ全体で最も一般に使われているWebサーバーです。ほとんどの場合Pumaで問題なく動作しますが、状況によっては別のサーバーに変更したい場合があります。

アプリケーションサーバーは、独自のコンカレンシー処理方法を採用しています。たとえば、UnicornというWebサーバーはプロセスを利用しますが、PumaやPassengerのコンカレンシー処理はプロセスとスレッドのハイブリッドベースであり、Falconはファイバー(fiber)を利用します。

Rubyのコンカレンシー処理方法の詳しい説明はこのガイドの範囲外ですが、プロセスとスレッド間の重要なトレードオフについて説明します。プロセスとスレッド以外の方法を利用する場合は、別のアプリケーションサーバーを使う必要があります。

このガイドでは、Pumaのチューニング方法を中心に解説します。

2 どの項目を最適化すべきか

Ruby製Webサーバーのチューニングは、本質的に「メモリ使用量」「スループット」「レイテンシ」などのさまざまなプロパティの間でトレードオフを行うことです。

スループット(throughput)は、サーバーが1秒あたりに処理可能なリクエスト数の指標であり、レイテンシ(latency)は、個々のリクエストの処理に要する時間(応答時間とも呼ばれます)の指標です。

ホスティングコストを低く抑えるためにスループットを最大化したいユーザーもいれば、最高のユーザーエクスペリエンスを提供するためにレイテンシを最小化したいユーザーもいます。多くのユーザーは、両者の中間のどこかで妥協点を探ります。

重要なのは、あるプロパティを最適化すれば、少なくともそれと引き換えに別のプロパティが損なわれることを理解しておくことです。

2.1 Rubyのコンカレンシーとパラレリズムを理解する

CRubyにはグローバルインタプリタロックがあり、これはGVLまたはGILとも呼ばれます(訳注: GVLはRuby 3.0からはthread_schedと名称が変わりました(#5814。また、GILという名称は主にPythonで使われています)。

GVLは、1つのプロセスで複数のスレッドが同時にRubyコードを実行することを防ぎます。複数のスレッドがネットワークデータ、データベース操作、または一般にI/O操作と呼ばれるその他のRuby以外の作業を待機している可能性がありますが、Rubyコードをアクティブに実行できるのは一度に1つだけです。

つまり、スレッドベースのコンカレンシーを採用すれば、WebリクエストがI/O操作を実行するたびに並行処理する形でスループットが向上しますが、I/O操作が完了するたびにレイテンシが低下する可能性があります。その操作を実行したスレッドは、Rubyコードの実行が再開されるまで待機しなければならない場合があります。

同様に、Rubyのガベージコレクタはいわゆる「ストップザワールド」であるため、トリガーされたときにすべてのスレッドを停止する必要があります。

これは、Rubyプロセスに含まれるスレッド数に関係なく、CPUコアが1個より多く使われることはないということでもあります。

このため、アプリケーションがI/O操作に費やす時間が50%しかない場合、プロセスごとのスレッド数が2〜3個を超えるとレイテンシが大幅に悪化し、スループットが向上するメリットがたちまち目減りしてしまう可能性があります。

一般的に、きちんと作り込まれた(SQLクエリが遅くならず、N+1クエリ問題も発生していない)Railsアプリケーションであれば、I/O操作に費やす時間が50%を超えることはないため、スレッド数を3個より多くしてもメリットはほとんど得られません。ただし、サードパーティAPIをインラインで呼び出す一部のアプリケーションでは、I/O操作に非常に多くの時間を費やす可能性があり、スレッド数を3より多くするメリットが得られる可能性もあります。

Rubyで真のパラレリズム(並列処理)を実現する方法は、複数のプロセスを利用することです。Rubyプロセスは、CPUコアが空いている限り、I/O操作の完了後に実行を再開する前に互いに待機する必要は生じません。 ただし、プロセスはコピーオンライトを介してメモリの一部のみを共有するため、プロセスが1個増えると、スレッドが1個増えるよりも多くのメモリが消費されます。

スレッドはプロセスよりも安価ですが、無料ではなく、プロセスあたりのスレッド数を増やすとメモリ使用量も増加する点にご注意ください。

2.2 現場向けのアドバイス

スループットとサーバーの使用率を最適化したいユーザーは、CPUコアごとに1個のプロセスを実行し、レイテンシへの影響がそれほど重要でなくなるまでプロセスあたりのスレッド数を増やす必要があります。

レイテンシを最適化したいユーザーは、プロセスあたりのスレッド数を低く抑える必要があります。

レイテンシをさらに最適化するには、プロセスあたりのスレッド数を1に設定し、プロセスがI/O操作を待機してアイドル状態になっている場合を考慮して、CPUコアあたりの実行プロセス数を1.5または1.3にすることも可能です。

一部のホスティングソリューションでは、CPUコアあたりに提供されるメモリ(RAM)が比較的少量しかない場合があり、すべてのCPUコアを利用するのに必要なプロセス数を実行できなくなることにご注意ください(ただし、ほとんどのホスティングソリューションには、メモリとCPUの比率が異なるさまざまなプランが用意されています)。

もう1つ考慮しておきたい点は、Rubyのメモリ使用量がコピーオンライトのおかげでスケールメリットを受けられることです。 したがって、2個のサーバーで32個ずつRubyプロセスを実行する方が、16個のサーバーで4個ずつRubyプロセスを実行するよりも、CPUコアあたりのメモリ使用量が少なく済みます。

3 設定

3.1 Puma

Pumaの設定はconfig/puma.rbファイルにあります。 Pumaで最も重要な2つの設定は、「プロセスあたりのスレッド数」と「プロセス数」(Pumaではこれをworkersと呼びます)です。

プロセスあたりのスレッド数は、threadディレクティブで設定します。 生成されるデフォルト設定では3に設定されています。 この値は、RAILS_MAX_THREADS環境変数を設定するか、設定ファイルを編集するだけで変更できます。

プロセス数は、workersディレクティブで設定します。 プロセスあたりのスレッドを複数にする場合は、サーバーで実際に利用可能なCPUコア数に設定する必要があります。 または、サーバーで複数のアプリケーションを実行する場合は、アプリケーションが利用するコア数に設定する必要があります。 ワーカーあたりのスレッド数が1個のみの場合は、ワーカーがI/O操作を待機してアイドル状態になっている場合を考慮して、プロセスあたりのスレッド数を1より多く増やせます。 生成されるデフォルト設定では、Concurrent.available_processor_countヘルパーを利用することで、サーバーで利用可能なすべてのプロセッサコアを使うように設定されています。この値は、WEB_CONCURRENCY環境変数で変更することも可能です。

3.2 YJIT

最近のRubyにはYJITと呼ばれるJust-in-timeコンパイラが付属しています。

このガイドでは詳しく説明しませんが、JITコンパイラを有効にすることで、メモリ使用量が増加する代わりにコードをより高速に実行できます。メモリ使用量の増加分をどうしてもまかなえない場合を除いて、YJITを有効にすることを強くオススメします。

Rails 7.2以後は、アプリケーションがRuby3.3以上で実行されていれば、RailsによってYJITが自動的にデフォルトで有効になります。 RailsまたはRubyのバージョンがこれより古い場合は、手動で有効にする必要があります。方法については、YJITのドキュメントを参照してください。

メモリ使用量の増加が問題になる場合は、YJITを完全に無効にする前に、--yjit-exec-mem-sizeオプションで、メモリ使用量を削減する調整方法を試してみてください。

3.3 メモリアロケータとその設定方法

ほとんどのLinuxディストリビューションでは、デフォルトのメモリアロケータの動作が原因で、Pumaを複数スレッドで実行したときにメモリの断片化によってメモリ使用量が予期せず増加する可能性があります。そうなると、メモリ使用量の増加によって、アプリケーションがサーバーのCPUコアを十分に活用できなくなる可能性があります。

この問題を軽減するには、代替のメモリアロケータであるjemallocライブラリを使うようにRubyを設定することを強くオススメします。

Railsによって生成されるデフォルトのDockerfileでは、既にjemallocをインストールして利用するよう事前設定されています。ただし、ホスティングソリューションがDockerベースでない場合は、jemallocをインストールして有効にする方法を個別に調べる必要があります。

何らかの理由でjemallocを利用できない場合は、効率は落ちるものの、環境でMALLOC_ARENA_MAX=2を設定することで、メモリの断片化を減らすようにデフォルトのアロケータを設定する代替手段も利用可能です。ただし、これによりRubyが遅くなる可能性もあるため、ソリューションとしてはjemallocが推奨されます。

4 パフォーマンステスト

Railsアプリケーションは多種多様であり、どのプロパティを最適化したいかはRailsユーザーによって異なるため、あらゆるユーザーに最適なデフォルト設定やガイドラインをフレームワークが提供することは不可能です。

したがって、アプリケーション設定を選択するベストな方法は、アプリケーションのパフォーマンスを測定して、目標が達成されるまで設定の調整を繰り返すことです。

パフォーマンス測定は、production環境の負荷を再現するシミュレーション環境で行うことも、production環境で実際のアプリケーショントラフィックを用いて直接行うことも可能です。

パフォーマンステストは奥が深いテーマなので、このガイドでは簡単なガイドラインを示すにとどめます。

4.1 測定する項目の解説

スループットとは、アプリケーションが1秒あたりに正常に処理するリクエスト数です。 優秀な負荷テストプログラムであればスループットを測定できます。 スループットは、通常「1秒あたりのリクエスト数」で表された単一の数値です。

レイテンシ(latency)とは、リクエストが送信されてからそのレスポンスが正常に受信されるまでの遅延時間のことで、通常はミリ秒単位で表されます。レイテンシはリクエストごとに異なります。

パーセンタイル(percentile)レイテンシは、リクエストのレイテンシがその項目を上回っているレイテンシの割合を示します。 たとえば、P90は90パーセンタイルのレイテンシを表します。このP90は、10%のリクエストのみがそれよりも長い処理時間を要した単一の負荷テストのレイテンシです。 P50は、リクエストの半分が遅いレイテンシに占められていることを表し、メジアン(median: 中央値)レイテンシとも呼ばれます。

「テールレイテンシ(tail latency)」は、高パーセンタイルレイテンシを指します。 たとえば、P99は、リクエストの1%のみがそれよりも悪いレイテンシです。 P99はテールレイテンシですが、P50はテールレイテンシではありません。

一般的に、平均レイテンシは最適化に適した指標ではありません。 中央値レイテンシ(P50)とテールレイテンシ(P95またはP99)に重点を置くのが最適です。

4.2 production環境での測定

production環境に複数のサーバーがある場合は、その環境でA/Bテストを実行するのがオススメです。 たとえば、サーバーの半分を「プロセスあたり3スレッド」で実行し、残り半分のサーバーを「プロセスあたり4スレッド」で実行することで、アプリケーションパフォーマンス監視(APM: application performance monitoring)サービスを用いて2つのグループのスループットとレイテンシを比較できます。

アプリケーションパフォーマンス監視サービスはさまざまなものがあり、セルフホスト型やクラウドソリューションもあり、無料プランを提供しているものも多数あります。このガイドでは、特定のサービスを推奨することはしません。

4.3 負荷テストプログラム

アプリケーションにリクエストを行う何らかの負荷テストプログラムが必要です。 専用の負荷テストプログラムを利用することも、HTTPリクエストを行ってその所要時間をトラッキングする小さなアプリケーションを作成することも可能です。 所要時間をRailsログファイルでチェックすることは、通常は行うべきではありません。ログでわかる時間は、Railsがリクエストを処理するのにかかった時間だけであり、アプリケーションサーバーでかかった時間は含まれません。

多数のリクエストを同時に送信して時間を計るのは微妙な測定エラーが発生しやすいため、難しい場合があります。 通常は、独自のプログラムを作成するよりも専用の負荷テストプログラムを使うべきです。負荷テストプログラムの多くは利用も簡単ですし、優れた負荷テスターの多くは無料で利用で行きます。

4.4 変更すべき箇所を見つける

テストのスレッド数を変更することで、アプリケーションのスループットとレイテンシの最適なトレードオフを見つけられます。

メモリ容量やCPUコア数が多い大規模なホスト環境では、最適な利用のためにさらにプロセスが必要になります。ホスティングプロバイダが提供するホストのサイズや種別も変更できます。

反復回数を増やせば、多くの場合より正確な答えを得られますが、その分テストに時間がかかります。

テストは、production環境で実行されるのと同じ種類のホストで行う必要があります。開発マシンでテストしても、その開発マシンに最適な設定しかわかりません。

4.5 ウォームアップ

アプリケーションの起動直後に、最終測定に含めない多数のリクエストを処理しておく必要があります。このようなリクエストは「ウォームアップ」リクエストと呼ばれ、その後の「定常状態」リクエストよりもはるかに低速になるのが普通です。

負荷テストプログラムは、多くの場合ウォームアップリクエストをサポートしています。また、リクエストを複数回実行して最初のセットを破棄する方法も使えます。

リクエスト回数を増やしても結果が大幅に変わらない場合は、ウォームアップリクエストが足りています。この背後に複雑な理論が潜んでいる場合もないわけではありません(関連情報)が、ほとんどの場合はシンプルです。ウォームアップの量を変えて数回テストし、結果が安定するまでに何回ウォームアップを繰り返す必要があるかを確認します。

非常に長いウォームアップは、メモリの断片化や、多くのリクエストの後でのみ発生するその他の問題をテストするのに有用な場合があります。

4.6 リクエストの種別

アプリケーションが受信するHTTPリクエストには、さまざまな種別があるはずです。 最初は、そのうちのいくつかだけを使って負荷テストを実施し、時間の経過とともにリクエストの種類を増やすとよいでしょう。productionアプリケーションで特定の種類のリクエストだけが異常に遅い場合は、負荷テストコードに追加します。

負荷の合算は、production環境でのアプリケーショントラフィックと完全には一致しませんが、それでも設定をテストするうえで役に立つことがあります。

4.7 チェックすべき項目

負荷テストプログラムは、「パーセンタイルレイテンシ」と「テールレイテンシ」を含むレイテンシをチェック可能なものを利用する必要があります。

プロセス数やスレッド数、または一般的な構成が異なる場合は、スループットと、1つ以上のレイテンシ(P50、P90、P99など)をチェックします。 スレッド数を増やすと、スループットはある程度向上しますが、レイテンシは悪化します。

アプリケーションのニーズに基づいて、レイテンシとスループットのトレードオフを選択しましょう。

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加