Gatlingを使って負荷テストをしよう!

Masashi Yamamoto
Eureka Engineering
Published in
18 min readDec 20, 2019

この記事は「eureka Advent Calendar 2019」20日目の記事です。昨日の記事は Daichi Haradaによる EC2 Image Builder + ansible + awscli を用いたゴールデンAMIの更新/反映自動化の検討でした。

こんにちはこんにちは、猫がより平和に暮らせる世界を作りたい
@marnieです。

ここ最近はデータベースやウェブサーバーなどなどの負荷・性能をコードを改善したり、DB側をいじいじしたり、cache入れたりなどなど
性能改善をする日々を過ごしています。

Pairsの性能改善と猫の関連性について、首をかしげたかもしれませんね。
Pairsの安定稼働は巡り巡って、人の役に立ち、猫達の平和に繋がるはずなので問題ありません。
信じましょう。

さて、性能改善を行うためにミドルウェアやOSなどの実行環境を入れ替えてみた,etc…などなどを行なった際にそもそも性能はどのくらい改善したんだろう…?

など知りたい事はないでしょうか?

今回は、そんなあなたの助けになるGatlingというツールを使った負荷テストのやり方について簡単な紹介をしてみようと思います。

Gatling?

GatlingはJVM上で動作する軽量な負荷テストツールで、以下のような特徴があります。

  • 軽量
  • Scalaを使ってテストシナリオを記述できる
  • レポートの自動生成
  • recorderを使ってブラウザ操作からテストシナリオが自動生成が可能。

環境構築

Gatlingを動作させる環境を構築します 🐼

  • ダウンロード
curl -O https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/3.3.1/gatling-charts-highcharts-bundle-3.3.1-bundle.zip
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 — : — : — — : — : — — : — : — 0
100 60.1M 100 60.1M 0 0 33.4M 0 0:00:01 0:00:01 — : — : — 33.3M
  • 解凍
unzip gatling-charts-highcharts-bundle-3.3.1-bundle.zip

他にJDK8のインストールが必要なので、Oracleのサイトからダウンロードしてインストールをしてください。

バンドル版の場合はたったこれだけで完了です 🐱

ディレクトリ構成

実際に動かして見る前にディレクトリの構成を簡単にまとめてみました。

├── LICENSE
├── bin
│ └── gatlingやrecorderの実行ファイルが格納されている。
├── conf
│ └── 設定ファイル全般
├── lib
│ └── gatling自身が利用するjarファイルなど
├── results
└── テスト結果のログ、レポートの生成先
└── user-files
└── resources
│ └── search.csv
└── simulations
└── 実行するシュミレーション用のスクリプトを格納する場所

scalaでテストシナリオを記述する

まずはデフォルトで用意されている BasicSimulation.scala の中身を見て

空気感を掴みましょう 🐾

cd gatling-charts-highcharts-bundle-3.3.1/user-files/simulations/
cat computerdatabase/BasicSimulation.scala

中身はこんな感じです。

主だった設定部分に注釈をつけてみました。

package computerdatabase// 使用するライブラリのimport
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
// シュミレーションクラスを記述
class BasicSimulation extends Simulation {
// テストを行うために必要なhttp関連の設定オブジェクトを作成するval httpProtocol = http
.baseUrl("http://computer-database.gatling.io") // baseとなるUrlの指定。以降に扱うurlのrootになる。
.acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") // Accept Headerの設定
.doNotTrackHeader("1") // DNT Headerの拒否設定
.acceptLanguageHeader("en-US,en;q=0.5") // Accept-Language Headerの設定
.acceptEncodingHeader("gzip, deflate") // Accept-Encoding Headerの設定
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0") // User-Agent Headerの設定
// テストシナリオオブジェクトを作成
val scn = scenario("Scenario Name") // A scenario is a chain of requests and pauses
.exec(
http("request_1") // リクエストにラベリングする名前。後のテスト結果・レポートなどで利用するだけなので好きな名前をつければよい。
.get("/") // http://computer-database.gatling.io/ にGETリクエストを投げる
)
.pause(7) // 7秒間停止する
.exec(
http("request_2")
.get("/computers?f=macbook") // http://computer-database.gatling.io/ にGETリクエストを投げる
)
.pause(2)
.exec(
http("request_3")
.get("/computers/6")
)
...(中略)
.pause(670 milliseconds)
.exec(
http("request_6")
.get("/computers?p=2")
)
.pause(1)
...(中略)
.exec(
http("request_10")
.post("/computers") // POSTリクエスト
.formParam("name", "Beautiful Computer") // 送信するフォームデータを定義
.formParam("introduced", "2012-05-30")
.formParam("discontinued", "")
.formParam("company", "37")
)
// セットアップ処理
// テストシナリオ(scn) を 先に定義したhttp設定(httpProtocol)を使って1回だけ1並列で実行する。
setUp(scn.inject(atOnceUsers(1)).protocols(httpProtocol))

大雑把な流れとしては、以下を記述するような形となります。

  • シュミレーションクラスの定義
  • url,headerなどテストに使用するhttp関連の設定をするオブジェクトの定義
  • テストシナリオオブジェクトの定義
  • setUp処理の呼び出し

BasicSimulation.scalaをコピーして簡単なテストを作ってみます。

mkdir example
cp computerdatabase/BasicSimulation.scala example/ExampleSimulation.scala
vim example/ExampleSimulation.scala

内容を編集して以下のようにします。

package example
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class ExampleSimulation extends Simulation {val httpProtocol = http
.baseUrl(“http://テストしたいサイトドメイン”)
.acceptHeader(“text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8”)
.doNotTrackHeader(“1”)
.acceptLanguageHeader(“en-US,en;q=0.5”)
.acceptEncodingHeader(“gzip, deflate”)
.userAgentHeader(“Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0”)
//7秒おきに /xxx /xxx2 /xxx3にGETするだけの簡単なテスト
val scn = scenario(“example”)
.exec(
http(“request_1”)
.get(“/xxx”)
)
.pause(7)
.exec(
http(“request_2”)
.get(“/xxx2”)
)
.pause(7)
.exec(
http(“request_3”)
.get(“/xxx3”)
)
setUp(scn.inject(atOnceUsers(100)).protocols(httpProtocol)) //100Userで並列実行
}

Gatling発射(テスト実行)

さて、ようやく弾(テスト)の装填もできたので、早速Gatlingを発射してみましょう 。さぁパーティの始まりだァ! 😎

cd ..//bin/gatling.sh
GATLING_HOME is set to xxxx/test/gatling/gatling-charts-highcharts-bundle-3.3.1
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] example.ExampleSimulation
6 //作成したテストを選択
Select run description (optional)
example:cat:
Simulation example.ExampleSimulation started…
================================================================================
2019–12–18 19:53:29 5s elapsed
— — Requests — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —
> Global (OK=200 KO=0 )
> request_1 (OK=100 KO=0 )
> request_1 Redirect 1 (OK=100 KO=0 )
— — Scenario Name — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — -
[ — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — ] 0%
waiting: 0 / active: 100 / done: 0
================================================================================
.......Reports generated in 0s.
Please open the following file: /xxxxx/test/gatling/gatling-charts-highcharts-bundle-3.3.1/results/examplesimulation-20191218105323158/index.html

無事実行されていれば、最後にレポートが作成された旨が出力されていますので、レスポンスタイムの分布などの統計情報を含むレポートがhtml形式で出力されます。便利。

今回はサンプルレベルですが、多機能なので、テストシナリオやセットアップ時の指定オプションを変更すれば、より高度なテストをする事も可能です。

例えば

.exec(
http("request_2")
.get("/xxx")
.check(status.is(200))
)

のように書くと特定ステータスだけをOKに分類したりする事もできますし、特定の結果が含んでいる場合のみを成功とみなすような書き方もできます。

また、実行数や並列での負荷のかけ方についても以下のように変更する事で柔軟な変更が行えます。

atOnceUsers(100) // 今回指定したタイプ 100回一気に起動する
constantConcurrentUsers(10) during (10 seconds), // 10秒間10Userを維持
rampConcurrentUsers(10) to (20) during (10 seconds) // 10秒かけて10User -> 20Userに増やす

詳細な設定や記法などは公式サイトのセットアップの説明ページ

チートシートが参考になります。

また、user-files/simulations/computerdatabase/advanced/配下にもサンプルがありますので、雰囲気を掴みたい時はサラっと見てみると良いと思います 😸

Recorderを使って操作からテストシナリオを生成する

とはいえ、scala書くのつらいよ…..もっと簡単にできないの?

と言う方向けに、Recorderというブラウザ操作からテストシナリオを生成できる機能があります。

bin/recorder.sh
GATLING_HOME is set to xxxx/test/gatling/gatling-charts-highcharts-bundle-3.3.1

GUIが立ち上がるので、startを押します。

RecorderがProxyとなって通信を記録してくれるので、利用しているPCと

ブラウザの設定を変更してHTTP/HTTPS通信がRecorderのPortを向くように設定してあげてください。

(上の例だとlocalhost:8000を指定する)

ブラウザ上で操作を行い、Stopするだけで以下のようなテストファイルが出来上がります。

cat user-files/simulations/RecordedSimulation.scalaimport scala.concurrent.duration._import io.gatling.core.Predef._
import io.gatling.http.Predef._
import io.gatling.jdbc.Predef._
class RecordedSimulation extends Simulation {val httpProtocol = http
.baseUrl("http://xxxx")
.inferHtmlResources()
.acceptEncodingHeader("gzip, deflate")
.acceptLanguageHeader("ja,en-US;q=0.9,en;q=0.8")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36")
val headers_0 = Map(
"Accept-Encoding" -> "gzip, deflate",
"Pragma" -> "no-cache",
"Proxy-Connection" -> "keep-alive")
val headers_2 = Map(
"Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Sec-Fetch-Mode" -> "navigate",
"Sec-Fetch-Site" -> "none",
"Sec-Fetch-User" -> "?1",
"Upgrade-Insecure-Requests" -> "1")
val headers_3 = Map(
"Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Sec-Fetch-Mode" -> "navigate",
"Sec-Fetch-Site" -> "same-site",
"Sec-Fetch-User" -> "?1",
"Upgrade-Insecure-Requests" -> "1")
val uri1 = "https://xxxx.com"
val uri2 = "https://xxxxx.com"
val scn = scenario("RecordedSimulation")
.exec(http("request_0")
.get("/generate_204")
.headers(headers_0))
.pause(15)
.exec(http("request_1")
.get("/generate_204")
.headers(headers_0))
.pause(5)
.exec(http("request_2")
.get(uri2 + "/")
.headers(headers_2))
.pause(9)
.exec(http("request_3")
.get(uri1 + "/")
.headers(headers_3)
.check(status.is(302)))
setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)
}

BlackList/WhiteListを指定しておけば余計な通信を捕まえずに済みます。

scalaでの記述をガリガリ頑張るのが億劫な方はRecorderでまずテストシナリオを生成して、そこから変更を加える方が捗るかと思います 🐼

まとめ

簡単に、負荷テストツールであるGatlingの使い方を紹介してみました :)

最終的に本番の負荷をより実現していきたいとなってくると、本番で発生しているリクエスト分布や頻度にどれだけ近づけられるか?という課題とぶつかり、シナリオ構築の難しさと戦う事になると思います。

もし本当にそのようなテストがしたい場合はシナリオベースというよりもshadowProxyのような仕組みを導入してテスト環境にリクエストを流すような方が楽かもしれません。

ただ、多くの場合、そこまでのテストをしたいわけではなく、ボトルネックの抽出のために特定の処理を並列・連続させてみたい、とか例えば単純なベンチーマーク的な検証をしたい事の方が多いと思いますので、そう言ったシーンでは有効に活用できると思います 😃

注意として高頻度で大量のテストを行う際は、外部ホスティング環境の場合、内容によってはDDoSなどの攻撃として検知されて通信がブロックされてしまう可能性もありますので、その辺は事前に確認しておく方が良いと思います。

簡単な負荷試験をすぐに始められるのが良いところだと思いますので、是非使って見てください!

それでは良い年末を :)

--

--

Masashi Yamamoto
Eureka Engineering

猫と猫が平和に暮らす社会の実現のために働いているエンジニアです。主にバックエンドアプリケーションとインフラのお世話を趣味にしています。趣味はロケ地を調べる事です。