Вы наверняка знаете, что есть большая разница между тем, как будет работать ваше приложение/сервис в зависимости от того, сколько пользователей его используют. То, что работало во время разработки, может развалиться, когда придут первые реальные пользователи со своим окружением, а то, что работало с сотней пользователей, может умереть, когда их станет 10 тысяч. Или бывает, что вы все потестили на искусственных данных, а потом ваша база начинает торзмозить из-за пользователя с именем İnari.
О том, как выживают баги, когда «включать» в проекте нагрузочные тесты, откуда брать для них данные и можно ли вообще не тестировать, вывалив результаты сразу в продакшн, мы поговорили с Алексеем Лавренюком («Яндекс») и Владимиром Ситниковым (Netcracker).
Пару слов о себе и своей работе. Как ваша работа связана с тестированием?
Алексей Лавренюк: Я разработчик в «Яндексе», в службе нагрузочного тестирования. Делаю инструменты и сервисы для тестирования производительности. Можно посмотреть на наши open-source инструменты для нагрузочного тестирования – Yandex.Tank и Pandora, и на наш сервис для нагрузочного тестирования – Overload. Сейчас открыт бесплатный доступ к его бета-версии.
Раньше мы умели тестировать только производительность серверных приложений, теперь осваиваем мобильные приложения – это новый тренд. Например, мы тестируем энергопотребление телефонов. Прочитать про это можно тут.
Владимир Ситников: Я уже 12 лет работаю в компании Netcracker. Сейчас я занимаю позицию инженера по производительности, т.е. я не тестирую продукты, но наблюдаю, как ведет себя система в реальной эксплуатации и в тестах. В сферу моей ответственности входит анализ того, как те или иные решения в сфере разработки и дизайна влияют на финальный результат. Поэтому я зачастую не просто смотрю на результаты тестирования, но и планирую его. В основном это относится к нагрузочному тестированию.
Когда-то я и сам занимался тестированием, но сейчас это осталось в прошлом.
— Давайте немного поговорим о теории. Корректно ли говорить, что проблемы на стадии тестирования появляются из-за разработчиков, не учитывающих какие-то параметры задачи или использующих неподходящее решение?
Алексей Лавренюк: Нельзя все предугадать заранее. В типичном сервисе очень много крутилок: для решения продуктовых задач можно выбирать разные алгоритмы, подбирать параметры этих алгоритмов, использовать библиотеки, фреймворки, настройки сервиса в продакшне, параметры железа. В сочетании эти крутилки дают миллиарды комбинаций, и без глубокого понимания того, как работает сервис, перебирать их можно очень долго. А понимание это нельзя получить, если у тебя нет инструмента для проведения экспериментов и анализа их результатов.
Нагрузочное тестирование — это не дубинка, которая бьет разработчиков по голове, когда они написали непроизводительный код. Это очень мощный измерительный прибор, по сути осциллограф для электронщика, позволяющий прощупать код и найти в нем узкие места. А потом после оптимизации увидеть (и продемонстрировать) результат в цифрах и графиках.
Владимир Ситников: Я бы хотел дополнить. «Откуда вообще возникают баги» — это вопрос тысячелетия. И он тесно связан с другим интересным вопросом — почему, несмотря на все тестирование, баги доживают до реальных систем и появляются только там. Почему в ходе тестирования мы их не находим? Неужели мы неправильно ставим задачу?
На мой взгляд, это связано с тем, что команды разработки и поддержки — разные. Одни люди делают решение и совсем другие — занимаются его поддержкой в ходе эксплуатации.
Половина всех проблем на продакшене — это сочетание 2-3 дурацких ошибок или допущений разработки: кто-то создал неправильный код, использовал дурацкий алгоритм и т.п. Каждая такая ошибка сама по себе не сказывается на производительности — современное железо довольно мощное, много чего переваривает. Но вместе две или три ошибки «выстреливают». А из-за разделения разработки и поддержки авторы этих ошибок о них и не узнают.
Редко когда по факту ищут авторов кода и говорят: «Пожалуйста, так больше не надо писать». Но чтобы гарантированно не совершать подобных ошибок в будущем, разработчику надо получить этот опыт. При поддержке своей собственной системы выводы из «набитых шишек» делались бы быстрее.
— Есть ли смысл в раннем нагрузочном тестировании (на этапе разработки)?
Алексей Лавренюк: Самые дорогие ошибки получаются тогда, когда нагрузочное тестирование притащили за уши на готовый проект (я уже не говорю о тех случаях, когда код тестируют в продакшене). Код написали, функционально он работает, но выясняется, что там, где требуется 10 ответов в секунду (всего-то), сервис может переварить только один, да и то со скрипом. Еще хуже, если в этом виноват фундамент – фреймворк, который выбрали, потому что «им все пользуются» или «ну он такой новый, прикольный». То есть придется переписывать вообще все.
Чем раньше отлавливаются проблемы, тем проще их решить.
— То есть начинаем разрабатывать и одновременно запускаем тестирование?
Владимир Ситников: И да, и нет. Иногда на ранней стадии надо совсем грубо посмотреть, что происходит. Это может иметь смысл. Но, как правило, вначале код не работает. А замерять производительность кода, который, допустим, возвращает неправильный ответ, — гиблое дело. Мы тратим время, ресурсы (в том числе машинные), а результат — неизвестный.
Однако запуск тестирования — история довольно длительная. Перед тем как что-то замерить, нужно понять, что именно замерять и на каких данных, закончатся ли у нас эти данные по ходу теста, как мы будем их возобновлять, за какими метриками будем следить, какие у нас есть ожидания относительно результатов и т.п. В совсем небольшом проекте все это можно провернуть за день-два, просто обсудив и приняв нужное решение. Но для более-менее крупного проекта подобные вопросы за один день не решаются, тем более тут требуется тесное взаимодействие с заказчиком.
Бывают случаи, когда заказчик приходит с определенными требованиями с самого начала, но чаще их приходится формулировать только на этапе «подключения» нагрузочного тестирования.
Поэтому думать о нагрузочном тестировании стоит с самого начала. И я бы не рассматривал нагрузочное тестирование как конечный продукт. Это некий процесс, которому надо следовать, чтобы избежать определенных ошибок. По мере создания решения (в процессе перехода непосредственно от разработки к тестированию и выводу на продакшн) меняется и назначение нагрузочного тестирования. Сначала оно используется для отладки, затем — для поиска ошибок, и в конце — как критерий того, что решение готово к продакшн.
Здесь мне кажется уместной аналогия с обычным тестированием. Когда его надо включать? Отнюдь не в конце разработки. То, что в конце разработки придут тестировщики и все исправят, — это миф. Тестирование не исправляет, а ищет ошибки. И нагрузочное тестирование — это инструмент поиска ошибок определенного рода. Однако в отличие от обычного тестирования, проверяющего по сути только возможность прохождения программы по ветке алгоритма до определенной точки, нагрузочное представляет собой бета-тест продакшна, что ведет к отличиям в данных, нагрузке, соотношении тестов в миксе и т.п.
Чтобы успешно находить ошибки, надо иметь критерии поиска. Поэтому как процесс нагрузочное тестирование запускается ближе к началу разработки, а по факту оно проходит условно после первого цикла тестирования (автоматического или ручного), когда кто-то дает отмашку, что в целом система ведет себя правильно.
— Понятно, что абсолютно все не протестировать. Какие моменты необходимо тестировать в первую очередь (в ракурсе нагрузочного тестирования)?
Алексей Лавренюк: Тестировать в первую очередь надо критический сценарий — то есть тот, который приносит деньги. И провести как минимум два вида тестов: на разладку, чтобы определить пределы производительности, и на измерение таймингов, чтобы убедиться, что сервис укладывается в SLA. То есть сервис обязательно нужно «добить» и померить тайминги на том уровне нагрузки, который предполагается в продакшн.
— С чего необходимо начинать нагрузочное тестирование?
Алексей Лавренюк: Перед тем, как начинать нагрузочное тестирование, нужно убедиться, что перед этим провели функциональное и поправили все баги. Причем именно на вашем стенде. Удостоверьтесь, что в середине вашей стрельбы к вам на стенд никто не придет, чтобы скачать пару сотен гигабайт. В общем, подготовьте удобное тестовое окружение, в котором вам никто не будет мешать.
Владимир Ситников: Само тестирование стоит начинать с формулирования нефункциональных требований — т.е. требований по производительности и стабильности.
Наиболее типичные нефункциональные требования (важнейшие количественные оценки приложения) — размер обрабатываемых данных, время их обработки и частота запусков. Какая-либо из этих метрик встречается почти в каждом проекте.
Понятие «нефункциональные требования» обширно. В разных проектах их составляющие могут иметь различный приоритет. Например, «система должна быть способна отработать 3 дня без перезагрузки» или «в случае пропадания и восстановления связи с базой данных приложение должно вернуться к нормальной работе не более чем за 2 минуты» — это тоже нефункциональные требования.
К слову, понятие «нефункциональные требования» формирует представление, что это нечто самостоятельное. Но по факту эти требования, описывающие, как должен работать функционал. Т.е. без нефункциональных требований и функциональные теряют смысл, равно как невозможно приложить NFR к любому случайно выбранному функциональному требованию.
— Существуют ли какие-то общие рекомендации по проведению нагрузочного тестирования?
Владимир Ситников: В целом нагрузочное тестирование похоже на обычное. Мы берем наиболее важные сценарии или те, за которые мы боимся больше всего. Поэтому кто-то с пониманием проекта должен прийти и указать на наиболее опасные сценарии. Дальше мы уже занимаемся автоматизацией и проводим замеры.
— Насколько большую роль играет интерпретация результатов нагрузочного тестирования?
Владимир Ситников: Огромную. Важны не цифры, а понимание, почему они именно такие. Если у нас получилось 42, это не значит, что результат — хороший. Заказчик просил, чтобы работало не дольше минуты, а у нас получилось полминуты. Мы молодцы, расходимся? Нет! Важно понимать, почему мы не могли сделать быстрее, во что мы упираемся. А еще надо быть уверенным в том, что отчет — реален.
Был такой пример: мы замеряли, как влияет виртуальная машина на производительность приложения, т.е. сравнивали производительность приложения, работающего на железе, и приложения, работающего на виртуальной машине. Сделали замеры, получили хорошие цифры (близкие к ожидаемым) — расхождение в производительности в несколько десятых процента. На этом все могло закончиться. Но кто-то внимательнее посмотрел на результаты и понял, что у нас вместо запуска приложения возвращалась страница с ошибкой логина. В тестах мы не прошли по реальному приложению. Вместо этого под видом каждого из шагов мы проверили скорость логина, который на неправильном пароле выдал отказ.
О чем говорит этот пример? О том, что без анализа того, что творилось внутри во время тестов, у нас возник неправильный вывод. Поэтому с точки зрения нагрузочного тестирования важны не полученные цифры, а понимание того, чем эти цифры объясняются.
Алексей Лавренюк: Очень часто люди недооценивают важность графиков, смотрят только на итоговые статистики. Если вы тоже так делаете, рекомендую погуглить «квартет Энскомба» и посмотреть вот эту статью от Autodesk.
Многие популярные в интернете нагрузочные инструменты, например ab, дают только итоговые статистики и ложную уверенность в работоспособности сервиса под нагрузкой. Они скрывают детали, провалы на графиках. Такой провал может стоить денег (покупатель ушел), а исправить его очень просто (поправить параметры garbage collector).
Кроме того, многие популярные и дорогие нагрузочные инструменты архитектурно устроены неправильно и лгут вам. Об этом написано в этой статье.
— Какие есть особенности нагрузочного тестирования веб-сервиса? По идее тестировать надо не только код, но и окружение, как это делается?
Алексей Лавренюк: Тестировать надо все, что вас интересует. Если вы подозреваете, что производительность приложения зависит от пробок в Москве, найдите способ это протестировать. Я не шучу, наши геосервисы запускают автоматически нагрузочные тесты на данных о пробках, когда их уровень достигает 7 баллов.
Еще у нас на тесте было мобильное приложение, которое активизировалось только в том случае, если телефон приходил в движение. То есть телефон во время теста нужно было шатать. Поэтому эти телефоны мы носили с собой. Была даже идея построить специальную шаталку телефонов, но приложение отправили на доработку, и необходимость в автоматизации пока отпала.
— Откуда берутся данные для нагрузочного тестирования вашего типичного проекта? Используются ли моки или данные с продакшн?
Владимир Ситников: Я бы не сказал, что к данным есть какой-то типичный подход. Проекты все-таки разные. Есть такие, где системы вообще не хранят данные, а являются промежуточными звеньями в некой цепочке. Для их тестирования мы просто генерируем необходимую информацию. Иногда, наоборот, системы что-то хранят. Если это, например, история записей, проблем, вроде как, нет. Но если системы хранят какое-то состояние (БД и т.д.), то здесь проявляются нюансы.
На начальном этапе, когда у нас нет никакого дампа продакшена, данные приходится генерировать (обсуждать бизнес-структуру и генерировать нужное количество подходящего сорта данных). К сожалению, я бы не сказал, что это успешно работает. Сгенерированные данные позволяют пережить какое-то время, но, к сожалению, получить таким образом данные, похожие на правду и подходящие для разных сценариев, тяжело. И начинаются игры, когда мы генерируем часть данных для одного типа запросов, часть — для другого, часть — для третьего. Нам удобнее генерить частями, но при этом получаются перекосы: либо данных слишком много (потому что мы по каждому типу сценариев хотим иметь запас), либо мы пропускаем какие-то сценарии. Из-за этого рано или поздно мы переходим на нечто более-менее похожее на дампы — импорт из внешних систем или продакшн с маскированием.
С импортом тоже есть свои проблемы. Мы работаем в телеком-сегменте, где дампы продакшн-систем нельзя копировать направо-налево. Там присутствуют определенного типа данные, которые просто так копировать нельзя. Поэтому приходится изобретать, как сделать так, чтобы система вроде бы работала, но данные были заменены на звездочки и т.д.
— Не проще тестировать сразу на продакшене, подключая какую-то долю пользователей?
Алексей Лавренюк: В нашем случае это не тестирование, это аккуратный релиз. По-хорошему, всегда, даже после тестирования, нужно выкатывать на долю пользователей, потому что в продакшне может пойти все не так, как в тестинге.
Чем подключение доли пользователей отличается от нагрузочного тестирования? В этом случае у вас нет возможности полностью управлять нагрузкой, вы не можете довести сервис до предела (пользователи просто 500 ошибку начнут получать), вы не можете безнаказанно крутить на нем ручки и замониторить столько, сколько могли бы на тестовых серверах.
С другой стороны, можно исхитриться: часть трафика с продакшн направить на ваши тестовые сервера и там использовать нагрузочные инструменты. Вот это уже нагрузочное тестирование, просто исходные данные берутся в реалтайме с продакшн окружения.
Иными словами, если вас устраивает ответ «держим/не держим» (причем с вероятностью разочаровать какую-то долю своих пользователей), то можно и не тестировать нагрузочно. Если же вы хотите знать пределы производительности, узкие места, в какие мониторинги лучше смотреть, за какие крутилки в первую очередь хвататься и к кому бежать – то все-таки сделайте нагрузочное тестирование.
Владимир Ситников: Это не наш случай. Существуют компании, которые без проблем тестируют в продакшене. У них есть часть клиентов, которая может быть перенаправлена на новую версию кода. В нашем случае речь идет о системе, важной для бизнеса наших заказчиков. Если она не работает, заказчик теряет деньги. Поэтому и нагрузочные тесты проводим до попадания кода в эксплуатацию.
Наши системы важны для бизнеса заказчиков, и во многих случаях ее устанавливают и обслуживают совершенно другие люди — представители заказчика. Поэтому у наших заказчиков нет желания экспериментировать на боевой системе, и нагрузочные тесты проводятся до ввода в эксплуатацию.
— Есть ли у вас какой-то излюбленный инструментарий для проведения нагрузочного тестирования?
Владимир Ситников: Конечно. У нас 3 основных инструмента: для тестирования серверной части мы используем Apache JMeter, для тестирования браузера мы используем Selenium и для тестирования Java — JMH. Эти инструменты закрывают подавляющее большинство потребностей.
То, какие инструменты для тестирования предпочитает использовать Алексей Лавренюк, вы можете узнать у него лично на конференции по тестированию Гейзенбаг 2017 Piter. Перед тем, как побеседовать со всеми желающими в дискуссионной зоне, Алексей выступит с докладом о нагрузочном тестировании web-проектов, где в режиме real time show будет «обстреливать» демонстрационный web-сервис на Python Tornado, специально написанный так, чтобы проявились проблемы производительности, анализировать отчеты нагрузочных тестов, исправлять «узкие места», а затем сравнит производительно «до» и «после».
Описание этого и других докладов, которые ожидают вас на Гейзенбаг 2017 Piter, смотрите на сайте конференции.
Автор: ARG89