![](https://habrastorage.org/getpro/habr/upload_files/353/e84/90c/353e8490c4b856d28d382267cc618af8.png)
Привет, Хабр!
Сегодня расскажем, как мы в MANGO OFFICE занимаемся нагрузочным тестированием и экономим время и ресурсы команд, применяя неочевидные решения и подходы.
Мы проводим через фазу нагрузочного тестирования большинство наших систем при каждом релизе. Без такого тестирования сложно поддерживать качество систем и избежать деградации времени отклика, стабильности и доступности.
Нагрузочное тестирование — это сложно и дорого. И чем сложнее система, тем дороже его разработка и поддержка. Если тесты ловят какую-то аномалию, начинается процесс ре-тестов, подтверждения производительности или ее деградации. Релизы откладываются или переносятся. Сонные, злые и небритые разработчики корпят над кодом, а в их глазах стоит немой вопрос: где тот косяк в новом функционале, из-за которого все сломалось?
При этом в процессе нагрузки сложных систем всегда множество нюансов — от особенностей самой системы, наличия интеграций и кешей до человеческого фактора в лице DevOps-инженеров, администраторов и разработчиков. Само нагрузочное тестирование тоже не всегда может дать четкие результаты. Поэтому часто в проектах нагрузки допускается определенный уровень ошибок, при котором результат все еще считается валидным.
Ликбез: проблемы с нагрузочным тестированием и как их решать
Немного предыстории. В нашей компании много систем асинхронной обработки событий. В основном они работают около телефонии, на интеграциях между виртуальной АТС и внешними системами типа AmoCRM или Bitrix.
В продакшне это выглядит следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/815/57b/e63/81557be6342555edf312a385683efa6e.png)
В нагрузочном тестировании мы используем Apache Jmeter, которым эмулируем нагрузку от внутренних сервисов и телефонии на системы интеграции.
В ходе тестирования мы смотрим на время отклика приложений интеграции, на то, как система реагирует на события, и далее по списку: ЦПУ, RAM, HEAP. Отдельно замеряем время от поступления события в очередь на RabbitMQ до момента отправки этого события в виде HTTP-запроса согласно бизнес-логике.
Если вы строили стенды для нагрузочного тестирования, то наверняка сталкивались с вопросом: как грамотно связать два совсем не связанных друг с другом события? В данной модели нагрузки у нас отсутствует четкое понятие «запрос–ответ». Ответ может прийти асинхронно и совсем не в ту систему, с которой мы даем нагрузку, так как запрос для нас — это событие в RabbitMQ, а ответ — HTTP-запросы во внешнюю систему.
Чаще всего такие задачи решают так:
![](https://habrastorage.org/getpro/habr/upload_files/2c5/6c2/f66/2c56c2f66295c520107dd58a84ef8ffa.png)
Первый способ — обработать логи приложений и поместить их в базу для дальнейшего анализа. Второй способ — заставить заглушку писать в эту базу и потом потратить время на разбор результатов или написать дополнительные скрипты. Но любые решения — это всегда компромиссы и множество нюансов.
Минусы первого способа:
1) Необходимо дополнительно идентифицировать и связывать записи об отправленных событиях в RabbitMQ c записями в логах об исполненных HTTP-запросах, что не всегда тривиальная задача.
2) В случае наличия нескольких нод имеем несколько источников логов.
3) Логи — не всегда надежный источник.
4) Обязательно что-нибудь пойдет не так, особенно на высокоинтенсивных тестах.
В итоге результаты нагрузки будут врать. Команда продукта не сможет доверять нечетким результатам, которые сопровождаются невнятными доводами вроде: «Процент идентифицированных ивентов немного отличается от эталонного — возможно система теряет события».
Минусы второго варианта:
1. Требуется держать еще одну базу для того, чтобы накапливать события и сопоставлять их с запросами.
2. Заглушка может дополнительно влиять на результаты.
Оба представленных подхода имеют серьезные минусы, из-за которых снижается доверие к результатам тестирования. Мы все еще не можем гарантировать, что приложение не теряет данные под нагрузкой.
Как мы работали раньше
Несколько лет мы проводили релизы, обрабатывая логи с помощью Python после теста и подмешивая их к результатам. Но в какой-то момент владелец продукта пришел к нам с проблемой: возникло подозрение, что система начала терять данные. На нагрузочных тестах такое не воспроизводилось, но и четко сказать, что этого нет — мы не могли. Процент потерь логов был не выше обычного уровня в 10–15 %.
Подсчеты показали, что на обработке логов мы теряли до полутора миллионов событий за один тест. При том, что, во время тестов Jmeter генерировал около 10 миллионов событий. Плюс от теста к тесту результаты плавали: иногда это был миллион, иногда почти два.
Мы решили придумать новый механизм, который давал бы нам стопроцентную уверенность, сделала ли система под нагрузкой свою работу за отведенное в SLA время или нет. Это позволило бы нам понимать, нужно ли обращаться к разработчикам.
Техническая реализация
Для начала нужно было придумать, как засекать обращения в CRM быстро, просто и без использования дополнительных ресурсов, новых сервисов типа KAFKA, сложных расчетов на БД или переписывания ядра Jmeter.
Так как в проекте мы уже получали часть событий по Websocket, решили и дальше развивать идею в этом направлении. В итоге разработали следующую схему:
1) Создали свой плагин для Apache Jmeter — для поддержки нашего тестового WebSocket-протокола.
2) Слегка докрутили приложение: добавили в некоторые запросы сервисные хедеры для более точной идентификации HTTP-запросов.
3) Доработали профили нагрузки:
a) Сделали дополнительную инициализацию подключения на старте.
b) Написали свой Jmeter Sampler, которому на вход передавался ключ для идентификации события в кеше, а также время отправки изначального запроса в RabbitMQ. Данные семплеры отрабатывали в конце сценария и идентифицировали прилетевшие ответы в кеше по ключу. На выходе они создавали стандартный для Apache Jmeter объект с результатом, который обычным способом уходил дальше на обработку в Jmeter.
Схема нагрузки получилась такой:
![](https://habrastorage.org/getpro/habr/upload_files/9bf/caa/bbb/9bfcaabbbc419d891788163d8c9e3468.png)
Сценарий нагрузки выглядит следующим образом:
На старте мы инициируем дополнительное WebSocket-подключение к заглушке.
JM по сценарию начинает генерировать ивенты в шину, предварительно сохраняя внутренние ID абонентов.
Записи с параметрами звонковых событий складываются в специальную очередь. Таким образом фиксируются параметры и «образно» listener на асинхронное событие.
Приложение начинает реагировать на это, совершая HTTPs-запросы в CRM, в нашем случае — в заглушку. В хедерах запросов дополнительно пробрасываются ID, если их нет в теле запросов.
Заглушка при поступлении HTTP-запросов совершает асинхронную отправку ивентов в формате JSON во все подключенные к ней WebSocket-сессии. (Вообще хотели в protobuf, но нам хватило и JSON реализации.
Jmeter асинхронно получает эти события и складывает их в кеш (по сути это обычная хеш-мапа, без всякой магии типа Chronicle Map).
При этом отдельная независимая тред-группа обрабатывает очередь ожидания событий и фиксирует наступление асинхронного события по мере появления событий в кеше. Чем достигается практически true асинхронность и независимость потоков асинхронных и обычных событий.
Далее Jmeter берет из события timestamp поступления HTTP-запроса, сверяет его со временем отправки соответствующего события в RabbitMQ и формирует стандартный SampleResult, который поступает в движок Jmeter совершенно стандартным способом.
Jmeter обрабатывает этот результат как еще один семпл.
Что получилось в итоге
По такой схеме мы тестируем наши системы интеграции уже почти три года. Внедрение заняло пару месяцев, в основном за счет тонкой корректировки точек отсчета для асинхронных событий.
Чего мы добились:
Появилась уверенность в системе (confidence).
Возможность отследить и зафиксировать практически каждое событие: как его наступление и время отклика, так и его отсутствие.
У продуктовой команды сильно выросло доверие к результатам нагрузочного тестирования.
Полное отсутствие ошибок во время тестирования стало нормой.
Ошибки на этапе нагрузки стали серьезным поводом инициировать расследование и откладывать выпуск функционала до их устранения.
В целом данная схема хорошо себя зарекомендовала в задачах НТ, но развитие ее далеко от завершения, поскольку надежная основа лишь стимулирует совершенствование и расширение функционала.
Статья подготовлена Иваном Приходько, ведущим инженером по нагрузочному тестированию.
Подписывайтесь на наши соцсети:
Аккаунты Mango Office
ВКонтакте: https://vk.com/mangotelecom
Телеграм: https://t.me/mango_office