Gatling (как обсуждается в K6: Modern Load Testing with JavaScript for DevOps Teams) — это мощный, высокопроизводительный инструмент нагрузочного тестирования, разработанный для тестирования веб-приложений, API и микросервисов. Построенный на Akka и Netty, Gatling (как обсуждается в Load Testing with JMeter: Complete Guide) превосходно симулирует тысячи одновременных пользователей с минимальным потреблением ресурсов, предоставляя детальные, действенные метрики производительности через красивые HTML-отчёты.

Почему Gatling для Performance Тестирования?

Gatling выделяется на ландшафте performance тестирования благодаря уникальной комбинации дружественного разработчикам DSL, исключительной производительности и enterprise-grade функций:

  • Scala DSL: Выразительный, type-safe скриптинг с поддержкой IDE
  • Высокая производительность: Async non-blocking архитектура справляется с массивной конкурентностью
  • Реалистичные симуляции: Продвинутое моделирование пользователей с think times и pacing
  • Богатые протоколы: Поддержка HTTP, WebSocket, SSE, JMS out of the box
  • Красивые отчёты: Детальные HTML-отчёты с графиками и метриками
  • Готов к CI/CD: Выполнение из командной строки и интеграция Maven/Gradle

Gatling vs Другие Инструменты Load Testing

ФункцияGatlingJMeterK6Locust
ScriptingScala DSLGUI/XMLJavaScriptPython
ПроизводительностьОтличнаяСредняяОтличнаяСредняя
Использование РесурсовНизкоеВысокоеНизкоеСреднее
Поддержка ПротоколовHTTP, WS, JMSОбширнаяHTTP/2, WS, gRPCHTTP
ReportingОтличныйБазовыйХорошийБазовый
RecorderДаДаНетНет
Интеграция CI/CDНативнаяПлагиныНативнаяРучная

Начало Работы с Gatling

Установка с Maven

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>gatling-tests</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <gatling.version>3.10.3</gatling.version>
    <gatling-maven-plugin.version>4.7.0</gatling-maven-plugin.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>io.gatling.highcharts</groupId>
      <artifactId>gatling-charts-highcharts</artifactId>
      <version>${gatling.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>io.gatling</groupId>
        <artifactId>gatling-maven-plugin</artifactId>
        <version>${gatling-maven-plugin.version}</version>
      </plugin>
    </plugins>
  </build>
</project>

Установка с Gradle

plugins {
    id 'scala'
    id 'io.gatling.gradle' version '3.10.3.1'
}

repositories {
    mavenCentral()
}

dependencies {
    gatling 'io.gatling.highcharts:gatling-charts-highcharts:3.10.3'
}

gatling {
    simulations = {
        include '**/*Simulation.scala'
    }
}

Первая Симуляция Gatling

package simulations

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

class BasicSimulation extends Simulation {

  // Конфигурация HTTP протокола
  val httpProtocol = http
    .baseUrl("https://api.example.com")
    .acceptHeader("application/json")
    .userAgentHeader("Gatling Performance Test")

  // Определение сценария
  val scn = scenario("Basic Load Test")
    .exec(
      http("Get Users")
        .get("/users")
        .check(status.is(200))
    )
    .pause(1)
    .exec(
      http("Get User Details")
        .get("/users/1")
        .check(status.is(200))
        .check(jsonPath("$.name").exists)
    )

  // Инъекция нагрузки
  setUp(
    scn.inject(
      atOnceUsers(10),           // 10 пользователей сразу
      rampUsers(50) during (30.seconds) // 50 пользователей за 30 секунд
    )
  ).protocols(httpProtocol)
}

Запустить симуляцию:

mvn gatling:test
# или
./gradlew gatlingRun

Профили Инъекции Нагрузки

Модели Открытой vs Закрытой Нагрузки

// Открытая Модель: Новые пользователи прибывают независимо от производительности системы
setUp(
  scn.inject(
    nothingFor(4.seconds),              // Ждать 4 секунды
    atOnceUsers(10),                    // Инжектировать 10 пользователей сразу
    rampUsers(50) during (30.seconds),  // Подъём до 50 пользователей
    constantUsersPerSec(20) during (1.minute), // Постоянная частота
    rampUsersPerSec(10) to 50 during (2.minutes) // Подъём частоты
  )
)

// Закрытая Модель: Фиксированное количество одновременных пользователей
setUp(
  scn.inject(
    constantConcurrentUsers(10) during (30.seconds),
    rampConcurrentUsers(10) to 50 during (1.minute)
  )
)

Продвинутые Паттерны Инъекции

setUp(
  // Паттерн stress test
  scn.inject(
    rampUsers(10) during (1.minute),    // Прогрев
    constantUsersPerSec(100) during (5.minutes), // Устойчивая нагрузка
    rampUsersPerSec(100) to 500 during (2.minutes), // Подъём до пика
    constantUsersPerSec(500) during (5.minutes), // Пиковая нагрузка
    rampUsersPerSec(500) to 100 during (2.minutes)  // Охлаждение
  ),

  // Spike test
  scn.inject(
    constantUsersPerSec(50) during (2.minutes),
    atOnceUsers(500),                    // Внезапный spike
    constantUsersPerSec(50) during (2.minutes)
  )
).protocols(httpProtocol)

Throttling

setUp(
  scn.inject(rampUsers(1000) during (1.minute))
).protocols(httpProtocol)
  .throttle(
    reachRps(100) in (10.seconds),      // Достичь 100 RPS
    holdFor(1.minute),                   // Удержать
    jumpToRps(50),                       // Спуститься до 50
    holdFor(2.minutes)
  )

Конфигурация HTTP Протокола

Полная Настройка HTTP Протокола

val httpProtocol = http
  .baseUrl("https://api.example.com")
  .acceptHeader("application/json, text/html, */*")
  .acceptEncodingHeader("gzip, deflate")
  .acceptLanguageHeader("en-US,en;q=0.5")
  .userAgentHeader("Gatling/3.10")
  .shareConnections                     // Connection pooling
  .disableWarmUp                        // Пропустить warmup запросы

  // Headers
  .header("X-API-Version", "v1")

  // Аутентификация
  .authorizationHeader("Bearer ${token}")

  // Proxy
  .proxy(Proxy("proxy.example.com", 8080)
    .credentials("username", "password"))

  // Timeouts
  .requestTimeout(30.seconds)
  .readTimeout(30.seconds)

  // Keep-alive
  .connectionHeader("keep-alive")

  // Автоматическая обработка редиректов
  .disableFollowRedirect

Построение Сложных Сценариев

Композиция Сценариев

// Переиспользуемые компоненты сценария
val browse = exec(
  http("Homepage")
    .get("/")
    .check(status.is(200))
).pause(2, 5) // Случайная пауза между 2-5 секунд

val search = exec(
  http("Search Products")
    .get("/search")
    .queryParam("q", "laptop")
    .check(status.is(200))
    .check(jsonPath("$.results[*].id").findAll.saveAs("productIds"))
).pause(1, 3)

val viewProduct = exec(
  http("View Product")
    .get("/products/${productId}")
    .check(status.is(200))
).pause(3, 7)

// Скомпоновать в полный сценарий
val scn = scenario("E-commerce User Journey")
  .exec(browse)
  .exec(search)
  .exec(session => {
    val productIds = session("productIds").as[Vector[String]]
    val randomId = productIds(scala.util.Random.nextInt(productIds.length))
    session.set("productId", randomId)
  })
  .exec(viewProduct)

Условное Выполнение

val scn = scenario("Условный Поток")
  .exec(
    http("Get User")
      .get("/users/me")
      .check(status.is(200))
      .check(jsonPath("$.role").saveAs("userRole"))
  )
  .doIf(session => session("userRole").as[String] == "admin") {
    exec(
      http("Admin Dashboard")
        .get("/admin/dashboard")
        .check(status.is(200))
    )
  }
  .doIfOrElse(session => session("userRole").as[String] == "premium") {
    exec(http("Premium Features").get("/premium"))
  } {
    exec(http("Standard Features").get("/standard"))
  }

Циклы и Итерации

val scn = scenario("Пример Цикла")
  // Повторить фиксированное количество раз
  .repeat(5) {
    exec(http("Request ${counter}").get("/api/data"))
      .pause(1)
  }

  // Повторять пока условие истинно
  .asLongAs(session => session("continue").as[Boolean]) {
    exec(http("Poll").get("/api/status"))
      .pause(5)
  }

  // Повторять в течение времени
  .during(30.seconds) {
    exec(http("Sustained Load").get("/api/endpoint"))
      .pause(1)
  }

  // Итерировать по коллекции
  .foreach(List("product1", "product2", "product3"), "productId") {
    exec(
      http("Get Product ${productId}")
        .get("/products/${productId}")
    )
  }

Построение Запросов и Валидация

POST Запросы с Body

val scn = scenario("CRUD Операции API")
  // JSON body из строки
  .exec(
    http("Create User")
      .post("/users")
      .body(StringBody("""{"name": "John", "email": "john@example.com"}"""))
      .asJson
      .check(status.is(201))
      .check(jsonPath("$.id").saveAs("userId"))
  )

  // JSON body из шаблона
  .exec(
    http("Create Order")
      .post("/orders")
      .body(ElFileBody("templates/order.json"))
      .asJson
  )

  // Form data
  .exec(
    http("Submit Form")
      .post("/submit")
      .formParam("field1", "value1")
      .formParam("field2", "${dynamicValue}")
  )

Продвинутые Checks

val scn = scenario("Валидация Ответа")
  .exec(
    http("Комплексная Валидация")
      .get("/api/products")
      .check(status.is(200))

      // JSON path checks
      .check(jsonPath("$.products[*]").count.gte(10))
      .check(jsonPath("$.products[0].name").exists)
      .check(jsonPath("$.products[*].price").findAll.saveAs("prices"))

      // Response time check
      .check(responseTimeInMillis.lte(500))

      // Header checks
      .check(header("Content-Type").is("application/json"))

      // Body substring check
      .check(substring("success").exists)

      // Регулярное выражение
      .check(regex("""id":\s*(\d+)""").findAll.saveAs("ids"))

      // Проверка MD5/SHA hash
      .check(md5.is("expected-hash"))
  )

Управление Сессией

val scn = scenario("Переменные Сессии")
  .exec { session =>
    println(s"Session ID: ${session.userId}")
    session.set("customVar", "value")
  }

  .exec(
    http("Login")
      .post("/login")
      .body(StringBody("""{"username": "${username}", "password": "${password}"}"""))
      .check(jsonPath("$.token").saveAs("authToken"))
  )

  .exec(
    http("Authenticated Request")
      .get("/protected")
      .header("Authorization", "Bearer ${authToken}")
  )

  // Доступ к данным сессии
  .exec { session =>
    val token = session("authToken").as[String]
    println(s"Using token: $token")
    session
  }

Feeders для Data-Driven Тестов

CSV Feeder

// users.csv:
// username,password,role
// user1,pass1,admin
// user2,pass2,user

val csvFeeder = csv("users.csv").random // Случайный выбор
val csvFeederCircular = csv("users.csv").circular // Round-robin
val csvFeederQueue = csv("users.csv").queue // Последовательный, один раз

val scn = scenario("Data-Driven Login")
  .feed(csvFeeder)
  .exec(
    http("Login as ${username}")
      .post("/login")
      .body(StringBody("""{"username": "${username}", "password": "${password}"}"""))
      .check(status.is(200))
  )

JSON и Кастомные Feeders

// JSON feeder
val jsonFeeder = jsonFile("products.json").random

// Array feeder
val customFeeder = Array(
  Map("product" -> "Laptop", "price" -> "999"),
  Map("product" -> "Phone", "price" -> "599")
).random

// Генерируемый feeder
val idFeeder = Iterator.continually(
  Map("userId" -> scala.util.Random.nextInt(1000))
)

val scn = scenario("Custom Feeder")
  .feed(idFeeder)
  .exec(
    http("Get User ${userId}")
      .get("/users/${userId}")
  )

Реальные Сценарии Тестирования

Полный Пользовательский Поток E-commerce

package simulations

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

class EcommerceSimulation extends Simulation {

  val httpProtocol = http
    .baseUrl("https://ecommerce.example.com")
    .acceptHeader("application/json")

  val csvFeeder = csv("users.csv").circular

  // Сценарий: Просмотр и Покупка
  val browse = scenario("Browse Products")
    .feed(csvFeeder)
    .exec(
      http("Homepage")
        .get("/")
        .check(status.is(200))
    )
    .pause(2, 4)
    .exec(
      http("Category Page")
        .get("/category/electronics")
        .check(status.is(200))
        .check(jsonPath("$.products[*].id").findAll.saveAs("productIds"))
    )
    .pause(3, 6)

  val purchase = scenario("Complete Purchase")
    .feed(csvFeeder)
    .exec(browse)
    .exec { session =>
      val productIds = session("productIds").as[Vector[String]]
      session.set("selectedProduct", productIds.head)
    }
    .exec(
      http("Product Details")
        .get("/products/${selectedProduct}")
        .check(status.is(200))
    )
    .pause(5, 10)
    .exec(
      http("Add to Cart")
        .post("/cart")
        .body(StringBody("""{"productId": "${selectedProduct}", "quantity": 1}"""))
        .asJson
        .check(status.is(200))
    )
    .pause(2, 5)
    .exec(
      http("Checkout")
        .post("/checkout")
        .body(ElFileBody("checkout.json"))
        .asJson
        .check(status.is(201))
    )

  // Симуляция нагрузки
  setUp(
    browse.inject(
      rampUsers(100) during (2.minutes)
    ),
    purchase.inject(
      rampUsers(50) during (3.minutes)
    )
  ).protocols(httpProtocol)
    .assertions(
      global.responseTime.percentile3.lt(1500),
      global.successfulRequests.percent.gt(95)
    )
}

Интеграция CI/CD

Jenkins Pipeline

pipeline {
    agent any

    stages {
        stage('Checkout') {
            steps {
                git 'https://github.com/your/repo.git'
            }
        }

        stage('Run Gatling Tests') {
            steps {
                sh 'mvn gatling:test'
            }
        }

        stage('Publish Reports') {
            steps {
                gatlingArchive()
            }
        }
    }

    post {
        always {
            publishHTML([
                reportDir: 'target/gatling',
                reportFiles: 'index.html',
                reportName: 'Gatling Report'
            ])
        }
    }
}

GitHub Actions

name: Performance Tests

on:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:

jobs:
  gatling:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Run Gatling Tests
        run: mvn gatling:test

      - name: Upload Gatling Results
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: gatling-results
          path: target/gatling/

Assertions и SLAs

setUp(
  scn.inject(rampUsers(100) during (1.minute))
).protocols(httpProtocol)
  .assertions(
    // Глобальные assertions
    global.responseTime.max.lt(5000),
    global.responseTime.mean.lt(1000),
    global.responseTime.percentile3.lt(1500), // 95-й перцентиль
    global.responseTime.percentile4.lt(2000), // 99-й перцентиль

    // Показатель успеха
    global.successfulRequests.percent.gt(99),
    global.failedRequests.count.lt(10),

    // Assertions специфичные для запроса
    forAll.responseTime.max.lt(3000),
    details("Homepage").responseTime.max.lt(500),
    details("API Call").successfulRequests.percent.is(100)
  )

Лучшие Практики

Организация Симуляций

// BaseSimulation.scala - Общая конфигурация
abstract class BaseSimulation extends Simulation {
  val httpProtocol = http
    .baseUrl(System.getProperty("baseUrl", "https://api.example.com"))
    .acceptHeader("application/json")
    .userAgentHeader("Gatling")

  val defaultPause = 1.second
}

// Специфичные симуляции расширяют базовую
class UserSimulation extends BaseSimulation {
  // Код специфичный для симуляции
}

Think Time и Pacing

val scn = scenario("Реалистичный Пользователь")
  .exec(http("Action 1").get("/page1"))
  .pause(2, 5) // Случайно между 2-5 секунд

  .exec(http("Action 2").get("/page2"))
  .pace(10.seconds) // Поддерживать 10с между итерациями

  .exec(http("Action 3").get("/page3"))
  .rendezVous(100) // Ждать пока 100 пользователей достигнут этой точки

Заключение

Gatling сочетает высокую производительность с продуктивностью разработчика, что делает его отличным выбором для современного performance тестирования. Его Scala DSL обеспечивает type safety и выразительность, в то время как его async архитектура предоставляет исключительные возможности генерации нагрузки.

Ключевые сильные стороны:

  • Высокопроизводительная async архитектура
  • Выразительный Scala DSL с поддержкой IDE
  • Красивые, детальные HTML-отчёты
  • Отличная поддержка протоколов
  • Сильная интеграция CI/CD
  • (как обсуждается в Artillery Performance Testing: Modern Load Testing with YAML Scenarios) Enterprise-ready функции

Независимо от того, тестируете ли вы REST API, микросервисы или полные веб-приложения, Gatling предоставляет производительность, функции и отчётность, необходимые для комплексного нагрузочного тестирования в современных DevOps средах.