вторник, 19 ноября 2013 г.

Автоматизация нагрузочного тестирования при помощи инструмента Яндекс.Танк

В ходе тестирования некоторых продуктов компании Positive Technologies наша команда тестировщиков-автоматизаторов столкнулась с необходимостью проведения быстрых стресс-тестов для одного http веб-сервиса. Такие тесты должны были быть простыми и быстрыми в разработке, не требовательными к аппаратным ресурсам, должны давать значительную нагрузку однотипными http-запросами, предоставлять статистические данные для анализа системы под нагрузкой.

Для их реализации мы исследовали и опробовали некоторое количество инструментов, среди которых был и Apache JMeter, о котором было рассказано ранее, и даже сделанный нами самодельный инструмент LogSniper, написанный на Python, который выполнял реплей заранее подготовленных серверных логов с http-запросами на цель.

Использование JMeter-а мы отклонили из-за значительной сложности подготовки и проведения тестов, высоких требований к производительности нагрузочного стенда и довольно малых мощностей нагрузки при этом, хотя это и компенсировалось высокой информативностью показателей собираемой статистики. А LogSniper был отклонён из-за малой мощности генерируемой нагрузки и даже простота подготовки нагрузочных http-пакетов не могла стать большим преимуществом. Другие инструменты также были отклонены по тем или иным причинам.

В итоге мы с моим коллегой Олегом Каштановым остановились на инструменте Яндекс.Танк (yandex-tank), о котором узнали побывав на конференции YAC-2013 и пообщавшись со специалистами Яндекса на эту тему. Этот инструмент полностью отвечал всем нашим требованиям к простоте подготовки теста и к генерируемой нагрузке.

1. Вкратце об инструменте Яндекс.Танк


Яндекс.Танк - инструмент для проведения нагрузочного тестирования, разрабатываемый в компании Яндекс и распространяемый под лицензией LGPL.

В основе инструмента лежит высокопроизводительный асинхронный генератор нагрузки phantom, который был изначально переделан из одноимённого веб-сервера, который "научили" работать в режиме клиента. При помощи phantom возможно генерировать десятки и сотни тысяч http-запросов в секунду (далее http-rps - http-requests per second).

В процессе своей работы Яндекс.Танк сохраняет полученные результаты в обычных текстовых лог-файлах, сгруппированных по директориям для отдельных тестов. В течение теста специальный модуль организует вывод результатов в табличном виде в консольный интерфейс. Одновременно с этим запускается локальный веб-сервер, позволяющий видеть те же самые результаты на информативных графиках. По окончании теста возможно автоматическое сохранение результатов на сервисе Loadosophia.org. Также имеется модуль загрузки результатов в хранилище Graphite.

Некоторые полезные ссылки:

  1. Код проекта на GitHub:
    https://github.com/yandex-load/yandex-tank
  2. Официальная документация по настройке и использованию инструмента:
    http://yandextank.readthedocs.org/en/latest/
  3. Информация о модулях Яндекс.Танка в wiki разработчиков:
    https://github.com/yandex-load/yandex-tank/wiki/%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D0%B8
  4. Презентация, в которой рассказывается об истории возникновения инструмента:
    http://tech.yandex.ru/events/yasubbotnik/msk-jul-2012/talks/296/
  5. История возникновения сервиса нагрузочного тестирования в Яндексе и разработка Яндекс.Танка:
    http://habrahabr.ru/company/yandex/blog/202020/
  6. Яндекс-клуб, посвященный вопросам использования инструмента:
    http://clubs.ya.ru/yandex-tank/

Функциональные особенности Яндекс.Танка:

  • возможность простого конфигурирования инструмента используя ini-файлы, либо опции командной строки;
  • гибкая настройка профилей нагрузки;
  • настраиваемый авто-стоп теста по различным критериям, например, если время отклика сервера превышает заданный порог или допустимое количество сетевых ошибок;
  • высокая производительность (до 300k http-rps на стенде 16 vCPU, 8 GB, 10 Gb/s) при использовании генератора phantom, которая однако, может быть ограничена производительностью тестируемого веб-сервера - до ~64k http-rps, связанной с количеством одновременно открываемых соединений;
  • наглядная визуализация процесса тестирования с использованием локального веб-сервера, а также наличие подробной статистической информации в консоли;
  • наличие собственного агента для мониторинга ресурсов на стороне сервера с тестируемым веб-приложением по протоколу ssh;
  • возможность использовать Apache Jmeter в качестве альтернативного генератора нагрузки.

2. Настройка и стандартные режимы работы Яндекс.Танка


Для разворачивания нагрузочного стенда мы использовали виртуальную машину с Ubuntu 12.04.2 Server x64.

Установка Яндекс.Танка из пакета осуществляется одной простой командой:
sudo apt-get install yandex-tank-base

Настройка профилей производится в файлах с расширением *.ini, которые должны находиться в директории /etc/yandex-tank/

Запуск Яндекс.Танка осуществляется под рутом одной командой (при этом нужно находиться в директории с ini-файлами):
yandex-tank [ammo_file]

После запуска Яндекс.Танк ищет ini-файлы с конфигурациями профилей нагрузки, а также подгружает, при указании ammo_file, так называемый патрон - специальным образом подготовленный файл с http-запросами. Простейшие GET http-запросы можно указать в файле со стандартным профилем load.ini. Из заданного патрона Яндекс.Танк предварительно готовит ленту запросов, генерируя *.stpd-файлы. Лента хранит сразу все запросы, которые будут отправлены в течение теста для определенного профиля. После смены патрона или настроек профиля она будет сгенерирована заново.

Задание профилей нагрузки через стандартный файл с настройками load.ini

[phantom]
address=example.com:80 ; Target's address and port
rps_schedule=const(65000,2m) ; load scheme
header_http = 1.1
headers = [Host: example.com]
[Connection: close]
uris = /test
[web]
port = 80 ;
interval = 1 ;
manualstop = 1 ;
Задание профиля включает в себя определение тегов с параметрами настроек:
  • используемый генератор нагрузки,
  • адрес нагружаемого веб-сервера и его порт,
  • используемая схема нагрузки.
Далее указываются, формируя при этом патрон, используемый при нагрузке:
  • хедеры http-пакета,
  • путь для GET-запроса (uris).
Дополнительно могут задаваться параметры веб-интерфейса Яндекс.Танка (тег [web] рассмотрен ниже).

Файлы с настройками чувствительны к пробелам: для задания комментария после параметра требуется указывать один пробел и точку с запятой, затем ещё один пробел.

В приведённом примере:

[phantom] - используемый генератор нагрузки, можно также указать модуль JMeter, тег [jmeter].

rps_schedule - схема нагрузки. В ней можно указать одну из функций: const(load,dur), line(a,b,dur), step(a,b,step,dur), либо комбинации данных функций.

Например, комбинация:
rps_schedule=const(1,30s) line(1,1000,2m) const(1000,3h)
задаёт схему нагрузки при использовании которой нагрузка в 1 http-rps будет держаться в течении 30 секунд, затем нагрузка будет линейно возрастать с 1 до 1000 http-rps в течении 2 минут, после чего будет держаться на уровне 1000 http-rps в течение 3 часов.

step(a,b,step,dur) - пошаговое увеличение нагрузки, где a и b начальное и конечное значения нагрузки в http-rps, step - шаг увеличения нагрузки, dur - время, через которое увеличивается нагрузка на указанный шаг.

line(a,b,dur) - линейная нагрузка, где a и b - начальная и конечная нагрузка, dur - время, в течение которого нагрузка линейно увеличивается от a до b.

const(load,dur) - постоянная нагрузка, где load - значение нагрузки, dur - время нагрузки.

Запросы можно размещать как в теге [phantom] файла load.ini, так и в отдельном файле-патроне.

3. Подготовка собственных запросов для Яндекс.Танка


При создании патрона внутри файла load.ini вид запросов может следующий:
header_http = 1.1
headers = [Host: example-domain.ptsecurity.ru]
[Connection: close]
uris = /test
/test1
/test2
Таким образом, для каждого uri будут добавлены заголовки, описанные в headers.

При размещении запросов в отдельном файле вид запросов может быть следующий:
[Connection: close]
[Host: example-domain.ptsecurity.ru]
[Cookies: None]
/?arg
/
/buy
/buy/?rt=0&station_to=7&station_from=9

Для нестандартных запросов, например POST-http, необходимо создавать текстовый файл следующего формата:
[size_of_request] [tag]\n
[body_request]
\r\n
[size_of_request2] [tag]\n
[body_request2]
\r\n

Алгоритм подготовки запросов по формату Яндекс.Танка:

  1. Создать ammo.txt - текстовый файл *nix-формата (с переносами вида \n).
  2. В текстовом редакторе записать пакет как есть: так как он обычно выглядит, например, в браузере.
  3. Добавить в конце пакета последний перенос (тоже только \n без \r).
  4. Сохранить, затем определить длину файла в байтах.
  5. Если в файле один запрос, то перед ним нужно записать первым числом определённый размер пакета в байтах, в конце строки также поставить перенос \n.
  6. Для разделения нескольких запросов в файле нужно вставлять между ними перенос строки \n.
  7. Для GET http-запроса с пустым боди - нужно сделать дополнительный перенос строки \n, чтобы пакет соответствовал RFC 2068.
  8. Последний перенос строки \n для разделения двух запросов также обязателен.
Примечание: вообще, по спецификации RFC 2068, любой http-запрос должен завершаться двумя байтами CRLF (0x0D 0x0A, \r\n), но опытным путем было установлено, что при подготовке ленты запросов Яндекс.Танк сам добавляет нужные окончания строк. А первое число - размер запроса в байтах - он требует вычислять считая, что пакет подготовлен в *nix-формате.

Подготовив таким образом патрон с запросами, достаточно запустить Яндекс.Танк и указать его, а ленту запросов (.stpd-файл) он сгенерит сам:
yandex-tank ammo.txt

Дополнительно можно почитать о способах генерации различных видов запросов с помощью perl-скриптов.

4. Дополнительная конфигурация Яндекс.Танка


Включение веб-монитора


Тег [web] в ini-файле конфигурации подключает модуль веб-монитора. Его основные параметры:
  • port = 80 ; порт на котором запущен веб-сервер,
  • interval = 60 ; интервал в секундах, который должен быть отображен на графике, по умолчанию, 1 минута,
  • manualstop = 1 ; 1 - означает, что для остановки веб-сервера после тестирования в консоли Яндекс.Танка необходимо нажать Enter.


Типы отображаемых графических данных:
  1. На центральном графике отображается схема нагрузки и распределение квантилей времени http-ответов. Переключатели под ним управляют отображением графиков времени, в которое укладывается соответствующее число процентов http-ответов. Также можно включить или отключить отображение схемы текущей нагрузки.
  2. На левом нижнем графике отображается информация о статус-кодах сетевых ответов или протокола.
  3. На правом нижнем графике отображаются усредненные значения временных задержек для отправленных запросов (send), при обработке данных на стороне сервера (latency), полученных ответов (receive) и время соединения с сервером (connect).
Данные графики очень просты и наглядны для анализа. Если в какой то момент времени на них наблюдаются всплески или скачки показателей для среднего времени отклика, резко увеличивается число сетевых ошибок или появляются статус-коды http-ответов 5xx, значит при соответствующей нагрузке веб-сервер начинает испытывать проблемы с производительностью.

Подключение агента Яндекс.Танка для измерения параметров на тестируемом сервере


Для получение метрик с целевой машины - той, на которой установлен тестируемый веб-сервер - необходимо настроить к ней доступ по ssh и выполнить следующие шаги:
  1. Убедиться, что на целевой машине установлен и запущен ssh-сервер.
  2. На стенде Яндекс.Танка создать пару ключей под тем пользователем, под которым будет создаваться нагрузка:
    ssh-keygen
  3. Запустить ssh-агента на стенде Яндекс.Танка:
    exec ssh-agent bash
  4. Добавить агенту сгенеренные ключи:
    ssh-add
  5. Передать на целевую машину сгенеренные ключи:
    ssh-copy-id <hostname>
  6. Указать собираемые метрики в отдельном xml-файле. Пример содержимого такого файла config_file:
    <Monitoring>
      <Host address="example-domain.ptsecurity.ru" comment="Load testing">
        <CPU measure="user,system"/>
        <Memory measure="free,used"/>
        <Net measure="tx,rx,recv,send"/>
      </Host>
    </Monitoring>
  7. В ini-файле с настройками для Яндекс.Танка необходимо указать путь к метрикам, определив тег [monitoring]:
    [monitoring]
    config = config_file ;
После этого, Яндекс.Танк при запуске скопирует своего агента по ssh на целевую машину и будет выдавать значения её метрик в консоли в реальном времени:


Настройка параметров автостопа для тестов


Критерии автостопа теста определяются в секции [autostop] ini-файла с настройками Яндекс.Танка. Для этого задается опция autostop - это список критериев автостопа, разделенных пробелами.

Формат критериев: 
type(parameters)

Базовые типы критериев:
  1. time - остановить тест, если среднее время ответа превышает заданный порог в течение заданного времени, код выхода 21. Например:
    time(1s500ms,30s)
    time(50,15)
  2. http - остановить тест, если число http статус-кодов, соответствующих маске, превысит заданный абсолютный или относительный порог, код выхода 22. Например:
    http(404,10,15)
    http(5xx,10%,1m)
  3. net - аналогично http, но применяется к сетевым статус-кодам, код выхода 23. Допустима маска xx, означающая "все ненулевые".
  4. quantile - остановить тест, если выбранный процентиль находится выше определенного уровня в течение N секунд. Список допустимых квантилей для процентиля: 25, 50, 75, 80, 90, 95, 98, 99, 100. Например:
    quantile(95,100ms,10s)
  5. instances - тип добавляется при подключении модуля phantom. Тест останавливается, если число активных инстансов выше абсолютного или относительного порога, код выхода 24. Например:
    instances(80%, 30)
    instances(50,1m)
  6. metric_lower и metric_higher - срабатывают, если значение метрики ниже или выше порога в течение заданного времени, коды выхода 31 и 32 соответственно. Например:
    metric_lower(127.0.0.1,Memory_free,500,10)
Нужно обратить внимание на то, что имена метрик (кроме кастомных) пишутся не через пробел, а через нижнее подчеркивание. В именах хостов также допустимо использовать маски, например:
target-*.load.net

Продвинутые типы критериев:
  1. total_time - остановить тест, если N% ответов превышает порог времени ответа в течение заданного интервала времени, код выхода 25. От time отличается тем, что аккумулирует информацию. Это означает, что в заданном интервале могут быть моменты времени, которые не удовлетворяют критерию, но весь интервал в целом ему удовлетворяет. Например:
    total_time(100ms,70%,3s)
  2. total_http - остановить тест, если N% (или абсолютное значение) ответов пришли с кодом по заданной маске в течение заданного времени, код выхода 26. Так же использует аккумуляцию данных. Например:
    total_http(5xx,10%,10s)
    total_http(3xx,40%,10s)
  3. total_net - остановить тест, если N% (или абсолютное значение) ответов пришли с кодом по заданной маске в течение указанного промежутка времени, код выхода 27. Аккумулирующий критерий. Например:
    total_net(79,10%,10s)
    total_net(11x,50%,15s)
  4. negative_http - остановить тест, если более N% кодов ответов не подходят под заданную маску, код выхода 28. Например:
    negative_http(2xx,10%,10s)
    В этом примере тест остановится, если количество http статус-кодов неравных 200 OK превысит 10% от общего числа http-ответов.
  5. negative_net - остановить тест если более N% кодов ответов не подходят под заданную маску, код выхода 29. Например:
    negative_net(0,10%,10s)
    В этом примере тест остановится, если доля всех сетевых ошибок превысит 10% за последние 10 секунд.
  6. http_trend - автостоп, который следит за трендом ответов по заданной маске, код выхода 30. Например:
    http_trend(2xx,10s).
    Он означает, что если тренд http статус-кодов 200 OK за последние 10 секунд начал снижаться с учетом погрешности измерения, то требуется остановить тест. Критерий может использоваться для того, чтобы не подбирать отдельно для каждого сервиса и его конфигурации границы времени ответа.

Настройки логирования


Данные о полученных ответах на http-запросы добавляются в процессе тестирования в файлы phout_*.log, которые содержат столбцы в следующем порядке: time, tag, interval_real, connect_time, send_time, latency, receive_time, interval_event, size_out, size_in, net_code, proto_code. Часть этих данных отображается также на веб-мониторе.

Заголовки полей означают следующее:
  • time - unix-время в миллисекундах,
  • tag - кастомный маркер для идентификации и анализа различных URL,
  • interval_real - полное время прохождения пакета, mcs (microseconds),
  • connect_time - время соединения с сервером (например, tcp-handshake), mcs,
  • send_time - время отправки запроса на сервер (с первого до последнего байта запроса), mcs,
  • latency - продолжительность обработки данных на стороне сервера (от последнего байта запроса, до первого байта ответа), mcs,
  • receive_time - время получения данных с сервера (с первого до последнего байта ответа, время загрузки), mcs,
  • interval_event - полное время прохождения данных, mcs,
  • size_out - размер запроса (включая хедеры и спец-знаки \n, \r и т. п.), bytes,
  • size_in - размер ответа (включая хедеры, http статус-коды, POST данные и т. п.), bytes,
  • net_code - статус-код сетевого уровня, например, 0 - OK, 101 - Timeout, 104 - Reset by peer и т. д.,
  • proto_code - статус-код http-протокола, например, 404 - Not Found, 200 - OK и т. д.


Дополнительно можно включить лог запросов и ответов опцией writelog=1 в теге [phantom]. Тогда их запись будет осуществляться в файлы answ_*.log.


5. Использование Яндекс.Танка для сравнения производительности двух аналогичных веб-сервисов


Схема тестовых стендов


В ходе работы нам потребовалось сравнить характеристики двух веб-сервисов, работу которых можно примерно описать как «прозрачные HTTP-прокси, перенаправляющие входящие запросы на backend-приложение». Именно для этого мы использовали Яндекс.Танк.

Общую схему работы можно изобразить следующим образом:


На стенде с Яндекс.Танком использовался генератор нагрузки phantom со включенным монитором производительности.

В качестве стенда web-proxy на схеме использовались два тестируемых веб-сервиса, с которых снимались показатели производительности при помощи агента Яндекс.Танка. Условно назовем их Первый веб-сервис и Второй веб-сервис. Нам требовалось сравнить: соответствует ли производительность Второго веб-сервиса Первому.

Для backend использовалось небольшое веб-приложение, запущенное под Nginx и возвращающее одну простую html-страничку.

Выявленные ограничения


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

Стенд backend-приложения с характеристиками:
8 vCPU, 4 GB, 10 Gb/s. 
и веб-сервер Nginx, установленный на нём, выдерживали режимы нагрузки:
~25000 http-rps - максимальная отдача сервера, которой удалось добиться, но и при нагрузке выше 25k http-rps его работа не была нарушена.

Стенд Яндекс.Танка с характеристиками:
16 vCPU, 8 GB, 10 Gb/s,
позволил реализовать нагрузку:
до 300000 http-rps.

Пропускная способность виртуальной среды ESXi, определенная с помощью Iperf:
8 Gb/s в одну сторону, 4 Gb/s при двухсторонней нагрузке между двумя виртуальными машинами.

Метрики и критерии сравнения


Мы определили и измеряли в процессе тестирования следующие метрики для каждого профиля нагрузки:
  • http_rps_out - значение http-rps отправляемое с Яндекс.Танка на веб-приложение,
  • http_rps_in - значение http-rps принимаемое на Яндекс.Танке со стороны веб-приложения,
  • http_request_size - размер http-запроса в байтах,
  • send_requests - количество отправленных http-запросов,
  • bs_out - bytes per seconds, байт в секунду - параметр определяет скорость отправки данных с Яндекс.Танка,
  • bs_in - значение bs отправляемое с веб-приложения в сторону Яндекс.Танка,
  • test_time - время теста в секундах,
  • response_time_med - среднее время в которое укладывается 90% всех ответов.
Зная число http-запросов и их размер получаем, что bs и http-rps связаны по формуле:
bs = http_rps * http_request_size

Основными критериями для сравнения работы веб-сервисов под нагрузкой мы выбрали следующие:
  1. За всё время теста значение параметра "время, в которое укладывается 90% ответов" у Второго веб-сервиса должно быть не больше, чем у Первого веб-сервиса.
  2. На отрезке возрастания нагрузки на очередные 1000 http-rps значение параметра "время, в которое укладывается 90% ответов" у Второго веб-сервиса должно быть не больше, чем у Первого веб-сервиса.
  3. За всё время теста общее количество правильно обработанных запросов у Второго веб-сервиса должно быть не меньше, чем у Первого веб-сервиса.
Аналогичным образом можно определить иные критерии и профили нагрузочных тестов для своих проектов.

Тестовые http-запросы


Для одного из профилей нагрузочных тестов нам требовалось создать смешанный http-трафик из GET и POST запросов с линейным возрастанием нагрузки до 10k http-rps в течение 10 минут.

HTTP-запросы, вошедшие в патрон Яндекс.Танка:
GET /loadtest/index.php?id=1&login=user&pwd=password HTTP/1.1
X-Sniffer-Forwarded-For: yandex-tank-example-domain.ptsecurity.ru
Host: backend-example-domain.ptsecurity.ru
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)


POST /loadtest/index.php HTTP/1.1
X-Sniffer-Forwarded-For: yandex-tank-example-domain.ptsecurity.ru
Host: backend-example-domain.ptsecurity.ru
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Content-Length: 32

id=1&login=user&pwd=password


POST /loadtest/index.php HTTP/1.1
X-Sniffer-Forwarded-For: yandex-tank-example-domain.ptsecurity.ru
Content-Type: multipart/form-data; boundary=validFile
Host: backend-example-domain.ptsecurity.ru
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Content-Length: 150

--validFile
Content-Disposition: form-data; name="login"; filename="validFile.txt"
Content-Type: text/plain

Valid file content
--validFile--
Для упрощения подготовки патрона для такого смешанного трафика мы сделали скрипты, аналогичные perl-скриптам, предлагаемым на форуме Яндекс.Танка.

Сбор данных и анализ результатов


После подготовки запросов мы просто запустили Яндекс.Танк стандартным образом и выполнили нагрузочный тест со смешанным трафиком для обоих тестируемых веб-сервисов.

Результаты для Первого веб-сервиса


Информация веб-монитора Яндекс.Танка:


Информация консоли Яндекс.Танка:


Результаты для Второго веб-сервиса


Информация веб-монитора Яндекс.Танка:


Информация консоли Яндекс.Танка:


Даже судя по графикам, испытуемый сервис показывал результаты не хуже, чем эталонный. Проверив все три формально определенных выше критерия, мы в этом убедились.
  1. Второй веб-сервис удовлетворяет первому критерию, так как для 90% запросов среднее время ответов для Второго веб-сервиса не превышало такой же показатель для Первого веб-сервиса.
  2. Требование второго критерия выполнялось для каждого этапа нагрузки.
  3. Судя по анализу статус-кодов ответов, записанных в журналы Танка, Второй веб-сервис принял и корректно обработал запросов больше, чем Первый веб-сервис.


6. Выводы


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

Также данный инструмент хорошо внедряется в имеющиеся системы автоматизации. Например, для упрощения работы со стендом Яндекс.Танка: его управлением, запуском, подготовки патронов для лент, контролем за процессом тестирования и сбором результатов, мной, без особых усилий, был написан класс-обвязка на Python, который подключается к стенду по ssh и выполняет все перечисленные действия. Затем он был встроен в нашу существующую систему авто-тестирования.

Дополнительно вы можете посмотреть, как подключить и использовать высокопроизводительную систему Graphit для анализа большого числа графиков, о которой рассказывалось в одной из презентаций на конференции YAC-2013. Её также можно приспособить для нужд нагрузочного тестирования с использованием Яндекс.Танка.