Play Frameworkアプリの負荷試験を行う前に確認すべきことまとめ

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Play Frameworkを利用してアプリケーションを開発する動機の一つとして「高い負荷に耐えることができる(C10K)」が挙げられます。 しかしながら、たとえPlayを使っていても、チューニングが十分でなければ高いスループットが出るとは限りません。
今回は、300RPS(RPS=リクエスト毎秒)を超えるような負荷をPlayアプリケーションにかける前に確認すべきポイントをいくつかご紹介します。

本稿ではPlay Frameworkのバージョン2.4を前提としています。

Playアプリの負荷試験前に確認すべきこと

目次

ExecutionContextが適切に分割されているか

ExecutionContextはFutureの実行スレッドを決定する重要な要素(スレッドプール)です。
Non-blockingを意識せずに全てのFutureを同一のExecutionContext配下で処理することは、高負荷時のパフォーマンスに甚大な悪影響を及ぼします。

より具体的に言えば、play.api.libs.concurrent.Execution.Implicits.defaultContextで全てのFutureを処理しているならば、それは高負荷時に問題になります。
もしそうなっているなら、次に上げるPlayのドキュメントページを参考にして「高負荷な(時間がかかる)処理」や「IO」を実行するための別のExecutionContextを作りましょう。

私たちがよくやるのは、アーキテクチャ上の役割によってExecutionContextを変える方法です。
プレゼンテーション層(controllersパッケージなど)のFutureは、デフォルトのExecutionContextで処理させます。インフラ層(DBアクセスやHTTPアクセス)で記述されたFutureには、それとは別のExecutionContextを用意します。

このように分割しておくことで、例えば「DBからのリードに必ず10秒かかる」ような状況が発生した場合に、DBと関係ないアクションのスループットへ影響を与えずにすみます。 分割されていない場合には、DBリードの10秒に引っ張られてアプリケーション全体のレスポンスが下がります。この問題は低負荷時には顕著に現れず、ExecutionContextが持つスレッドを全て食い潰しはじめる高負荷時において露呈します。

並列実行可能なスレッド数の設定は適切か

先ほど上げたデフォルトのExecutionContextも含め、Playアプリで使うスレッドプールごとの並列実行可能なスレッド数はapplication.confにAkkaの設定を記述することで調整可能です。

AkkaのConfigurationとして記述できる設定値がそのまま利用できます。それゆえ、設定可能な項目は驚くほど多岐にわたっています。
しかしながら、Playアプリのスレッドプール設定において重要な設定値は、ほとんどの場合下記の2つのみです。

設定名 概要
*.fork-join-executor.parallelism-factor CPU1コアあたりのスレッド数
*.fork-join-executor.parallelism-max スレッド数の最大値

並列実行されるスレッドの最大数をMaxThreadsとするとき、値の関連は次のように表現できます。

\[ x = CPUs \cdot ParallelismFactor \\ x \leq ParallelismMax \Rightarrow MaxThreads = x \\ Other \Rightarrow MaxThreads = ParallelismMax \]

この設定値を変更することで、スレッドプールごとに並列実行可能なスレッド数を制御できます。
この値は、CPUの性能に大きく左右されるもので、設定遺憾はCPU利用率に大きく影響します。
高性能なCPUを使う場合には、かなり大きめの値を設定する場合もあります。

Amazon Linuxのc3.largeインスタンスを利用する場合の実例を紹介します。
c3.largeインスタンスは次の性能を持っています。

種別
CPU Intel Xeon E5-2680 v2 (Ivy Bridge)
vCPU 2
RAM 3.75GiB

このインスタンスにPlayアプリをデプロイする前提で、Playのデフォルトスレッドプールの設定を次のように行いました:

設定名
akka.actor.fork-join-executor.parallelism-factor 256.0
akka.actor.fork-join-executor.parallelism-max 512

この設定でアプリをデプロイし、c3.largeに高い負荷を書けた結果、最高負荷時で70%強のCPU利用率となりました。

ログ出力方法は十分考慮されているか

ログは出しすぎくらいがちょうどいい。実装時には「何に使うんだこのログは」と感じるようなものであっても、運用時に大きな価値を持つことがある。

100RPSを超える負荷試験においては、通常の負荷状態では気に止めていなかった意外な箇所がボトルネックになる場合があります。
ログ出力はその代表例です。play.api.Loggerと標準のlogback.xml設定をそのまま利用している場合、出力しているログの量にもよりますが、ボトルネックになる可能性が大いにあります。
ログ出力方法の違いによるスループットの差は、こと高負荷時においては、無視できないほど開きます。これは実体験ですが、処理性能が50RPS程度だったアプリケーションが、ログ出力のチューンナップ後には300RPSで捌けるようになりました。

play.api.Loggerを利用してログを出力するならば、以下に挙げるチェックポイントを熟慮し、logback.xmlに反映すべきです。

  • 標準出力へのログ出力は本番環境ではちゃんと無効になっているか。
  • ログファイルのローテーションの設定が適切になされているか。
  • 出力ログレベルの設定が適切になされているか。
  • Akkaアクターのライフサイクルに関する過剰なログ出力がないか。

設定方法は次のドキュメントを参照してください。

また、相当の処理性能を求める場合には更にもう一段考慮すべきことがあります。

  • ログ出力を担当するアクターを用意する。
  • ログ出力アクターが属するアクターシステムをPlayのもと分ける(ログ出力があるクラスの単体テスト記述が容易になります)。
  • ログの出力処理は、タイムスタンプの発行とログ出力アクターへのメッセージ送信処理とする。

全体性能として1000RPSを超えるアプリケーションの作成を目標としている場合には、これらの対応も視野にいれてください。

(おまけ)Amazon Linuxの設定で配慮すべきもの

RedHat系の環境(少なくともAmazon Linux)にPlayアプリをデプロイする場合は、次に示すカーネル設定がパフォーマンスに影響を及ぼす可能性があります。

  • /etc/security/limits.conf
    • open filesのソフトリミット
    • open filesのハードリミット
  • /etc/sysctl.conf
    • net.ipv4.ip_local_port_range

これらの設定により、プロセスが利用可能なUnixドメインソケット数やポートレンジが設定できます。
実際にAmazon LinuxでPlayアプリを動かす場合には、これらの設定を最適化しています。それぞれ奥が深いため、今回は参考までのご紹介とさせていただきます。

まとめ

  • Playアプリに対する負荷試験を行う前のチェックリスト
    • ログの出力処理は最適化されているか
    • 標準出力にログを出力していないか
    • ExecutionContextは適切に分割されているか
    • スレッド数の設定は適切か

Playでアプリケーションを開発する以上は、高いスループットを叩き出したいですね。もちろん、DB性能などの外因はありますが……。
ではまた!