ご機嫌よう、ガラパゴスのおとめです。
先日出社したら机に「Amazon Web Services 負荷試験入門 ークラドの性能の引き出し方がわかる」という本がおもむろに意味ありげに置かれていました。
さて、負荷試験と言ったらまず攻撃ツールに考えが及びますね? たとえばJMeterとかが有名どころかしらん? 先述の本には以下の攻撃ツールが掲載されています。
Erlangで書かれたTsungが掲載されているのが興味深いところですが、 ガラパゴスではwrkやGatlingが使われたりしています。
そこで今日はGatlingについてお話してみようと思います。
なぜGatling?
例えばJMeterでシナリオテストを作成する時はGUIで操作したりするかと思います。保存するとXMLが吐き出されましたね?
でもシナリオが複雑になると、ちまちまGUIで設定するのが面倒だったり、引き継いだ場合など−−数年前に自分で書いたシナリオを再び引っ張り出してきた場合もそうですが−−前任者の真心が読み取りにくかったりしますね? さらに (これは個人的事情ですが) ガラパゴスのおとめにはXMLに対する脆弱性があって、ちょっとシナリオを編集しようと思ってもXMLを見ると気絶してしまいます。
そういうことがあって、できればシナリオもリーダブルかつライタブルであって欲しいというお気持ちが溢れ出てきます。そこで何がリーダブル/ライタブルなのかというお話になるのですが、簡単簡潔なDSLとかでコードにそのまま書いてあるといいな……でも一方で負荷はちゃんとかけて欲しいですね。
それGatlingでできます。
使ったことはありませんがLocustもシナリオの記述がPythonでそのまま書くだけのようなのですが、Pythonかあ……。
実行環境の準備
GatlingはScalaで動いて、ScalaはJVMで動きますので、環境に合わせてこれらをインストールしましょう。Gatlingそのものは公式からダウンロードしたzipを適当な場所に展開しておけば良いです。
えっScalaなの? と身構える必要はありません。
テストを書いてみる
簡単なシナリオを書いてみる
先ほど簡単簡潔なDSLでコードにそのまま書いて、と書きました。シナリオをちょっと見てみましょう。シナリオそのものの記述はこのようになります。
val scn = scenario("シナリオの名前をお好きに ") .exec(http("リクエストの名前をお好きに") .get("/url") .check(status.is(200))) .pause(1) // 次のリクエストまでの待ち時間 .exec(http("次のリクエストの名前をお好きに") .post("/other/url") .body(StringBody("""{"json":"body"}""").asJSON) .check(statsu.is(201))) .check(jsonPath($.something[0].json))) .pause(1) .exec(... // 以下続く
何やらGETして、その直後に何やらPUTして、結果を確認していることが読み取れます。しかもとても読みやすいですね!
テスト実行部分はこのようになります。
object TestA { val scn = scenario("...") .exec(... // (ry ... } val testA = scenario("scenario a").exec(TestA.scn) val httpConf = http.baseURL("http://exapmle.com") setUp(testA.inject(atOnceUsers(10))).protocols(httpConf)
いっぺんに10アクセスすることが読み取れますね。
ヘッダを指定してみる
もちろんリクエストヘッダも簡単に設定できます。例えば何かの認証ヘッダを設定してみましょう。
val header = Map("auth" -> "header", "and" -> "something") val scn = senario("なまえ") .exec(http("(ry") .get("/some/url") .headers(header))
レスポンスの値を利用してみる
ここではレスポンスボディがJSONの場合を例にしますが、JSONパスを指定してセッション変数にしまっておいて、後で使うことができます。
.exec(http("something post") .post("/url") .body(StringBody("""{"json":"body"}""").asJSON) .check(statsu.is(201))) .check(jsonPath($.something[0].id).saveAs("some_id")) .exec(http("something get") .get("/url/" + "${some_id}") .check(status.is(200)))
セッション変数には${variable_name}
のようにアクセスします。
変数を使ってみる
さて、ここまでお読みになられたら普通のScalaな変数も簡単に使えそうな気がしますね。例えばこんな風に。
val foo = "bar" ... .exec(http("simething get") .get("/url/" + foo) .check(status.is(200)))
もちろんなんの問題もありません。また、独自の値をセッションに変数を入れて使うこともできます。
val my_session_vals = exec({_.setAll(("foo", "bar"))}) ... .exec(my_session_vals) .exec(http("simething get") .get("/url/${foo}") .check(status.is(200)))
複数のシナリオを同時に実行する
もちろん複数シナリオを同時に実行することもできます。setUp
に並べて書くだけというお手軽さです。
object TestA { val scn = exec(... // 省略 } object TestB { val scn = exec(... // 省略 } val testA = scenario("scenario a").exec(TestA.scn) val testB = scenario("scenario b").exec(TestB.scn) setUp(testA.inject(atOnceUsers(10)), testB.inject(atOnceUsers(10))).protocols(httpConf)
負荷のかけ具合を調整する
setUp
に書いてあるtestA.inject(atOnceUsers(10))
はいっぺんに10アクセスして、それだけでおしまいですが、これはもちろん調整できます。
例えば以下のような設定ができます。
atOnceUsers(n)
:n ユーザーが一度に。rampUsers(n) over(t)
:t 秒かけて n ユーザーに線形で増加。constantUsersPerSec(n) during(t)
:t 秒間 n ユーザーをキープ。rampUsersPerSec(n) to(m) during(t)
:t 秒かけて n ユーザーから m ユーザーに線形で増加。heavisideUsers(n) during(t)
:ヘヴィサイドの階段関数を使って t 秒間で n ユーザーになるまで段階的に増加。splitUsers(n) into(s) separatedBy(t)
:t 秒ごとに n ユーザーが s を繰り返す。
Feederを使う
API1とAPI2の呼び出し間隔をランダムにしたり、ランダムな値を設定したりするには、Feederを使ってセッションに値を渡すと良いかもしれません。以下の例では、${key}
というセッション変数で1から10の数値がランダムに取れます。
val feeder = (1 to 10) .toArray .map(n => Map("key" -> n)) .random val scn = // 中略 .feed(feeder) .pause("${key}") ... // 以下略
もちろんこの簡単な例では、そんなの普通に乱数にすればいいような気もしますが、Feederが威力を発揮するのは、例えばCSVから読み込んで使いたい時です。
例えば、tokenという列に何かテスト用の認証情報が、idには対応するIDが書いてあるCSVを使って、IDのパスにアクセスするテスト、のようなことができます(CSVに列名は必須です)。
val feeder = csv("/path/to/csv.csv").random val numOfFeeds = feeder.records.length val scn = // 中略 .feed(feeder) .exec(http("test") .headers(Map("auth_token": "${token}")) .get("/path/${id}") .check(status.is(200)))
ところでFeederを使ってテストしていると、以下のようなエラーが出ることがあります。
Exception in thread "main" java.lang.IllegalStateException: Feeder is now empty, stopping engine
Feederは単なるIterableで、内部的には.next
して次の値を取っているだけですので、全部を使い切ってしまうとこのエラーになります。つまり負荷に対して用意した値が少なすぎるということですね。Feederを使い切らないようにテストを見直しましょう。
テストしてみる
テストはコンソールから実行します(テストコードを/path/to/gatling/user-files
にコピーする必要があります)。gatlingを展開したパスに移動して、bin/gatling.sh
を実行します。コンパイルエラーがなければ、作成したテストが[6]あたりに出てきます(1〜5はデフォルトで入っているテストです)。
$ cd /path/to/gatling $ bin/gatling.sh GATLING_HOME is set to /*****/gatling/gatling-charts-highcharts-bundle-2.3.0 Choose a simulation number: [0] computerdatabase.BasicSimulation [1] computerdatabase.advanced.AdvancedSimulationStep01 [2] computerdatabase.advanced.AdvancedSimulationStep02 [3] computerdatabase.advanced.AdvancedSimulationStep03 [4] computerdatabase.advanced.AdvancedSimulationStep04 [5] computerdatabase.advanced.AdvancedSimulationStep05 [6] **********.*********
数字を選ぶとテストが始まります。
テスト結果を見てみる
テストするとコンソールにも概要が表示されます(一部伏せさせていただきます)。
================================================================================ 2017-11-22 17:04:47 17s elapsed ---- Requests ------------------------------------------------------------------ > Global (OK=40 KO=0 ) > ********** (OK=10 KO=0 ) > ********** (OK=10 KO=0 ) > ********** (OK=10 KO=0 ) > ********** (OK=10 KO=0 ) ---- ********** load test ---------------------------------------- [##########################################################################]100% waiting: 0 / active: 0 / done:10 ================================================================================ Simulation **********.********** completed in 16 seconds Parsing log file(s)... Parsing log file(s) done Generating reports... ================================================================================ ---- Global Information -------------------------------------------------------- > request count 40 (OK=40 KO=0 ) > min response time 29 (OK=29 KO=- ) > max response time 180 (OK=180 KO=- ) > mean response time 70 (OK=70 KO=- ) > std deviation 32 (OK=32 KO=- ) > response time 50th percentile 69 (OK=69 KO=- ) > response time 75th percentile 87 (OK=87 KO=- ) > response time 95th percentile 111 (OK=111 KO=- ) > response time 99th percentile 163 (OK=163 KO=- ) > mean requests/sec 2.353 (OK=2.353 KO=- ) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms 40 (100%) > 800 ms < t < 1200 ms 0 ( 0%) > t > 1200 ms 0 ( 0%) > failed 0 ( 0%) ================================================================================
が、素敵なことに詳細な結果レポートがHTMLで出力されます。
例えば統計情報は綺麗なグラフで表示されます(この画像にはありませんが、NG分は目立つ赤で表示されます)。
レスポンスタイムの推移などもみれますし、カーソルを合わせると詳細を見ることもできます。
同様にユーザー数の推移やリクエスト・レスポンスの推移なども見ることができます。
これらの情報は、全リクエストの統計でも見ることができますし、exec
ブロックごとに見ることもできます。
もちろん、この結果レポートでわかるのは全体のレイテンシだけですので、プロファイリングなどは適切に行なっていると良いですね。
突然ですがガラパゴスでは負荷試験にも覚えのあるエンジニアを絶賛募集中です。ご興味をお持ちの方はぜひ弊社の採用ページをご覧ください。
では、ご機嫌よう。
--
この記事は業務の一環として業務時間中に書きました。