負荷測定ツールGatlingで負荷テストをする

こんにちは。プロダクト事業部のしげぞうです!

前回は、【PostgreSQL】一括でデータを入出力できるCOPYコマンド について書きました!
こちらの記事についても、機会がありましたら読んでいただけると幸いです。

最近、負荷測定ツール Gatling を使って、現在開発中のアプリケーションの負荷テストを行いました。その時に実施したテストシナリオの作成や実際の負荷計測時にハマった点などをまとめてみました。

そもそもGatlingとは

Gatling は、オープンソースのWebアプリケーション負荷測定ツールです。

似たようなツールとしては、誰でも一度は聞いたことのある Apache JmeterPython でテストシナリオを作成する Locust があります。

今回の負荷測定のシナリオは、 特定のAPIを一定の間隔でコールする という非常にシンプルなものでした。

そこで今回は、 Scala 言語でシンプルにシナリオを作成できる Gatling を採用しました。

環境構築

Gatling を動かすには、JDKが必要となります。公式によれば、OpenJDK 8とOpenJDK 11の2つをサポートしているとのこと。

作業PCにインストールしているJDKのバージョンがこれらと違ったため、今回は Docker で、Gatling実行環境を整備することにしました。

Docker での実行環境構築については、こちら をご覧いただければ幸いです。

テストシナリオの作成

Apache Jmeter では、GUI上でスレッドグループ内のサンプラーやロジックコントローラを組み合わせていくことで、シナリオを作成していくのですが、 Gatling は、 Scala 言語でシナリオを作成していきます。

以下、テストシナリオ(testScenario.scala)のサンプルです。

import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class testScenario extends Simulation {

  // httpの設定
  val httpProtocolConf = http
    .baseUrl("https://example.jp") // ベースURL
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") // リクエストヘッダー
    .basicAuth("xxxxxxxxxx", "yyyyyyyyyy") // Basic認証
    .doNotTrackHeader("1") // DNTヘッダー
    .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0") // ユーザーエージェント

  // シナリオの記述
  val scn = scenario("シナリオA")
            // 1つ目に実行されるAPI
            .exec(
              http("API-1") // リクエスト名称
              .post("/xxxxx/auth") // POSTメソッドとURIの指定
              .formParam("loginId", "1a2b3c4d5e") // リクエストパラメータの指定
              .formParam("loginPassword", "abcdefg12345") // リクエストパラメータの指定
              .check(status.is(200)) // ステータスのチェック
              .check(jsonPath("$.contents.accessToken").saveAs("accessToken")) // レスポンスのBodyの取得
            )
            // デバッグとして、取得したアクセストークンを出力
            .exec(session => {
                 val accessTokenParam = session("accessToken").asOption[String]
                 println("accessToken:"+ accessTokenParam)
                 session
            })
            // 1秒スリープ
            .pause(1)
            // 2つ目に実行されるAPI
            .exec(
              http("API-2")
              .post("/xxxxx/get/userData")
              .formParam("accessToken", "${accessToken}")
              .check(status.is(200))
            )
  // 実行
  setUp(
    // ユーザー数や実行期間を設定
    scn.inject(
      constantUsersPerSec(5) during(1800 seconds)
    ).protocols(httpProtocolConf)
  )
}

シナリオは以下の流れで書いていきます。

  1. Httpリクエストの設定
  2. 実行する処理の記述(シナリオ)
  3. シナリオの実行

Scala 言語は未経験だったのですが、シナリオの構成がシンプルなため、割とスピーディーにシナリオを書いていくことができました!!

。。。と言いましても、所々、個人的にハマったところがあったので、以下で紹介します。

レスポンス結果の取得について

シナリオを作成する際、 「1APIのレスポンス結果を取得して、次に呼ぶAPIのパラメータに指定したい」 といったケースが出てくると思います。

レスポンスBodyがJSON形式である場合は、 checkjsonPathsaveAs を使用してデータを取得します。

例えば、こんなJSON形式のBodyが返却される場合、、

{
 "userLastLoginDate" : "2020-12-01 10:00:00", // 1
 "contents" : { 
   "accessToken" : "abcdefghijklmn" // 2
 },
 "userInfo" : [
     {
         "address1" : "Tokyo",
         "address2" : "Shibuya" // 3
     },
     {
         "address1" : "Osaka",
         "address2" : "Umeda"
     }
 ]
}

それぞれのデータは、以下の形式で取得することができます。

// 1の値を取得する場合
check(jsonPath("$.userLastLoginDate").saveAs("userLastLoginDate"))

// 2の値(オブジェクト形式の値)を取得する場合
check(jsonPath("$.contents.accessToken").saveAs("accessToken")) // ピリオド(.)でつないで値を指定

// 3の値(配列形式の値)を取得する場合
check(jsonPath("$.userInfo[0].address2").saveAs("address2")) // 配列のindex番号を指定

取得したデータは、セッションに保存されています。セッション内のデータは、 "${xxxxx}" の形式でアクセスすることができます。

.exec(
  http("API-2")
 .post("/xxxxx/get/userData")
 .formParam("accessToken", "${accessToken}")
 .check(status.is(200))
)

シナリオ作成時、こちらの欲しい値が正しく取得できておらず、不適当なパラメータがAPIに渡されており、原因不明のエラーステータスが返却。。

レスポンス結果をパラメータに使用する際は、値のデバッグをしながら確実に記述ミスを減らしていきましょう!

負荷の種類について

システムに負荷をかけるといっても、様々な負荷のかけ方があります。Gatling公式 では、2種類の負荷モデルについてサポートしています。

モデル名 概要 負荷イメージ(例)
Open Workload Model システムに対して複数のリクエスト同時アクセスさせるような負荷のかけ方 1秒あたり15人のユーザーがサーバにアクセスしてくる状況
Closed Workload Model システムの処理可能上限最大に達した状態をキープしつづけるような負荷のかけ方 常に10ユーザーがサーバにアクセスしている状況

システムやサービスによって、想定される負荷や満たすべき要件は異なってきます。

満たすべきシステムの稼働状況をあらかじめ洗い出し、テスト計画を作成していくことが重要になりますね。

最後に Open Workload ModelClosed Workload Model それぞれのシナリオ例を紹介します。

constantUsersPerSec(rate).during(duration)

こちらは、 Open Workload Model で、 指定した期間(duration) において、 1秒間に指定した割合(rate) でユーザーが立ち上がり、サーバにアクセスするシナリオになります。

例えば、 constantUsersPerSec(0.5).during(30 seconds) だと、

「30秒間、1秒当たり0.5ユーザーの割合でユーザーが立ち上がり、サーバーにアクセスする」というシナリオを表現します。

負荷測定後は、測定結果を下記のような html 形式のサマリでまとめてくれます。( result ディレクトリ配下に出力される)

レスポンスタイムのMax値、Min値などを処理ごとにまとめてくれるので、結果が見やすく、分かりやすいです!

シナリオ実行中に起動したユーザー数の推移(Active Users along the Simulation)を見てみると、設定したシナリオ通りに動いていることが確認できます。(今回の場合、30秒の間、1秒当たり0.5ユーザーの割合でユーザーが起動するケース)

constantConcurrentUsers(nbUsers) during(duration)

一方こちらは Closed Workload Model で、 指定した期間(duration) において、 同時指定のユーザー数(nbUsers) まで立ち上がり、サーバにアクセスするシナリオになります。

例えば、 constantConcurrentUsers(5).during(30 seconds) だと、

「30秒間、5ユーザーが常に立ち上がった状態を維持し、サーバーにアクセスする」という風なシナリオになります。

constantUsersPerSec(rate).during(duration) と同様に、シナリオ実行中に起動したユーザー数の推移(Active Users along the Simulation)を見てみると、設定したシナリオ通りに動いていることが確認できます。(30秒の間、5ユーザーが起動している状態を維持する)

終わりに

今回は、負荷測定ツール Gatling を利用した際に得た知見やハマったところについて書いてみました。

前述でも述べましたが、負荷といっても、様々な負荷のかけ方が存在するため、 想定される負荷に適したシナリオの作成ツールの選択 が必要だなあと今回の業務経験で実感しました。

まだまだ Gatling の機能を使い倒せていないので、何か便利な機能や面白い機能等見つけたら、また書いてみたいなと思います。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です