Это статья о том, как мы в Cognianceделали нагрузочное тестирование. Она является отчасти продолжением статьи Димы «Держим 11k req/s», поэтому я не буду повторять информацию об архитектуре приложения, вдаваться в детали наших модулей — все это Вы можете найти там.
Единственная часть, на которой я хотел бы остановиться детальнее, прежде чем перейти к сути, — требования. Самое интересное здесь то, что через 6 месяцев после старта проекта ожидаемая нагрузка увеличилась приблизительно в 100+ раз. Требования к нагрузочному тестированию из разряда «дайте нам хоть что-то» перешли в разряд «мы хотим видеть все виды тестирования, о которых читали».
Требования при старте проекта:
— Request rate: 50 req/sec normal rate
— Request rate high: 150 req/sec peaks
— Protocol: http
— Authentication: no
— Response time : average 99% < 200ms.
Требования после 6 месяцев разработки:
— Request rate: 7000 req/sec normal rate
— Request rate high: 21000 req/sec peaks
— Protocol: https
— Authentication: 2WAY(mutual ssl)
— Response time : 99% total < 100ms.
Я не буду останавливаться на том этапе, когда требования позволяли нам хостить приложение на Димином телефоне, а сразу перейду к той части, где нам необходимо было поднапрячься и придумать, как в корне поменять нагрузочное тестирование. Для модуля, который показывает рекламу (Ad Server), клиент попросил сделать тестирование приблительно так, т.е. используя «Performance Testing Guidance for Web Applications» от Microsoft.
В самом начале нам надо было определить и описать, какие тесты мы можем провести в относительно сжатые сроки. Остановились на следующих тестах:
— Performance Testing. Benchmarking (Simple scenario, Average scenario, Complex scenario)
— Stress testing (Complex scenario)
— Scalability testing (Complex scenario)
— Soak testing (Complex scenario)
— Layer testing
— Production environment. Instance identification.
Расскажу немного деталей о каждом типе.
Performance Testing. Benchmarking.Делая так называемый «Benchmarking», мы пробуем определить:
— как ведет себя приложение в условиях нагрузки, близкой к максимальной, используя сценарии нагрузки от простого до сложного;
-сделать заключение, укладываемся мы в SLA или нет;
— собрать «эталонные» метрики;
Переходя к Stress testing, мы старались понять:
— как «чувствует» себя приложение при нагрузке более установленных лимитов;
— определить точку невозврата. Максимальное количество запросов, которое может выдержать наше приложение;
— понять, при какой нагрузке приложение чувствует себя OK;
Scalability testing.Говоря коротко — понять, насколько мы можем «скейлиться», линейно или нелинейно.
С помощью Soak testingнам нужно было определить, как будет себя вести приложение при пиковой нагрузке на протяжении N времени.
Во время Layer testingмы хотели выявить «узкие» места и проверить каждый модуль в отдельности.
И как итог нашего нагрузочного тестирования от нас ожидали рекомендации к производственной среде.
Теперь давайте попробуем понять, каким образом и используя какие средства можно достичь всего вышесказанного.
Jmeter
Начнем с самого популярного, наверное, решения — Jmeter. Официальный сайт говорит нам следюущее:
«Ability to load and performance test many different server/protocol types.»
К сожалению, даже для нашего простейшего сценария он не совсем подходил. Например, POST запрос (mutual ssl), максимально уникальный, с множеством различных параметров ~3kb, ответ ~3kb и еще несколько GET запросов, которые мы создали на основе ответа.
При таком сценарии, генерируя порядка 300 requests/sec с каждой ноды* (всего нод 5), мастер умирал в распределённом режиме через какое-то время, более того, Jmeter требовал огромного количества ресурсов, в частности, памяти, периодически падая по OOMи не производя ожидаемой нагрузки. Ещё Jmeter использует относительно устаревший протокол для общения между мастером и слейвами — RMI, из-за которого они обмениваются большим количеством данных, что, в свою очередь, плохо сказывается на производительности в распределенном режиме.
*CPU Intel® Core™ i7 CPU, 920 @ 2.67GHz, # of CPU’s — 1, # of cores — 8, memory — 12 gb.
Load UI
Следующим кандидатом, который мы успешно использовали раньше для генерации не столь существенной нагрузки, был Load UI. У такого решения было много «плюшек»:
— отлично работает в распределенном режиме;
— легко писать/ранить свои скрипты;
— легко запускается в non-gui режиме;
— можно добавлять агенты «на лету»;
— красивые графики;
— и еще много другого.
Но снова — слишком большое потребление ресурсов и слишком маленькая производимая нагрузка (а именно — большое потребление памяти и всего ~300 requests/sec с каждой ноды*). Нужно так же отметить, что loadui не поддерживает больше 5 нод в распределнном режиме работы. Даже если воспользоваться платной версией.
*CPU Intel Xeon E5-2680, 2.80GHz, # of cores — 8, memory — 15 gb.
Load UI подходил нам практически всем: относительно удобный интерфейс позволял делать довольно сложные конфигурации для тестов и изменять их «на лету».
Он позволял нам добавлять/убирать агенты в рантайме, и если мы делали смоук тесты или только начинали нагрузочное тестирование нового проекта, позволял делать это в относительно сжатые сроки.
Еще одна «плюшка» Load UI — графики, они очень близки к тем, которые хочет в итоге получить большинство клиентов (или продакт оунеров).
Скрипты для собственных сценариев можно было относительно быстро написать на Groovy. Если же возникали сложности, Dev команда всегда могла быстро помочь в тонкостях написания.
Что же заставило нас отказаться от использования Load UI на проекте?
Ответ уже был дан выше — нагрузка слишком мала, да и количество агентов, которые можно подключить к одной мастер-ноде, также невелика — всего 5.
И самая большая проблема Load UI, которую мы, к сожалению, так и не смогли побороть — «умирающие агенты». Мы долго не могли понять, то ли это проблемы с нашим аппликейшном, или же это проблемы агентов, но приблизительно через 20 — 60 минут агенты в хаотичном порядке «отваливались» или «зависали». После перепроверки другими инструментами проблема была локализована, и это послужило дополнительным толчком к тому, чтобы отказаться от Load UI навсегда.
Велосипед
У нашего клиента был собственный велосипед по нагрузочному тестированию, но решение это было очень сырым и работало не лучшим образом. Во время тестов невозможно было понять, где проблема — на стороне приложения или же на стороне генератора нагрузки.
Tsung
Нашим спасителем стал Tsung. У этого решения тоже есть свои минусы:
— Нельзя добавлять агенты «на лету»;
— Нет возможности написать тест на java/groovy;
— Нет UI;
— Порог входа" немного выше, по сравнению с описанными выше решениями;
— Нет готовых сценариев.
Но при этом он просто делает свою работу, и справляется с этим на отлично:
— Не требует большого количества ресурсов для генерации нагрузки при сложных сценариях;
— Может сгенерировать просто огромную нагрузку (~11k requests/sec с одной ноды*);
*CPU Intel Xeon E5-2680, 2.80GHz, # of cores — 8, memory — 15 gb.
Создатели описывают цель Tsung’a следующим образом:
«The purpose of Tsung is to simulate users in order to test the scalability and performance of IP based client/server applications. You can use it to do load and stress testing of your servers. Many protocols have been implemented and tested, and it can be easily extended.»
Здесь я попробую немного детальнее остановиться на некоторых ньюансах.
UI Tsung’a выглядит приблизительно так:
Все тесты в Tsung — xml, и выглядят приблизительно так:
<http url="/adserver/ad?uid=200_request_%%_rnduid%%&type=TTTT&cellid=%%_rndcell%%&lat=%%_lat%%&lon=%%_lon%%" version="1.1″ method="POST" contents="{" app":nmn","appver":"2345","channels":[],"blocked":["%%_channel%%"],"device":{"device":"9910","time":%%_time%%,"platform":"nokia","sd":2,"sw":360,"sh":240,=«" «os»:"%%_os%%","mf":"bla"},=«" «demo»:[%%_male%%,"carrier="" %%_carrier%%"],=«" «tastes»:[%%_taste%%]}"="">
<http_header name="Accept" value="text/json"/>
<http_header name="Content-type" value="text/json;charset=UTF-8"/>
<http_header name="Accept-Charset" value="UTF-8"/>
<http_header name="X-Forwarded-For" value="1.1.1.1"/>
<http_header name="Accept-Language" value="%%_language%%"/>
<http_header name="User-Agent" value="Mozilla/5.0 (Nokia; U; Nokia 3310; en-US) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.0.0 Mobile Safari/534.11+"/>
<http_header name="Connection" value="keep-alive"/>
<http_header name="Cache-Control" value="no-cache"/>
<http_header name="Accept-Encoding" value="gzip,deflate«/>
Тем не менее, разобравшись, мы поняли, что не все так страшно, как казалось в начале.
Предположим, что мы уже написали какой-то тест и хотим его запустить при помощи Tsung.
Сделать это просто:
$cd /home/ubuntu/.tsung $tsung -f script.xml -l /var/www/ start -l path to logs and results
Узнать текущую генерируемую нагрузку:
$tsung status
Ожидаем увидеть что-то похожее на:
Tsung is running [OK] Current request rate: 7727.28 req/sec Current users: 44 Current connected users*: 45 Current phase: 1
*Current connected users: amount of users generating load
Детальные результаты нагрузки (работает только если включить детальное логирование):
$cd /var/www/20140506-1628 $sudo perl /usr/lib/tsung/bin/tsung_percentile.pl --percentiles 99
И как результат мы должны получить:
Read : tsung-fullstats.log Read : 21245 values for sample,connect Percentile 99 : 51.39892578125 Read : 3342222 values for sample,request Percentile 99 : 13284.6879882812 Read : 1764201 values for sample,page Percentile 99 : 20497.458984375
Tsung также позволит нам сгенерировать графики:
Они не настолько красивы и информативны, как у Load UI, и, скорее всего, их надо будет еще «обработать напильником», прежде чем отдать клиенту или продакт оунеру:
$sudo perl /usr/lib/tsung/bin/tsung_stats.pl --dygraph
Нюансы тестирования с Tsung:
— Если вы хотите, чтобы master node участвовал в нагрузке, ему нужен будет беспарольный ssh доступ на себя, помимо беспарольного ssh на все агенты.
— Закончились локальные порты (детальнее):
echo 5 > /proc/sys/net/ipv4/tcp_fin_timeout echo 15000 65000 > /proc/sys/net/ipv4/ip_local_port_range
Итог
При приблизительно* одинаковых условиях мы получили следующую нагрузку при помощи вышеописанных средств:
*Незначительные отличия в инстансах и мелкие изменения в теле тестов
JMETER ~ 300 requests/second per node. Total nodes 5. Total load ~ 1,5k req/sec.
LOAD UI ~ 300 requests/second.Total nodes 5. Total load ~ 1,5k req/sec.
TSUNG — 11k requests/second. Total nodes 2. Total load ~ 21k req/sec.
CUSTOM — 1k headaches/second. Total headache.
В заключение я хочу привести пример того, какие результаты отдавать клиенту или продакт оунеру.
Самому нетребовательному, думаю, будет достаточно графиков, которые вам сгенерирует Tsung. Но так или иначе, чтобы понять, насколько хорошо (или плохо) перформит ваш апликейшн, следующие метрики могут быть полезны:
- Test resume. I.e.:
- Time for 99 % of connections, ms
- Time for 99 % of ad requests, ms
- Time for 99 % of ad event, ms
- Test Specification. I.e.:
- Amount of entities in the system
- % of served requests
- % of non-matched
- Requests stats. I.e.:
- tomcat localhost_access.log
- Instance specification. I.e.:
- Amazon c3.2xlarge, Redis: Amazon ElastiCache m2.xlarge.
- Result metrics. I.e.:
- Stats from agent:
- Name
- highest 10sec mean
- lowest 10sec mean
- Highest Rate
- Mean
- Count
- Common number of requests/ concurrent users from Agent
- Graphs
- Conclusion
Какие выводы мы успели сделать во время тестирования:
- Новое соединение — это очень $$$. Пример:
- Open/close connection ~ 70 ms;
- Serve request within open connection ~ 12 ms;
- RTFM. Без комментариев;
- Разбивайте систему на части. Иногда сложно «угадать», где проблемы, и можно потратить на это много времени;
- Тестируйте любой, даже, казалось бы, самый «хорошо описанный в интернете» солюшн. Это позволит вам не переписывать систему потом;
- 21k req/sec — это далеко не предел.
Я и ребята попробуем ответить на Ваши вопросы, если такие будут.