Gatlingを使ってDB単体負荷試験を改善した話

# はじめに

こんにちは、この記事は DBRE Advent Calendar 2024 20日目の記事です。

株式会社ZOZOの @tenn25(てん) と申します。

普段はZOZOTOWNのSREとしてマイクロサービスの構築・改善に携わっています。
また、ワーキンググループという形でDBREチームにも所属しています。

弊社DBREについては以下のテックブログや、同じ DBRE Advent Calendar 2024 15日目の記事も合わせてご覧ください！

https://techblog.zozo.com/entry/dbre-wg

https://qiita.com/takumi_links/items/07d8c6da4fb1fbb2c07e

# Gatlingを使ったアプリケーション負荷試験

現在、レガシーなオンプレミス環境からクラウド上のマイクロサービス環境へのリプレイスを進めており、マイクロサービスの負荷試験には [Gatling](https://gatling.io/) を利用しています。


また、Gatling を Kubernetes クラスター上で稼働できるようにした [Kubernetes Operator](https://github.com/st-tech/gatling-operator) を社内で開発しており、OSSツールとして公開しています。

こちらの開発経緯や構成については以下のブログをご参照ください。

https://techblog.zozo.com/entry/gatling-operator-introduction


# DB負荷試験における課題

弊社ではオンプレミス環境のデータベースとしてSQL Serverを多く利用しており、SQLの試験にはSQL Server用のRML UtilsであるOstressというツールを使用していました。

https://learn.microsoft.com/ja-jp/troubleshoot/sql/tools/replay-markup-language-utility#using-ostressexe-to-stress-test-a-query


このツールでもSQLの負荷試験はできますが、以下のような課題がありました。

- スケールできない
    - オンプレミスのサーバーに配置して利用する必要がある
    - 負荷をかけすぎるとツールが固まる
- 動的なクエリの実行が難しい
    - 固定のクエリを実行することしかできず、動的に実行するためにはかなりの工夫が必要
- 複雑なシナリオを書くことができない
    - 固定のSQLを実行するだけなので柔軟性がない
- 実行結果がわからない
    - レポート機能が充実しておらず、別途可視化が必要
- SQLServer以外のDBに対する負荷試験方法が確立されていない

そこで、SQLの性能テストやDBの負荷試験にも Gatling を使うことで上記の課題が解決しないかと考えました。
Gatlingであればスケーラビリティとシナリオの柔軟性があり、これらの課題に対応できます。


# Gatling JDBC Pluginとは

Gatling JDBC Pluginは、Gatlingのサードパーティープラグインです。

[公式サイト](https://docs.gatling.io/reference/integrations/third-parties/) のプラグイン一覧にも載っており、GatlingでJDBCを利用する場合はこれを利用することになります。

https://github.com/Tinkoff/gatling-jdbc-plugin

最近リポジトリが変わったようで、今後は以下がメンテされそうです。

https://github.com/galax-io/gatling-jdbc-plugin

# 使い方

GatlingのシナリオはScalaやJavaで書くことができ、プロジェクトをsbtで管理する場合は`build.sbt`に追加することで利用できます。

```scala
libraryDependencies += "ru.tinkoff" %% "gatling-jdbc-plugin" % <version> % Test
```


弊社では Gatling Operator によって kubernetes 上で稼働させており、Docker Imageをビルドする際に .jar ファイルを直接ダウンロードしています。
Dockerfile内で以下のように指定しています。

```Dockerfile
 wget -q -O /opt/gatling/lib/mssql-jdbc-12.2.0.jre8.jar \
 https://repo1.maven.org/maven2/com/microsoft/sqlserver/mssql-jdbc/12.2.0.jre8/mssql-jdbc-12.2.0.jre8.jar && \
 wget -q -O /opt/gatling/lib/gatling-jdbc-plugin_2.13-0.10.4-RC1-it.jar \
 https://repo1.maven.org/maven2/ru/tinkoff/gatling-jdbc-plugin_2.13/0.10.4-RC1/gatling-jdbc-plugin_2.13-0.10.4-RC1-it.jar
```

あとはシナリオ内で以下のようにimportすることで利用できます。
```scala
import ru.tinkoff.load.jdbc.Predef._
import ru.tinkoff.load.jdbc.actions.actions._
import ru.tinkoff.load.jdbc.protocol.JdbcProtocolBuilder
```

## 1.DBへの接続

DBへの接続情報の定義が必要です。
Gatling Operator であればKubernetesのSecretsを参照できるので便利です。

最大プールサイズやタイムアウト時間を設定できます。

```scala
  val dataBase: JdbcProtocolBuilder = DB
    .url(dbUrl)
    .username(dbUser)
    .password(dbPassword)
    .maximumPoolSize(1000)
    .connectionTimeout(1.minute)
```

## 2.パラメーターの準備

Gatling では Feeder を使うことで、パラメータを変えながら大量にSQLを実行することができます。
Feederには CSV Feederや JDBC Feeder などが存在します。

https://docs.gatling.io/reference/script/core/session/feeders/

ここでは JDBC Feeder を使って、データベースからパラメータを動的に取得してみましょう。
JDBC Feeder は JDBC Plugin の機能ではなく、 Gatling 自体に備わっています。

以下はSQL Serverでのクエリ例です。
パラメータ元となるテーブルからランダムに5000件取得しています。
`TABLESAMPLE`は厳密なランダムではないですが擬似乱数を使ってデータを取得することができるため軽量で便利です。

```scala
  val user_ids = jdbcFeeder(
    dbUrl,
    dbUser,
    dbPassword,
    """
      |WITH SampledEntities AS (
      |  SELECT id, user_id
      |   FROM EntityTable
      |   (5000 ROWS)
      |)
      |SELECT TOP 5000 S.id, S.user_id, D.detail_id
      |FROM SampledEntities AS S
      |INNER JOIN EntityDetails AS D
      |  ON S.id = D.entity_id;
    """
  )
```

これをシナリオファイルの始めに定義することで、シナリオ開始時にランダムなデータを取得し、変数`user_ids`に格納します。

CSVファイルでFeedを用意することもできますが、JDBC Feederを使うことで以下のメリットがあります。

- 手作業でCSVファイルを用意しなくて良い
    - シナリオ実行時に自動で取ってきてくれる
- 実行時のデータベースに存在する最新データでFeedを作ることができる
    - データベース上に存在しないidでないとシナリオが失敗する場合などに有用
- CSV変更の度にimageを再ビルドしなくて良い
    - パラメータを変えながら何度もシナリオを実行する場合にimageの再ビルドが手間になる
    - Feed用のテーブルをDB上に用意することで再ビルドせずにパラメータを変えることができる

また、本来の使い方とは違いますが、JDBC Feederはシナリオ開始時に１度だけ実行されるSQLです。
DBのキャッシュクリアなど、実行前に必ず実行しておきたい準備があれば、Feederとして定義することで手動実行の手間がなくなるでしょう。



## 3.SQL実行

以下のサンプルが参考になります。

https://github.com/galax-io/gatling-jdbc-plugin/blob/main/src/test/scala/org/galaxio/performance/jdbc/test/cases/Actions.scala


例えばSELECT文を実行するリクエストは以下のように書くことができます。


```scala
setUp(
  scenario("TEST")
    .feed(user_ids.random) //リクエストの度にFeederからランダムで取得
    .exec(
      jdbc("SELECT TEST")
        .queryP(
          """
            |SELECT id, name
            |FROM TEST_TABLE
            |WHERE id = {user_id}
          """.stripMargin
        )
        .params("user_id" -> "#{user_id}") //Feederから取得されたuser_idを使う
        .check(
          simpleCheck(_.nonEmpty),  // 結果が空でないかをチェック
          allResults.saveAs("result")    // SELECT結果をセッション変数resultに保存
        )
    )
    .inject(constantUsersPerSec(10) during (120 seconds))
    .protocols(dataBase) //上で定義したDB接続情報
)

```

ここではFeederで取得した`user_id`をクエリパラメータに利用しています。

パラメータ化クエリの場合は `.queryP()` を使うことを推奨します。
`.query()` も使うことができますがアドホッククエリとして実行されるため、変数部分が固定値として認識され、リクエストごとにクエリのコンパイルが走るため注意して下さい。
クエリ内のパラメータ値 `#{user_id}` はGatlingのセッション変数を参照しています。

## 4.SELECT結果を後続のリクエストで使う

上記の例では`allResults.saveAs("result")`としていますが、結果はセッション変数resultに保存されています。
以下のように後続でresultから任意の値を取り出しSQL実行でパラメータとして利用できます。

```scala
exec(session => {
  val results = session("result").as[Seq[Map[String, Any]]]
  val headName = results.headOption.flatMap(_.get("name"))
  session.set("headName", headName.getOrElse(0)) // 取得したnameを新しい変数に設定
})
.exec(
  jdbc("Next Query")
    .queryP("SELECT * FROM ANOTHER_TABLE WHERE name = {headName}")
    .params("headName" -> "#{headName}") // 保存した変数を利用
)
```


# まとめ

Gatling Operator 及び Gatling JDBCを使うことで負荷試験の柔軟性、スケール性、効率性が大幅に上がりました！

はじめに挙げた以下の課題が解決されました。

- スケールできない
    - →Gatling Operatorを使うことでKubernetesのスケーラビリティを享受できるようになりました
- 動的なクエリの実行が難しい
    - →JDBC Feederによって動的なパラメータクエリが簡単に実行できるようになりました
- 複雑なシナリオを書くことができない
    - →ScalaやJavaなどの開発者フレンドリーな言語で柔軟にシナリオが組めるようになりました
- 実行結果がわからない
    - →Gatlingのレポート機能によって実行結果が把握できるようになりました
- そもそもSQLServer以外のDBに対する負荷試験方法が確立されていない
    - →JDBCなので主要なRDBMSへは同じ方法で接続が可能　MySQLの負荷試験もできるようになりました


DBREは、データベースの信頼性に責任を持ち、データベースにおける諸問題を解決・改善することで、サービスの拡大や開発に支障が出ないようにすることが使命だと考えています、
使いやすい負荷試験ツールを導入することで、今までより気軽にDB負荷試験を実施できるようになりました。
これによりSQLの改善や問題発見に繋がりデータベースの信頼性向上に繋がることを期待しています。