Когда вы работаете с приложением с высоким трафиком или просто развертываете новое приложение в производственной среде, вам необходимо определить нагрузку, которую готовы принять ваше приложение и инфраструктура.

Также очень важно хорошо протестировать поведение вашего приложения: действительно, если часть его, например, имеет кеш, важно принять это во внимание и попытаться смоделировать «настоящую» нагрузку.

Для тестирования этой нагрузки на рынке существуют различные инструменты, позволяющие вам создавать сценарии сценариев, в зависимости от языка, который вам нравится, среди них: Locust, если вы хотите разработать свои сценарии на Python, или Vegeta, разработанный на Go, но позволяя довольно просто создавать сценарии.

Обзор Gatling

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

Gatling - это инструмент, написанный на Scala, для выполнения которого требуется JVM, поэтому для его выполнения вам потребуются инструменты Java.

Это, конечно, продукт с открытым исходным кодом, даже если доступно корпоративное (и, следовательно, платное) предложение на случай, если вам нужно быстро и просто получить отчеты о надежном приложении.

Цель этой статьи - технически продемонстрировать вам преимущества, которые Gatling привносит в управление вашими сценариями.

Фактор запроса в секунду / Коэффициент продолжительности

Когда вы запускаете сценарий, вы наверняка захотите иметь возможность быстро и легко адаптировать количество пользователей, которых вы хотите моделировать, а также продолжительность сценария.

Для этого вы можете просто настроить в своем сценарии две переменные, которые позволят вам вычислить множитель. Например:

val rps_factor = Integer.getInteger("rpsFactor", 1).toInt
val time_factor  = Integer.getInteger("timeFactor", 1).toInt

Затем назначьте своим тестовым сценариям нужные значения нагрузки, передав коэффициент множителя:

setUp(
    scn.inject(
      rampUsers(50 * rps_factor) over(5 * time_factor seconds),
      rampUsers(100 * rps_factor) over(10 * time_factor seconds),
    )
    .protocols(httpConf)
  )

Таким образом, вы можете передать значение 1 для тестов компиляции сценария (чтобы тесты не выполнялись слишком долго) и более высокие значения для производственных тестов.

Кормушки

Обычно это одна из первых концепций, которые вам понадобятся для запуска ваших тестов, но мы все равно поговорим об этом: фидеры, возможность предоставить вашему тесту набор значений.

Возьмем простой случай: вы хотите протестировать загрузку API в производственной среде и, чтобы избежать кеширования, вы хотите каждый раз выполнять вызовы для получения другого ресурса. В этом случае просто создайте файл CSV со списком допустимых идентификаторов.

Итак, давайте создадим файл user-files / data / my-identifiers.csv:

myIdentifiers
e9fbd24b-31f8-498f-ba03-7d758d4d2a17
2a012137-ec39-4d37-b2b7-0fc3186f78a0
507a036e-a946-4e82-ae52-305306981694

Затем загрузите этот файл в свой код и дайте его своему сценарию для использования следующим образом:

val myIdentifiersFeeder = csv("my-identifiers.csv").random
val scn = scenario("FTPPubSimulation")
  .feed(feeder)
  .exec(http("Call to obtain my ressource")
    .get("/my-ressource/${myIdentifiers}")
    .queryParam("id_diffusion", "${metaId}"))

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

Состояние на основе предыдущего запроса

Представьте, что вы делаете запрос к API и хотите сделать второй запрос только в том случае, если значение определено в вашем первом запросе.

Это возможно со следующим синтаксисом:

.exec(
    http("GET /api/conditionner/{conditionID}")
    .get("/api/conditionner/${conditionID}")
    .check(
        jsonPath("$..purchaseId").findAll.saveAs("purchaseID")
    )
    .doIf("${purchaseID.exists()}") {
        exec(http("GET /api/purchase/{purchaseID}")
        .get("/api/purchase/${purchaseID(0)}")
      )
    }
)

Здесь второй запрос (/ api / Purchase /…) будет выполнен только в том случае, если первым запросом будет возвращено поле «PurchaseId».

Для получения дополнительной информации о различных условиях, которые вы можете ввести, я приглашаю вас перейти на следующую страницу: https://gatling.io/docs/2.3/general/scenario/#conditional-statements.

Опрос: ожидание асинхронной задачи

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

Gatling также позволяет управлять этим делом с помощью функций tryMax и check:

.tryMax(100) {
    pause(1)
    .exec(http("GET /api/registration/{registrationID}")
        .get("/api/registration/${registrationID(0)}")
        .check(
            jsonPath("$..purchaseId").findAll.saveAs("purchaseID")
        )
    )
}
...

Здесь мы будем вызывать HTTP-запрос каждую секунду (потому что мы делаем паузу на одну секунду) до 100 раз, если в ответе не будет найдено поле «PurchaseID», и в этом случае проверка будет отмечена как положительная, и сценарий будет продолжен. до следующей казни.

Случайный запрос

Вы можете распределить нагрузку на два разных типа запросов: например, принять или отклонить регистрацию (случайным образом).

Для этого вы сможете сгенерировать случайное число (0 или 1) и использовать doIfEqualsOrElse для вызова соответствующего запроса:

exec(
    http("GET /api/contract/{contractID}/users")
    .get("/api/contract/${contractID}/users")
    .queryParam("limit", "10")
    .check(
        jsonPath("$.results[*].id").findAll.saveAs("userID")
    )
)
.foreach("${userID}", "elementID") {
    exec(
        http("PUT /api/user/{elementID}")
        .put("/api/user/${elementID}")
        .header("Cookie", "_token=" + token)
        .header("Content-Type", "application/json")
        .body(StringBody("""{"status": "status_has_contract"}""")).asJSON
        .check(status.not(404), status.not(500))
    )
}

В этом примере запрос PUT будет выполняться для всех пользовательских элементов, возвращенных первым запросом.

Играйте в тесты на нескольких узлах

Если вам нужно создать высокую нагрузку, одной машины может быть недостаточно ни с точки зрения доступных ресурсов (ЦП, ОЗУ), ни с точки зрения пропускной способности.

Gatling позволяет агрегировать данные из нескольких отчетов об испытаниях. Таким образом, идея состоит в том, чтобы воспроизводить отчеты на нескольких машинах одновременно и объединять отчеты для получения только одного.

Конечно, этот факт необходимо учитывать в пользовательских значениях и времени ваших сценариев.

Чтобы автоматизировать это, я потратил время на создание следующего сценария bash, который запускает один из моих сценариев и подключается к разным серверам для их одновременного запуска.

Начнем с подготовки различных переменных нашего скрипта:

#!/bin/bash
# Assuming we use this user for all hosts
USER_NAME='root'
# Remote hosts list
HOSTS=( 1.1.1.1 2.2.2.2 3.3.3.3 4.4.4.4 )
# Assuming all Gatling installation are in the same path (with write permissions)
GATLING_HOME=/opt/gatling/my-project
GATLING_SIMULATIONS_DIR=$GATLING_HOME/user-files/simulations
GATLING_RUNNER=$GATLING_HOME/bin/gatling.sh
# Simulation class name
SIMULATION_NAME='mynamespace.MyTestSimulation'
GATLING_REPORT_DIR=$GATLING_HOME/results/
GATHER_REPORTS_DIR=/gatling/reports/

Перед началом мы также очистим любой предыдущий отчет о тестировании, который может остаться локально и на удаленных серверах:

echo "Cleaning previous runs from localhost"
rm -rf reports
rm -rf $GATHER_REPORTS_DIR
mkdir $GATHER_REPORTS_DIR
rm -rf $GATLING_REPORT_DIR
for HOST in "${HOSTS[@]}"
do
  echo "Cleaning previous runs from host: $HOST"
  ssh -n -f $USER_NAME@$HOST "sh -c 'rm -rf $GATLING_REPORT_DIR'"
done

Пришло время обновить сценарии на серверах и запустить их:

for HOST in "${HOSTS[@]}"
do
  echo "Copying simulations to host: $HOST"
  scp -r $GATLING_SIMULATIONS_DIR/* $USER_NAME@$HOST:$GATLING_SIMULATIONS_DIR
done
for HOST in "${HOSTS[@]}"
do
  echo "Running simulation on host: $HOST"
  ssh -n -f $USER_NAME@$HOST "sh -c 'nohup $GATLING_RUNNER -nr -s $SIMULATION_NAME > /gatling/run.log 2>&1 &'"
done

После завершения тестов все, что вам нужно сделать, это получить файлы журнала и объединить их:

for HOST in "${HOSTS[@]}"
do
  echo "Gathering result file from host: $HOST"
  ssh -n -f $USER_NAME@$HOST "sh -c 'ls -t $GATLING_REPORT_DIR | head -n 1 | xargs -I {} mv ${GATLING_REPORT_DIR}{} ${GATLING_REPORT_DIR}report'"
  scp $USER_NAME@$HOST:${GATLING_REPORT_DIR}report/simulation.log ${GATHER_REPORTS_DIR}simulation-$HOST.log
done
mv $GATHER_REPORTS_DIR $GATLING_REPORT_DIR
echo "Aggregating simulations"
$GATLING_RUNNER -ro reports

Вот и все, ваш отчет теперь доступен. Вы можете объединить все эти команды, чтобы создать сценарий bash, который автоматически выполняет эти шаги.

Вывод

Gatling - это законченный инструмент, позволяющий создавать сценарии тестирования, адаптированные к вашему приложению. В этом случае выучить язык Scala несложно, и вы можете довольно просто написать свои сценарии.

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