Разделы портала

Онлайн-тренинги

.
Нагрузочное тестирование выполнять сложно, а инструменты далеки от совершенства. Почему?
14.01.2021 00:00

Автор оригинала: Nicholas Tietz-Sokolsky

Оригинальная публикация



Если вы создаёте приложение, которое должно масштабироваться — а все мы надеемся, что наши приложения будут расти — то в определённый момент нам приходится разбираться, может ли оно это делать на самом деле. Именно тогда на помощь приходит нагрузочное тестирование: если вы хотите узнать, справится ли ваше приложение с крупными масштабами, то мы просто генерируем эти масштабы и проверяем! Звучит достаточно просто.


Но потом мы пробуем действительно сгенерировать нагрузку. Это делается легко, только если ваше приложение ужасно простое, ведь тогда можно использовать что-нибудь типа Apache JMeter для генерации повторяющихся запросов. Если у вас это получится, то я вам завидую: все системы, с которыми мне приходилось работать, сложнее и требовали более изощрённой схемы тестирования.

Если ваше приложение становится чуть сложнее, то вы переходите к инструментам наподобие Gatling. Они позволяют симулировать виртуальных пользователей, выполняющих различные сценарии, что намного полезнее, чем простая осада одного или нескольких URL. Но даже этого недостаточно, если вы пишете приложение, использующее одновременно WebSockets и HTTP-вызовы в течение долговременной сессии, а также требующее повторения по таймеру определённых действий. Возможно, я серьёзно недоглядел чего-то в документации, но мне не удалось найти способа, допустим, настроить периодическое событие, запускаемое каждые 30 секунд и выполняющее определённые действия при ответе на сообщение WebSocket, а также производящее действия по HTTP, и всё это в рамках одной HTTP-сессии. Я не нашёл такой возможности ни в одном инструменте нагрузочного тестирования (и именно поэтому написал на работе свой собственный инструмент, который надеюсь выложить в open source, если найду время на подчистку кода и отделения его от проприетарных частей).

Но предположим, что у вас есть стандартный инструмент наподобие Gatling или Locust, который работает и удовлетворяет вашим нуждам. Отлично! Теперь давайте напишем тест. По моему опыту, сейчас это самая сложная задача, потому что нам сначала нужно разобраться, как выглядит реалистичная нагрузка — вас ждёт один-три дня кропотливого изучения логов и ведения заметок по показателям сетевых инструментов в браузере при работе веб-приложения. А после того, как вы узнаете реалистичную нагрузку, то вам придётся написать код, который сводится к следующему: подмножество вашего приложения будет притворяться пользователем, общаться с API и выполнять действия, которые совершает пользователь.

И на этом ещё ничего не закончилось! Здорово, мы написали нагрузочный тест, и он реалистичен. Но задача постоянно меняется, ведь выпускаются обновления. То есть теперь у нас появилась проблема поддержки: как обеспечивать актуальность нагрузочного теста при изменении приложения? Для этого нет качественных инструментов и почти ничто вам не поможет. Придётся сделать это частью процесса и надеяться, что вы ничего не упустите. Такой ответ не радует, и именно поэтому этот аспект является одним из самых сложных при тестировании приложения.

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


Откуда берётся сложность

По сути, ситуация такова:

  • Большинство инструментов нагрузочного тестирования поддерживает только простейшие нагрузки, и даже самые сложные не позволяют выполнять все необходимые действия для симуляции реального использования веб-приложения.
  • Самой сложной частью является написание теста с симуляцией реального использования, даже если инструменты поддерживают нужные вам возможности.
  • Второй по сложности задачей является поддержка теста, и инструменты в этом нам совершенно не помогают.

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

Симуляция пользователей. Действительно ли она нужна?

Здесь я могу ответить «да», хотя это может зависеть от конкретного приложения. В данном случае подразумевается пользователь сервиса: если у вас монолит, то это все ваши пользователи, но в случае микросервисов «пользователем» может быть один из сервисов! При создании приложений, над которыми я работал, мне удалось достичь незначительного успеха с таргетированными тестами отдельных конечных точек. Но в конечном итоге для этого требовалась такая сложная система, что это оказалось ненамного проще самого нагрузочного теста! И хотя мы получили некоторые результаты и усовершенствования, покрыть всё не удалось (в приложении могут быть взаимодействующие конечные точки) и мы не смогли получить реалистичную нагрузку.

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

Вероятно, лучшее, что вы можете здесь сделать, происходит на этапе проектирования API и системы, а не на этапе тестирования. Если упростить API, то тестировать придётся гораздо меньшую площадь поверхности. Если спроектировать систему с более независимыми частями (например, когда у каждого сервиса есть своя база данных), то их проще будет тестировать по отдельности, чем монолит. К тому же, при этом вы сможете использовать более простой инструмент — двойная выгода!

Написание тестов — сложная задача. Как и их поддержка

Создавать тесты сложно, потому что требуется выполнить несколько задач: нужно разобраться, каков поток использования API и написать симуляцию этого использования. Для понимания потока необходимо понимание других систем, кроме тестируемой, и поскольку ваша система вряд ли подробно рассматривается в их документации, не будет чёткой диаграммы того, когда и что вызывается; часто приходится разбираться в логах, пока не поймёшь, какова же истинная схема использования. Далее идёт написание этой симуляции — тоже нетривиальная задача, поскольку необходимо обрабатывать состояние большого количества акторов, представляющих собой пользователей вашего API!

А, да, и теперь вам нужно писать для всего этого ещё и интеграционные тесты.

Существуют исследования о том, как упростить некоторые из этих задач. Например, можно разобраться в том, что вам нужно для первоначального теста, и распознавать регрессии (отсутствующие новые нагрузки) по автоматизированному анализу логов. Но насколько я понимаю, на GitHub, не говоря уже о коммерческой доступности, нет продукта, способного выполнить за меня эту задачу. Поэтому похоже, что в нашей отрасли подобные системы не получили широкой поддержки. Для самостоятельной реализации этот проект был бы слишком большим, и в этом может быть причина его увядания (или он реализуется в больших компаниях и о нём не говорят).

Может быть, не подвергать всё нагрузочному тестированию?

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

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

  • Старый добрый анализ. Усесться с ноутбуком, ручкой и пониманием системы в целом, выделить на это полдня, и вы получите примерные расчёты основных параметров и ограничений масштабирования системы. Когда вы наткнётесь на «бутылочное горлышко» или встретитесь с неизвестными переменными (сколько транзакций в секунду может поддерживать наша база данных? Сколько мы их генерируем?), то можно будет протестировать конкретно их!
  • Разворачивание фич. Если вы можете медленно разворачивать фичи на всех пользователей, то вам может и не понадобиться нагрузочное тестирование! Вы можете измерять производительность экспериментально и проверять, достаточно ли её. Достаточно? Разворачиваем дальше. Мало? Откатываемся назад.
  • Повторение трафика. Это совершенно не поможет с новыми фичами (для них воспользуйтесь предыдущим пунктом), но поспособствует пониманию критичных точек системы для уже имеющихся фич, при этом не требуя большого объёма разработки. Вы можете взять отслеженный ранее трафик и повторять его (многократно снова и снова, даже комбинируя трафик различных временных периодов), наблюдая за производительностью системы.