こんにちは、こんにちは。
これは「FOLIO Advent Calendar 2023」3日目の記事です。
なので、いつもより気持ち真面目に書きますよ!
さて、今回は「これさえ覚えればGatlingとScalaで最低限はパフォーマンスが書ける!」を目指して虎の巻を書いてみました。
「パフォーマンステストどこから手をつけたらいいかわかんないよ〜(主に私の話)」とか「シナリオを書くのが面倒くさいよ〜(主に私の話)」という人の重い腰を上げる一助となれば幸いです。
「Gatling」ってなに?
JavaベースのWebアプリケーション向けのパフォーマンステストのフレームワークです。
DSLが提供されていてシュッとシナリオが書けるところ、きれいめなレポートが出力できるところがGood Pointです。
(説明するまでもなく有名なツールですが、念のため...)
sbtプラグインの導入と依存ライブラリの追加
GatlingはJava、Kotlin、Scalaでテストを記述することができるようになっています。
Maven、Gradleのほか、sbt向けのプラグインも存在します。
こちらのプラグインを導入することでscalatestの要領でGatlingでテストを行うことができます。
plugins.sbt 設定例
addSbtPlugin("io.gatling" % "gatling-sbt" % "4.6.0")
build.sbt 設定例
// ...省略 lazy val gatlingVersion = "3.9.5" lazy val gatling = (project in file("gatling")) .enablePlugins(GatlingPlugin) .settings( name := "subProject", libraryDependencies ++= Seq( "io.gatling.highcharts" % "gatling-charts-highcharts" % gatlingVersion % "test", "io.gatling" % "gatling-test-framework" % gatlingVersion % "test" ) )
指定しているバージョンは記事執筆時点でのものです。
最新のバージョンはMavenリポジトリを参照してください。
- Gatling - SBT Plugin
- Maven Central: io.gatling:gatling-sbt
- Maven Central: io.gatling:gatling-test-framework
- Maven Central: io.gatling.highcharts:gatling-charts-highcharts
テストプロジェクトの構成について
Gatlingのプラグインのデフォルトの設定では、プロジェクトの src/test/scala
以下を読みに行きます。
今回の場合は、 gatling/src/test/scala
ディレクトリを掘ります。
% mkdir -p gatling/src/test/scala % tree -I target # . # ├── README.md # ├── build.sbt # ├── gatling # │ └── src # │ └── test # │ └── scala # └── project # ├── build.properties # ├── plugins.sbt # └── project
変更することもできますがこの記事では説明を割愛します。
よくあるテストクラスの形
詳しい説明は後述しますが、Gatlingのよくあるテストクラスの形を示します。
// DSLのインポート import io.gatling.core.Predef._ import io.gatling.http.Predef._ // 負荷をかける秒数の指定をする際に便利、予めインポートしておくと良い import scala.concurrent.duration._ class Example1 extends Simulation { // 1. HTTPの設定 val httpProtocol = http.baseUrl("http://localhost:9000").userAgentHeader("UserAgentHere") // 2. シナリオの記述 val scn = scenario("シナリオ名").exec( http("リクエスト名").get("/path").header("key", "value") ) // 3. シナリオの実行方法(負荷のかけ方) setUp( scn .inject( // 10ユーザーが30秒間、バラバラのタイミングでシナリオを実行 rampUsers(10).during(30.seconds) ) .protocols(httpProtocol) ) }
これを覚えておけばOK! Gatling チートシート
Scenario
アプリケーション上でHTTPリクエストなどを伴う何らかの操作(シナリオ)を定義する仕組みです。
// 最もシンプルな何もしないシナリオの例 val scn1 = scenario("シナリオ例1")
複数の操作を繋げて書くこともできます。
// ホーム画面にアクセス、10秒待ってから、概要画面に遷移 val scn2 = scenario("シナリオ例2") .exec(http("ホーム").get("/home")) .exec(pause(10.seconds)) .exec(http("概要").get("/about"))
同じクラス内に複数のシナリオを定義することができますが、シナリオ名はユニークにする必要があります。
Session
シナリオ中の状態を保持する仕組みです。
前のリクエストのレスポンスを次のリクエストで使用したいシナリオを書きたいときに役立ちます。
Session
オブジェクトは Map<String, Object>
のようなデータ構造をしています。
exec
は Session
を引数として取り Session
を返す関数を引数に取ります。(説明が難しい)
オブジェクトはイミュータブルであるため、値を追加、変更、削除した場合には新しく生成された Session
を返してください。
説明よりもコードがわかりやすいと思います。
val scn = scenario("シナリオ例 3") .exec { session => // セッションへ値をセットする val newSession1 = session.set("key1", "value") val newSession2 = newSession1.setAll( ("key2", "value"), ("key3", "value") ) newSession2 } .exec { session => // セッションの値を削除する val newSession = session.remove("key") newSession } .exec { session => // セッションの値を取得する // 値が存在しない場合、型が合わない場合に例外が発生する val as = session("key1").as[String] // 型が合わない場合に例外が発生する val asOption = session("key1").asOption[String] // 安全にアクセスできる val validated = session("key1").validate[String] }
Checks
リクエストに対するレスポンスの結果をチェックする仕組みです。
XMLやJSON形式のレスポンスから値を展開、また展開した値をSessionに保持する機能も含みます。
Sessionのメソッドを呼び出すよりもこの機能を使用してSessionに値を格納するのが楽です。
簡単な例は以下のとおりです。
val scn = scenario("シナリオ例 4") .exec( http("リクエスト") .get("/me") .check(status.is(200)) // ステータスコードが200であるか? )
レスポンスボディを想定した場合
{ "username": "taro", "age": 10 }
レスポンスボディの username
をキー名 username
として Session
に保存する例は以下のとおりです。
取得したい値はJsonPathで指定します。
厳密さを求める場合は型や存在チェックを行うと良いでしょう。
val scn = scenario("シナリオ例 4") .exec( http("リクエスト") .get("/me") .check(status.is(200)) .check(jsonPath("$.username").ofType[String]) .check(jsonPath("$.username").saveAs("username")) )
(が、意識が低いためあまり厳密に書いたことはない...。)
HTTP
HTTPリクエストを組み立てる仕組みです。
基本的なメソッドは次のとおりです。
val scn = scenario("シナリオ例 5") .exec( http("GETの例") .get("/path") .queryParam("key", "value") // クエリパラメーターの指定 .header("key", "value") ) .exec( http("POSTの例1") .post("/path") .body(StringBody("""{"name": "taro", "age": 10}""")) // リクエストボディの指定 .asJson // accept,content-type ヘッダーに application/json を付与 ) .exec( http("POSTの例2") .post("/path") .formParam("key", "value") // フォームデータの指定 .asFormUrlEncoded // content-type ヘッダーに application/x-www-form-urlencoded を付与 )
また、Gatlingには Expression Language という機能があり、文字列中に #{Sessionのキー名}
のような形で値を指定すると、動的に値を展開することができます。
"#{key}" // 配列へのアクセス "#{key(n)}" // オブジェクトへのアクセス "#{foo.bar}"
formParam
, body
などは以下のように使用することもできます。
Expression LanguageはGatlingのDSLでのみ有効で、自前で宣言した関数内では機能しないため注意が必要です。
例として「sessionの値を展開する例2」ではパラメーターを展開することはできません。
// ... 省略 .exec( http("sessionの値を展開する例1") .post("/path") .formParam("key", "#{session-key-here}") .asFormUrlEncoded ) .exec( http("sessionの値を展開する例2") .post("/path") .formParam("key", session => { s"${session("session-key-here")}" }) .asFormUrlEncoded )
Injection
シナリオの同時実行数や実行時間などを定義する仕組みです。
以下に個人的によく使用する関数を指定します。
(duration
は実行時間です。)
関数 | 説明 |
---|---|
nothingFor(duration) | 一時停止 |
atOnceUsers(n) | 同時にn個 シナリオを実行 |
rampUsers(n).during(duration) | n回 シナリオを分散して実行 |
constantUsersPerSec(n).during(duration) | 1秒ごとにのシナリオ実行数をn個ずつ増やす |
rampUsersPerSec(n1).to.(n2).during(duration) | 1秒ごとのシナリオ実行数をn1からn2に増やす |
負荷をかけるシナリオの例は以下のとおりです。
setUp( scn1 .inject( rampUsers(10).during(30.seconds), // 10ユーザーが30秒間、分散してシナリオを実行 nothingFor(10.seconds), // 10秒一時停止 rampUsersPerSec(10).to(100).during(60.seconds) // 60秒間で同時シナリオ実行数を10回から100回まで増やす ) .protocols(httpProtocol), // シナリオを同時実行することも可能です scn2 .inject( rampUsers(10).during(30.seconds), // 10ユーザーが30秒間、分散してシナリオを実行 nothingFor(10.seconds), // 10秒一時停止 rampUsersPerSec(10).to(100).during(60.seconds) // 60秒間で同時シナリオ実行数を10回から100回まで増やす ) .protocols(httpProtocol) )
負荷をかける側のネットワーク帯域が足りずに十分なテストが行えないことがあるため注意してください。
どうでもいい話
Injectionとか注入とか聞くとなんとなく依存性の注入(Dependency Injection)とか想像してしまうのですが全く関係ないです。
Gatlingではシナリオにユーザーを注入するみたいな世界観なんですかね?
テストの実行
テストシナリオを作成したプロジェクトで Gatling/test
もしくは Gatling/testOnly テストクラス名
でテストを実行することができます。
sbt:gatling-template> projects [info] In file: [info] gatling [info] * root sbt:gatling-template> project gatling [info] set current project to gatling-template (in build file: ) sbt:gatling-template> Gatling/testOnly クラス名 ... 実行ログが出力 ... Reports generated in 0s. Please open the following file: file:///...hoge/fuga/gatling-playground/gatling/target/gatling/example6-20231109095951995/index.html [info] Simulation Example6 successful. [info] Simulation(s) execution ended. [success] Total time: 114 s (01:54), completed 2023/11/09 19:01:43 sbt:subProject>
テストが完了するとレポートがHTML形式で出力され、 Please open the following file:...
の行に表示されたパスに保存されています。
(デフォルトでは target/gatling
以下に出力されます。)
その他
- アプリケーションレイヤー、インフラレイヤーごとにログやメトリクスを収集できるようにしておくと問題やボトルネックの解消の糸口になるため収集することをおすすめします。
- パフォーマンステストを行う文脈を踏まえてシナリオを検討しましょう。
- 現実には起こり得ない負荷や頻度が少ないため問題になることは少ないなどといった無視しても良い結果が含まれることがあります。
- この記事ではテストの実行方法にかなりフォーカスしたものとなっています
- 「全体としてどのように進めていったら良いか?」という点に関して参考になった資料をこちらにリンクします。
- Performance Testing Guidance for Web Applications | Microsoft Learn)
- 性能テスト計画ガイド | Fintan