こんにちは、こんにちは。
これは「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のプラグインのデフォルトの設定では、プロジェクトの 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のよくあるテストクラスの形を示します。
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class Example1 extends Simulation {
val httpProtocol = http.baseUrl("http://localhost:9000").userAgentHeader("UserAgentHere")
val scn = scenario("シナリオ名").exec(
http("リクエスト名").get("/path").header("key", "value")
)
setUp(
scn
.inject(
rampUsers(10).during(30.seconds)
)
.protocols(httpProtocol)
)
}
これを覚えておけばOK! Gatling チートシート
Scenario
アプリケーション上でHTTPリクエストなどを伴う何らかの操作(シナリオ)を定義する仕組みです。
val scn1 = scenario("シナリオ例1")
複数の操作を繋げて書くこともできます。
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))
)
レスポンスボディを想定した場合
{
"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
)
.exec(
http("POSTの例2")
.post("/path")
.formParam("key", "value")
.asFormUrlEncoded
)
また、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),
nothingFor(10.seconds),
rampUsersPerSec(10).to(100).during(60.seconds)
)
.protocols(httpProtocol),
scn2
.inject(
rampUsers(10).during(30.seconds),
nothingFor(10.seconds),
rampUsersPerSec(10).to(100).during(60.seconds)
)
.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
以下に出力されます。)
その他
- アプリケーションレイヤー、インフラレイヤーごとにログやメトリクスを収集できるようにしておくと問題やボトルネックの解消の糸口になるため収集することをおすすめします。
- JVMで動作するアプリケーションの場合はJMXやJFRなどが存在します。
- パフォーマンステストを行う文脈を踏まえてシナリオを検討しましょう。
- 現実には起こり得ない負荷や頻度が少ないため問題になることは少ないなどといった無視しても良い結果が含まれることがあります。
- この記事ではテストの実行方法にかなりフォーカスしたものとなっています