Кратко
Locust — простой масштабируемый инструмент тестирования производительности сайтов. Вы описываете поведение пользователей обычным Python-кодом, что делает Locust расширяемым и удобным.
Идея заключается в том, что большое количество эмулированных пользователей («саранча», отсюда название) отправляет запросы на тестируемый сайт. Поведение каждого пользователя описывается вами, процесс отслеживается в веб-интерфейсе в реальном времени. Это позволяет провести “боевое” тестирование и выявить узкие места в коде, “прежде чем пускать реальных пользователей”.
Тестовый сервер
Чтобы запустить Locust, нужен «веб-сервер», производительность которого мы хотим проверить. В наш контейнер «locust» мы уже включили простой веб-сервер, который вы можете использовать в этом гайде. Чтобы запустить этот сервер, выполните в контейнере следующую команду:
$ simple-server
Должно появиться сообщение о том, что сервер запущен на порту 3000.
К сожалению, по умолчанию сервер работает в фоновом режиме, поэтому вам придется сначала завершить его, если вы хотите выполнить команду (например, ls) в контейнере, и в этот момент сервер уже будет недоступен.
У этой ситуации есть два возможных решения:
- Запустить сервер в фоновом режиме с помощью следующей команды:
$ simple-server > /dev/null &
Команда > /dev/null глушит вывод и & выполняет команду в дочернем процессе, отделенном от текущей оболочки, так что вам не придется ждать завершения процесса. Теперь вы сможете выполнить команду.
- Запустить другую оболочку (либо с помощью tmux, либо командой docker exec -it locust /bin/bash) и выполните следующую команду в новой оболочке.
Используя один из двух вышеперечисленных методов, убедитесь, что ваш сервер работает в контейнере и у вас по-прежнему есть интерактивная оболочка, через которую вы можете выполнять команды внутри контейнера.
Простой тест
Теперь, когда наш тестовый сервер запущен, давайте запустим на нем наш первый тест производительности. Мы создали простой «файл Locust», locustfile.py (ссылка). Загрузите этот файл и запустите сервер Locust Web UI на основе загруженного файла:
$ locust -f ./locustfile.py --host=http://localhost:3000
Вышеприведенная команда указывает “locust file” (-f), который определяет «поведение пользователя», например, сколько запросов выдается через какие промежутки времени и т. д. Также указывается адрес веб-сайта, который мы тестируем (—host), в нашем примере это порт 3000 на http://localhost.
После выполнения приведенной выше команды вы, скорее всего, увидите следующий результат, который означает, что Locust Web UI запущен:
[2021-04-21 20:30:06,201] 46d3e53a99d9/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2021-04-21 20:30:06,210] 46d3e53a99d9/INFO/locust.main: Starting Locust 1.4.3
Теперь откройте браузер на хост-машине и перейдите по адресу http://localhost:8089. Вы увидите следующий пользовательский интерфейс Locust с предложением «Start new Locust swarm” (Запустить новый рой):
Введите количество пользователей, которое хотите эмулировать, например 200, и укажите скорость эмуляции (“порождения”, или “спауна” в терминологии Locust), например 100. Здесь «количество пользователей для эмуляции» означает, сколько одновременных пользователей Locust должен симулировать. “Скорость порождения» означает скорость, с которой эти пользователи создаются вначале, пока не будет создано указанное количество одновременных пользователей.
(Из-за ограничений производительности процессора вашей машины реальная скорость может быть меньше указанного вами числа).
Теперь нажмите кнопку «Start» и посмотрите, что произойдет. В пользовательском интерфейсе будет отображаться статистика в реальном времени о том, как сервер отвечает на запросы, отправленные Locust:
Термины
Более подробное объяснение того, что означает информация на этой странице:
- Host — Адрес сайта, на котором мы тестируем. (http://localhost:3000 в нашем случае)
- Status — Статус показывает «SPAWNING» в начале, когда “порождаются” новые пользователи, но как только заданное количество пользователей будет порождено, статус изменится на «RUNNING» и покажет количество одновременных пользователей, 200 в нашем примере.
- RPS — Количество запросов в секунду. Показывает, сколько HTTP-запросов в данный момент отправляется на сервер.
- Failures — Процент запросов, которые не выполняются из-за ошибки соединения, таймаута, не найденной страницы, плохого запроса или по другой подобной причине. Абсолютное число и причина отказов записываются на вкладке «Failures».
- Stop — Остановить текущий тест.
- Reset Stats — Если вы нажмете эту кнопку, вся статистика будет обнулена.
- Median, 90 percentile, Average, Min, Max — Эти столбцы показывают время ответа сервера, интервал между отправкой запроса и получением ответа от сервера.
- Average size — Средний размер ответа от сервера
- Current RPS, Current Failures/s — Эти столбцы показывают, сколько запросов отправляется на сервер в данный момент, и сколько отказов он получает.
Запуская Locust с разным количеством пользователей и просматривая статистику, вы можете увидеть, насколько хорошо работает сервер при различных условиях нагрузки. Измените количество пользователей, например до 500, и повторно запустите задачи, чтобы оценить, как изменилась производительность. Поиграйте с интерфейсом.
Составление Locust-файла
Теперь, когда вы знакомы с возможностями Locust, давайте узнаем, как написать locust-файл, описывающий набор задач, которые необходимо выполнить для тестирования производительности. Прежде чем продолжить, взгляните на наш пример файла locust, locustfile.py (здесь ссылка), чтобы получить представление как он выглядит:
import sys, time, random from locust import HttpUser, task, between class MyUser(HttpUser): wait_time = between(1, 2.5) @task def list(self): self.client.get('/api/posts?username=user2') self.client.get('/editor/post?action=list&username=user2') @task(2) def preview(self): # generate a random postid between 1 and 100 postid = random.randint(1, 100) self.client.get("/blog/user2/" + str(postid), name="/blog/user2/") def on_start(self): """on_start is called when a Locust start before any task is scheduled""" res = self.client.post("/login", data={"username":"user2", "password": "blogserver"}) if res.status_code != 200: print("Failed to authenticate the user2 user on the server") sys.exit();
Примечание: К счастью, для пользования Locust не нужно знать многих тонкостей Python, вполне достаточно полистать учебник.
Разбираем код
import sys, time, random from locust import HttpUser, task, between
Файл locust — это обычный модуль Python, он может импортировать код из других файлов или пакетов.
class MyUser(HttpUser):
Здесь мы определяем класс для пользователей, которых мы будем эмулировать. Он наследуется от HttpUser, что дает каждому пользователю атрибут client, который является экземпляром HttpSession, который может быть использован для выполнения HTTP-запросов к целевой системе. Когда тест начнется, locust создаст экземпляр этого класса для каждого пользователя, которого он моделирует, и каждый из этих пользователей начнет работать в своем собственном «зеленом» потоке gevent.
wait_time = between(1, 2.5)
Наш класс определяет время ожидания (wait_time), которое заставит эмулируемых пользователей ждать от 1 до 2,5 секунд после выполнения каждой задачи (см. ниже). Более подробную информацию можно найти в атрибуте wait_time.
@task def list(self): self.client.get('/api/posts?username=user2') self.client.get('/editor/post?action=list&username=user2')
Методы, декорированные @task, являются ядром вашего locust-файла. Для каждого запущенного пользователя Locust создает greenlet (микропоток), который будет вызывать эти методы.
@task def list(self): ... @task(2) def preview(self): ...
Мы объявили две задачи, декорировав два метода с помощью @task, одному из которых был присвоен больший “вес” (2). Когда наш MyUser запустится, он выберет одну из объявленных задач — в данном случае list или preview — и выполнит ее. Задачи выбираются случайным образом, но вы можете придать им разный вес. В приведенной выше конфигурации Locust в два раза чаще выбирает preview, чем list. После завершения выполнения задачи пользователь будет “спать” в течение времени ожидания (в данном случае от 1 до 2,5 секунд). По истечении времени ожидания он выберет новую задачу и повторит все сначала.
Обратите внимание, что выбираться будут только методы, декорированные @task, поэтому вы можете определять свои кастомные внутренние методы-хелперы как угодно.
self.client.get('/api/posts?username=user2')
Атрибут self.client позволяет выполнять HTTP-запросы, которые будут регистрироваться Locust. Информацию о том, как выполнять другие виды запросов, проверять ответ и т. д., смотрите в разделе документации Использование HTTP-клиента.
@task(3) def preview(self): # generate a random postid between 1 and 100 postid = random.randint(1, 100) self.client.get("/blog/user2/" + str(postid), name="/blog/user2/")
В задаче preview мы используем 100 различных URL, выбирая случайный postid между 1 и 100. Чтобы не получить 100 отдельных записей в статистике — поскольку статистика группируется по URL — мы используем параметр name, чтобы сгруппировать все эти запросы под записью с именем «/blog/user2/».
def on_start(self): """on_start is called when a Locust start before any task is scheduled""" res = self.client.post("/login", data={"username":"user2", "password": "blogserver"}) if res.status_code != 200: print("Failed to authenticate the user2 user on the server") sys.exit();
Кроме того, мы объявили метод on_start. Метод с таким именем будет вызываться для каждого эмулируемого пользователя при запуске. Подробнее см. методы on_start и on_stop.
Запуск без веб-интерфейса
Вы можете запустить locust без веб-интерфейса, используя флаг —headless вместе с -u и -r:
$ locust -f ./locustfile.py --host=http://localhost:3000 --headless -u 100 -r 100
Параметр —headless в команде указывает на то, что мы отключим веб-интерфейс и запустим его сразу в консоли. -u задает количество Users для порождения, а -r — скорость порождения (количество пользователей, запускаемых в секунду). Когда вы выполните приведенную выше команду, Locust начнет тест и будет выводить статистику каждые 2 секунды, пока вы не остановите его нажатием «Ctrl+C».
[2021-04-22 09:34:24,116] 46d3e53a99d9/INFO/locust.main: No run time limit set, use CTRL+C to interrupt. [2021-04-22 09:34:24,116] 46d3e53a99d9/INFO/locust.main: Starting Locust 1.4.3 [2021-04-22 09:34:24,117] 46d3e53a99d9/INFO/locust.runners: Spawning 100 users at the rate 100 users/s (0 users already running)... [2021-04-22 09:34:25,211] 46d3e53a99d9/INFO/locust.runners: All users spawned: MyUser: 100 (100 total running) Name # reqs # fails | Avg Min Max Median | req/s failures/s ----------------------------------------------------------------------------------------------------- GET /api/posts?username=user2 85 0(0.00%) | 2 1 13 2 | 0.00 0.00 GET /blog/user2/ 64 0(0.00%) | 2 1 13 2 | 0.00 0.00 GET /editor/post?action=list&username=user2 85 0(0.00%) | 2 1 10 2 | 0.00 0.00 POST /login 100 0(0.00%) | 8 1 104 2 | 0.00 0.00 ----------------------------------------------------------------------------------------------------- Aggregated 334 0(0.00%) | 4 1 104 2 | 0.00 0.00 ... KeyboardInterrupt 2021-04-22T16:35:01Z [2021-04-22 09:35:01,697] 46d3e53a99d9/INFO/locust.main: Running teardowns... [2021-04-22 09:35:01,698] 46d3e53a99d9/INFO/locust.main: Shutting down (exit code 0), bye. [2021-04-22 09:35:01,698] 46d3e53a99d9/INFO/locust.main: Cleaning up runner... [2021-04-22 09:35:01,699] 46d3e53a99d9/INFO/locust.runners: Stopping 100 users [2021-04-22 09:35:01,713] 46d3e53a99d9/INFO/locust.runners: 100 Users have been stopped, 0 still running Name # reqs # fails | Avg Min Max Median | req/s failures/s ------------------------------------------------------------------------------------------------------ GET /api/posts?username=user2 1669 0(0.00%) | 2 1 24 2 | 44.43 0.00 GET /blog/user2/ 854 0(0.00%) | 2 1 13 2 | 22.73 0.00 GET /editor/post?action=list&username=user2 1669 0(0.00%) | 2 1 27 2 | 44.43 0.00 POST /login 100 0(0.00%) | 8 1 104 2 | 2.66 0.00 ------------------------------------------------------------------------------------------------------ Aggregated 4292 0(0.00%) | 2 1 104 2 | 114.26 0.00 Response time percentiles (approximated) Type Name 50% 66% 75% 80% 90% 95% 98% 99% 99.9% 99.99% 100% # reqs -----|----------------------------------------|---|---|---|---|---|---|---|---|-----|------|----|------| GET /api/posts?username=user2 2 2 3 3 4 5 6 8 21 25 25 1669 GET /blog/user2/ 2 2 3 3 3 4 5 7 13 13 13 854 GET /editor/post?action=list&username=user2 2 2 2 3 3 4 5 7 21 27 27 1669 POST /login 2 3 3 4 18 64 98 100 100 100 100 100 -----|----------------------------------------|---|---|---|---|---|---|---|---|-----|------|----|------| None Aggregated 2 2 3 3 3 4 6 9 64 100 100 4292
Давайте посмотрим на последнюю таблицу, которая описывает распределение времени ответа. В первой строке таблицы мы имеем 1669 запросов, отправленных по адресу /api/posts?username=user2 методом GET и получивших ответы, среди которых 50 % запросов отвечают за 2 мс, 90 % — за 4 мс, а 99 % — в пределах 8 мс. Допустим, если время отклика должно быть менее 10 мс, наш сайт обслуживает 99% пользователей в пределах этого лимита, в то время как 100 пользователей посещают сайт каждую секунду.
В ваших результатах количество запросов для GET /api/posts?username=user2 отличается от количества запросов для GET /editor/post?action=list&username=user2. Это может произойти потому, что Locust считает только те запросы, на которые был получен ответ, иначе у этих запросов не было бы статистики времени ответа. Когда время истечет, Locust просто прекратит подсчет, выведет сводку и завершится, даже если некоторые из отправленных им запросов не были отвечены. А причина, по которой POST /login содержит ровно 100 запросов, заключается в том, что для каждого пользователя выполняется одна аутентификация.
Ограничение по времени, сброс статистики и “Только сводка”
Когда вы хотите запустить Locust с помощью скрипта, полезно остановить его, установив ограничение по времени, а не нажимая «Ctrl+C». Это можно сделать с помощью опции -t, как показано ниже:
$ locust -f ./locustfile.py --host=http://localhost:3000 --headless -u 100 -r 100 -t 30s
Приведенная выше команда остановит выполнение Locust через 30 секунд. В общем случае вы можете использовать формат времени XhYmZs (X часов, Y минут и Z секунд).
Также полезно знать опцию —reset-stats, которая указывает Locust сбросить данные, когда STATUS меняется с «SPAWNING» на «RUNNING». Эта опция иногда полезна, потому что для того, чтобы как можно быстрее перейти на стадию «RUNNING», обычно нужно быстро увеличить количество пользователей на стадии «SPAWNING». Это часто приводит к неестественно высокой нагрузке на сервер и, возможно, к гораздо более высокому времени отклика и количеству ошибок, чем обычно. Сброс статистики после завершения начального этапа наращивания юзеров может предотвратить “загрязнение” текущей статистики.
Иногда нам не нужны все подробности при выполнении теста, и сводной таблицы, выведенной в конце, достаточно, чтобы понимать ситуацию. Мы можем запустить Locust без веб-интерфейса и добавить only summary в файл summary.txt, выполнив следующую команду:
$ locust -f ./locustfile.py --host=http://localhost:3000 --headless --reset-stats -u 100 -r 100 -t 30s --only-summary |& tee summary.txt
Параметр —only-summary указывает Locust подавлять вывод статистики во время теста и печатать только резюме. |& — это UNIX-пайп для перенаправления STDERR и STDOUT процесса Locust на STDIN UNIX-команды tee. Команда tee выведет все сообщения в консоль, а также сохранит их в файле, указанном в качестве параметра.
Прежде чем покинуть этот гайд, остановите наш простой сервер следующей командой, если вы запустили его в фоновом режиме:
$ pkill simple-server && pkill node
Этого гайда должно быть достаточно для знакомства с Locust. Но если хотите узнать больше, ознакомьтесь с официальной документацией.