Play Framework 2.0パフォーマンス測定
Typesafe Stack 2.0ソフトウェアプラットフォームについては
すでに説明しました。 その投稿で、プラットフォームのコンポーネントの1つである、JVMでアクターモデルを実装する
Akka 2.0フレームワークについて説明しました。 今日は、Typesafe Stackの別のコンポーネントである
Play 2.0フレームワークの機能について書きたいと思います。 このコンポーネントの機能はすでに
ここと
ここで説明さ
れていますが、私の意見では、Play 2.0を実行するソリューションのパフォーマンスのトピックは公開されていません。
フレームワークのテストは、それに基づいて開発された最も単純なアプリケーションを使用して実行されます。 テストの結果、次の質問に答える必要があります。 同時接続の最大可能数はいくつですか? これらの接続はどのくらいのRAMを消費しますか? テストアプリケーションで処理できる単位時間あたりの要求数は?
テストアプリケーション
テスト対象のアプリケーションの説明に進む前に、Play 2.0フレームワークの主要なアーキテクチャ機能を明確にする必要があります。 Play HTTPサーバーは、高性能の
Nettyライブラリに基づいています。 これにより、サーブレットコンテナの構成を除いて「そのまま」使用できるようになるだけでなく、クライアント要求を非同期的に処理する機能も提供されます。 従来の同期処理バリアントでは、何らかの計算が必要な答えに対する着信要求は、この計算が実行されている間、常にオペレーティングシステムのスレッドを占有します。 Playを使用すると、計算中にストリームをサーバープールに戻し、計算の準備ができたら再びストリームを受け取って応答することができます。 技術的には、これは同期バージョンよりも多くのクライアントを同時に接続できることを意味します。
テスト対象のアプリケーションは、3つの主要な機能を実行します。
- クライアントブラウザとコメット接続を作成します(
/wait?cid={connection_id}
) - 着信値を受け入れ、利用可能なすべてのコメット接続のブラウザーコンソールに送信します(
/put?v={value}
) - 既存のすべてのコメット接続を閉じます(
/closeall
)
開発中、Akka 2.0ライブラリが使用されました。 アプリケーションはScala言語で開発されました。私の観点からは、Javaと比較してAkkaを使用する方が便利だからです。 以下に、コードの主要部分を示し、Play 2.0での接続の操作の単純さのみを示し、この投稿の本質を残しません。 すべてのコードはgitリポジトリから取得できます。このリポジトリへのリンクは、出版物の最後にあります。
俳優のコメットアップ
... lazy val channel: PushEnumerator[String] = ch ... def receive = { case Message(message) => { channel.push(message) } ... case Close => { channel.push("closed") channel.close() self ! Quit } } ...
channel
変数は、彗星接続(タイプ
Enumerator
)のデータソースであり、以下に示すように、
Comet
アダプター(タイプ
Enumeratee
)を介して
Comet
接続に送信され
Enumeratee
。 Playでデータストリームのソース、コンバーター、コンシューマーの操作について詳しくは
こちらをご覧ください 。 データは、
channel.push(message)
関数を呼び出すことにより、彗星ソケットに送信されます。
channel.close()
呼び出して彗星ソケットを閉じる。
アプリケーションのメインアクター
ConnectionSupervisorアクターの機能には、コメット接続の作成、作成された接続へのメッセージの送信、すべての接続のクローズが含まれます。
... var connectionActors = Seq.empty[ActorRef] def receive = { case SetConnect(connectionId) => { lazy val channel: PushEnumerator[String] = Enumerator.imperative( onComplete = self ! Disconnect(connectionId) ) val connectionActor = context.actorOf(Props(new ConnectionActor(channel)), connectionId) connectionActors = connectionActors :+ connectionActor sender ! channel } case BroadcastMessage(message) => { connectionActors.foreach(_ ! Message(message)) } case CloseAll => { connectionActors.foreach(_ ! Close) } } ...
作成されたアクターへのリンクは、connectionActorsシーケンス(タイプ-Seq [ActorRef])に保存されます。 接続が確立されると、チャネルチャネルが作成され、新しいConnectionActorに渡されます。 アクターがアクターのリストに追加されます。 メッセージの送信方法と接続のクローズ方法は、コードから明確になります。
現在の値を保存するためのアクター
値が
StorageActor
に送信され、いくつかのアクションが実行され、値がすべてのコメット接続に送信され、クライアントにも返されると想定されています。 このようにして、クライアントが要求を行い、それに対する応答を期待するときに、実際のアプリケーションの動作がシミュレートされます。
... var value = "" def receive = { case Put(v) => { value = v connectSupervisor ! BroadcastMessage(value) sender ! value } } ...
アプリケーションコントローラー
... object Application extends Controller { ... def waitFor(connectionId: String) = Action { implicit val timeout = Timeout(1.second) AsyncResult { (ActorsConfig.connectSupervisor ? (SetConnect(connectionId)) ).mapTo[Enumerator[String]].asPromise.map { chunks => Ok.stream(chunks &> Comet( callback = "console.log")) } } } def broadcastMessage(message: String) = Action { ActorsConfig.connectSupervisor ! BroadcastMessage(message) Ok } def putValueAsync(value: String) = Action { implicit val timeout = Timeout(1.second) Async { (ActorsConfig.storageActor ? Put(value)).mapTo[String].asPromise.map { value => Ok(value) } } } def closeAll = Action { ActorsConfig.connectSupervisor ! CloseAll Ok } }
着信HTTP要求のアドレスは、このコントローラーのメソッドにマップされます(ルートの説明は
conf/routes
ファイルにあります)。 ここで最も興味深いのは、彗星ソケットを作成し、タイプ
Enumerator[String]
チャネルをそれに関連付ける
waitFor
メソッドです。
SetConnect
メッセージへの応答として、アクターによってチャネルがソケットに送信されます。 チャネルに到着した各メッセージは、
Comet( callback = "console.log")
オブジェクト
Comet( callback = "console.log")
指定された関数のパラメーターとしてクライアントのブラウザーに送信されます。 この場合、これは
console.log
関数です。
クライアント側では、非表示の
iframe
要素を使用してコメット接続が作成されます。次に例を示します。
<iframe src='/wait?cid=1' style='display:none'/>
テストプロセス
テストアプリケーションは、1 GBのRAMと1つのプロセッサコア(物理コンピューターのプロセッサはIntel Core i5-2400 3.1 GHz)を備えたUbuntu 11.10(32ビット)を実行する仮想マシンで実行されました。
標準ツール(JMeter、Visual Studio Load Test)でのテストは失敗しました。 700の並列スレッドの起動でもテストシステムを困惑させたため、少なくとも大きな負荷をかけることは不可能でした。
Gatling Stress Tool (アーキテクチャもAkkaに基づいています)などの特別なテストツールを使用することは、コメット接続のテスト機能がないため不可能でした。 同時に、改訂も難しい作業でした。なぜなら、 開発者向けドキュメントは現在作成中です。 これらの理由により、独自のテストツールが開発されました。
テストスクリプト
シナリオは3つのステップで構成されます。
- 特定の頻度で所定の数のコメット接続が作成されます
- 所定の数の値が特定の頻度で送信されます
- コメット接続は、対応するリクエストによって閉じられます
テストのプロセスでは、確立されたコメット接続の数、各コメット接続での平均受信値の数、送信および成功するリクエストの数、これらのリクエストの平均実行時間に関するデータが収集されます。 スクリプトの各ステップの実行中に、プロセッサの負荷と占有RAMの量が記録されました(コメット接続の場合)。
試験結果
- コメット接続の最大数と使用されるRAMの量を測定します。
このテストグループでは、comet-connectionsを設定した後、唯一の値が送信され、その後、接続が閉じられます。
コメット接続の数 | 接続頻度(1 / ms) | 占有メモリ(MB) | 接続セットアップ中の最大CPU使用率 | 値を送信するときの最大CPU使用率 |
---|
500 | 50 | 36 | 15% | 15% |
1000 | 40 | 56 | 17.5% | 15% |
3000 | 20 | 145 | 25% | 62% |
3000 | 10 | 142 | 40.6% | 61% |
3000 | 5 | 140 | 59.9% | 70.8% |
3000 | 3 | 138 | 98% | 69% |
6000 | 4 | 262 | 80.5% | 93.4% |
8000 | 4 | 394 | 73.7% | 80.9% |
10,000 | 4 | 485 | 77.1% | 100% |
4 msの期間で彗星接続を確立するプロセスでのプロセッサの負荷は妥当な制限内であるため、追加の接続を追加することはRAMの問題にすぎません。
- 値送信リクエストの最大フローと短期のピーク負荷の測定。
前のテストからわかるように、値をコメット接続に送信することはリソースを集中的に使用する操作であるため、このテストグループでは、サーバーの最大スループットを測定するために、接続数が削減されます。
コメット接続の数 | 送信された値の数 | 送信頻度(1 / ms) | 送信時の最大CPU使用率 | 平均クエリ実行時間(ミリ秒) | 拒否されたリクエストの数 |
---|
1000 | 10 | 100 | 85.7% | 163 | 7 |
1000 | 10 | 500 | 76.5% | 45 | 0 |
1000 | 10 | 200 | 100% | 374 | 2 |
500 | 10 | 200 | 77% | 39 | 0 |
100 | 10 | 100 | 25% | 19 | 0 |
100 | 100 | 50 | 68.9% | 35 | 0 |
100 | 100 | 30 | 100% | 250 | 0 |
10 | 100 | 20 | 31% | 12 | 0 |
10 | 1000 | 10 | 61.7% | 18 | 0 |
10 | 1000 | 5 | 98.7% | 47 | 0 |
1 | 1000 | 4 | 58.2% | 27 | 0 |
1 | 1000 | 2 | 92.3% | 33 | 0 |
1 | 10,000 | 2 | 100% | 400 | 4636 |
1 | 8000 | 4 | 100% | 415 | 3217 |
1 | 5000 | 5 | 100% | 399 | 292 |
1 | 3000 | 7 | 100% | 129 | 0 |
1 | 2000年 | 7 | 98% | 58 | 0 |
実行されたテストは、アプリケーションが1秒あたり最大500リクエストの短期ピーク負荷に対処し、1秒あたり100〜150リクエストの負荷で正常に動作することを示しています。
更新:
テスト済みのアプリケーション:git://github.com/tolyasik/testeeApp.git
テストアプリケーション:git://github.com/tolyasik/testerApp.git