Привет! Меня зовут Александра, я работаю в отделе тестирования производительности Тинькофф. Этот текст — часть цикла статей, посвященных тестированию производительности с помощью инструмента Gatling. В предыдущей статье мы с командой рассказали о работе Gatling с HTTP. Еще мы написали вводную статью, из которой можно узнать, что такое Gatling и как мы его используем. В этой статье мы поговорим о работе Gatling с протоколом JDBC.

Дисклеймер

Для написания скриптов по протоколу JDBC мы будем использовать версию Gatling 3.6. В версии 3.7 зафиксировали баг, который не позволяет работать по JDBC-протоколу.

Работаем в Gatling с JDBC

JDBC (Java DataBase Connectivity) — стандарт взаимодействия Java-приложений с системами управления базами данных. Он основан на драйверах, которые позволяют подключаться к базам данных по их DSN (Data Source Name).

Для работы с базами данных команда тестирования производительности Тинькофф написала плагин. С его помощью в нагрузочных скриптах Gatling можно выполнять запросы любой сложности: от обычных выборок до создания процедур. Таким образом, с помощью плагина можно как нагрузить саму базу данных, так и провести генерацию данных в ней перед подачей нагрузки. Давайте разберемся, как работать с этим плагином.

Тестовая база данных

Для демонстрации работы плагина развернем тестовую базу данных. Можно выбрать любую, мы будем использовать Postgres 12 версии на дистрибутиве Alpine. Актуальные версии можно посмотреть на Docker Hub. Чтобы локально развернуть тестовую базу данных, используем файл docker-compose.yml.

services:
  postgres:    
    image: postgres:12-alpine
    environment:
      POSTGRES_DB: "test_db"
      POSTGRES_PASSWORD: "secret"
      POSTGRES_USER: postgres
      POSTGRES_HOST_AUTH_METHOD: trust
      ports:      - '5432:5432'

Для запуска создаем файл docker-compose.yml и выполняем команду docker-compose up. Обратите внимание: для выполнения команды понадобится установленный и запущенный Docker.

После выполнения команды разворачиваем тестовую базу данных по адресу jdbc:postgresql://localhost:5432/test_db. К ней можно подключиться любым удобным для вас способом.

Разработка скрипта

Мы не будем разрабатывать проект с нуля, а используем готовый шаблон. Об использовании шаблона можно прочитать в первой статье цикла. Стоит учесть, что JDBC-плагин не работает с версией Gatling 3.7. Скорее всего, баг устранят в версии 3.8. В качестве IDE для разработки скриптов будем использовать IntelliJ IDEA. Теперь перейдем к процессу разработки скрипта. 

Шаг 1. Обновление зависимостей

В файле project/Dependencies.scala добавляем плагин для работы Gatling с протоколом JDBC (нужную версию можно найти на GitHub) и драйвер для работы с базами данных Postgres (релизы). Вместо Postgres можно использовать другой драйвер. Для этого вместо драйвера Postgres указываем другой драйвер для выбранной базы данных.

  lazy val jdbc_plugin: Seq[ModuleID] = Seq(
    "ru.tinkoff" %% "gatling-jdbc-plugin"
  ).map(_ % "<current version>")

  lazy val postgresJdbc: Seq[ModuleID] = Seq(
    "org.postgresql" % "postgresql"
  ).map(_ % "<current version>")

В файле build.sbt добавляем новые зависимости.

libraryDependencies ++= jdbc_plugin,
libraryDependencies ++= postgresJdbc,

Чтобы зависимости подгрузились в проект, нажимаем Load sbt Changes через интерфейс IntelliJ IDEA.

Шаг 2. Переменные сервиса

В файле src/test/resources/simulation.conf хранятся дефолтные переменные для запуска. В него нужно добавить переменные, которые требуются для подключения к тестовой базе данных.

baseUrl: "jdbc:postgresql://localhost:5432/test_db"
dbUser: "postgres"
dbPassword: "secret"

Шаг 3. Запросы

В директории cases создаем новый scala-класс, где будем описывать наши запросы. Назовем его JdbcActions. Затем описываем наши скрипты. Для начала создаем таблицу test_table, которая будет содержать в себе два поля: id и name.

package ru.tinkoff.load.myJdbcSample.cases
import io.gatling.core.Predef._
import ru.tinkoff.load.jdbc.Predef.jdbc
import ru.tinkoff.load.jdbc.actions
import ru.tinkoff.load.jdbc.actions.Columns

object JdbcActions {
    val createTable: actions.RawSqlActionBuilder = jdbc("create table")
    .rawSql(
      """create table 
        |if not exists 
        |test_table(
        |id SERIAL, 
        |name varchar(20))"""
        .stripMargin)
} 

Затем вставляем запись в нашу таблицу. 

val insertData: actions.DBInsertActionBuilder = jdbc("insert")
	.insertInto("test_table", Columns("name"))
	.values("name" -> "Some text")

Посмотреть, какие разновидности запросов есть в плагине, можно в нашем репозитории. Часто возникает ситуация, когда в таблицу нужно вставить случайное значение или текущую дату, не нарушая уникальность записей. Справиться с ней поможет feeder.

Шаг 4. Feeders

Подробнее о feeders можно прочитать во вводной статье. Варианты feeder могут быть разными: от файла до случайного значения или результата выполнения SQL-запроса в базе данных. Мы попробуем использовать feeder, который генерирует случайную строку.

В директории src/test/scala/ru/tinkoff/load/myJdbcSample создаем новую директорию feeders, а в ней — object Feeders.

package ru.tinkoff.load.myJdbcSample.feeders

import ru.tinkoff.gatling.feeders.RandomStringFeeder

object Feeders {  
  val myRandomStringFeeder = RandomStringFeeder("myString", 10)
}

Чтобы использовать feeder, меняем наш запрос на добавление данных в таблицу. 

val insertData: actions.DBInsertActionBuilder = jdbc("insert")
	.insertInto("test_table", Columns("name"))
	.values("name" -> "${myString}")

Шаг 5. Сценарий теста

В CommonScenario меняем код для выполнения наших запросов. При такой реализации запросы будут выполняться последовательно в указанном порядке.

package ru.tinkoff.load.myJdbcSample.scenarios

import io.gatling.core.Predef._
import io.gatling.core.structure.ScenarioBuilder
import ru.tinkoff.load.myJdbcSample.cases._
import ru.tinkoff.load.myJdbcSample.feeders.Feeders.myRandomStringFeeder

object CommonScenario {
  def apply(): ScenarioBuilder = new CommonScenario().scn
}

class CommonScenario {

  val scn: ScenarioBuilder = scenario("Common Scenario")
    // Добавление Feeder
    .feed(myRandomStringFeeder)
    // Выполнение запроса на создание таблицы
    .exec(JdbcActions.createTable)
  	// Выполнение запроса на вставку данных в таблицу
    .exec(JdbcActions.insertData)
}

Шаг 6. Описание JDBC-протокола

В файле myJdbcSample.scala описываем протокол для работы с базой данных. Метод getStringParam позволяет считывать параметры, заданные в шаге два, при локальной отладке.

package ru.tinkoff.load

import ru.tinkoff.gatling.config.SimulationConfig._
import ru.tinkoff.load.jdbc.Predef._
import ru.tinkoff.load.jdbc.protocol.JdbcProtocolBuilder

import scala.concurrent.duration.DurationInt

package object myJdbcSample {

  val jdbcProtocol: JdbcProtocolBuilder = DB
    .url(baseUrl)
    .username(getStringParam("dbUser"))
    .password(getStringParam("dbPassword"))
    .connectionTimeout(2.minute)
}

Шаг 7. Нагрузочные тесты

Описываем, как будем подавать нагрузку, в файле Debug.scala.

package ru.tinkoff.load.myJdbcSample

import io.gatling.core.Predef._
import ru.tinkoff.load.jdbc.Predef._
import ru.tinkoff.load.myJdbcSample.scenarios.CommonScenario

class Debug extends Simulation {

  setUp(
    // Вызов сценария
    CommonScenario()
      // Нагрузка будет подаваться 1 пользователем = 1 итерация  
      .inject(atOnceUsers(1)),
  ).protocols(
    // Указываем наш протокол
    jdbcProtocol,             
  )

}

По аналогии изменяем протокол в MaxPerformance и Stability. Помимо протокола нужно изменить сценарий для MaxPerformance и Stability-тестов. В нашем примере мы создаем таблицу, поэтому нет смысла пытаться создавать таблицу множество раз. Но вставлять значения в таблицу мы можем сколько угодно раз. В таких случаях можно воспользоваться конструкцией andThen. Она позволяет определить последовательность запуска сценариев. 

Для начала опишем новые сценарии в объекте CommonScenario.scala: отдельно для создания таблицы и отдельно для добавления туда записей. 

  val createTableScn: ScenarioBuilder = scenario("create table")
    .exec(JdbcActions.createTable)

  val insertInTable: ScenarioBuilder = scenario("insert")
    .feed(myRandomStringFeeder)
    .exec(JdbcActions.insertData)

Опишем подачу нагрузки в MaxPerformance.scala.

package ru.tinkoff.load.myJdbcSample

import io.gatling.core.Predef._
import ru.tinkoff.gatling.config.SimulationConfig._
import ru.tinkoff.gatling.influxdb.Annotations
import ru.tinkoff.load.myJdbcSample.scenarios.CommonScenario
import ru.tinkoff.load.jdbc.Predef._

class MaxPerformance extends Simulation with Annotations {

  setUp(
    new CommonScenario().createTableScn
      .inject(atOnceUsers(1))
      .andThen(
        new CommonScenario().insertInTable.inject(
          // интенсивность на ступень
          incrementUsersPerSec((intensity / stagesNumber).toInt)
            // Количество ступеней
            .times(stagesNumber)
            // Длительность полки
            .eachLevelLasting(stageDuration)
            // Длительность разгона
            .separatedByRampsLasting(rampDuration)
            // Начало нагрузки с
            .startingFrom(0),                                    
        ),
      ),
  ).protocols(jdbcProtocol)
    // общая длительность теста
    .maxDuration(testDuration)
}

Благодаря этому запрос на создание таблицы будет выполняться единожды, а после выполнения будет подаваться нагрузка запросами для вставки данных в таблицу. Такие же действия выполняем для Stability-теста.

Шаг 8. Запуск Debug-теста

Для запуска теста используем GatlingRunner. После выполнения скрипта можно подключиться к тестовой базе данных и убедиться, что запись попала в нашу таблицу. 

package ru.tinkoff.load.myJdbcSample

import io.gatling.app.Gatling
import io.gatling.core.config.GatlingPropertiesBuilder

object GatlingRunner {

  def main(args: Array[String]): Unit = {

    // this is where you specify the class you want to run
    // Указывает имя теста Debug, либо какой-то другой,
    // например, MaxPerformance
    val simulationClass = classOf[Debug].getName 

    val props = new GatlingPropertiesBuilder
    props.simulationClass(simulationClass)

    Gatling.fromMap(props.build)
  }
}

Заключение

В третьей статье цикла о Gatling мы разобрались, как можно выполнять запросы к базе данных с помощью нашего JDBC-плагина. Мы рассмотрели работу с Postgres, но плагин поддерживает и другие базы данных. Помимо нагрузки на базу данных в рамках тестирования, плагин будет полезен, если нужна генерация тестовых данных. В следующих статьях мы рассмотрим работу Gatling с gRPC, Kafka и AMQP.

Полезные ссылки

  1. Gatling JDBC Plugin

  2. Gatling Feeders

  3. Проект Gatling из примеров этой статьи.