Гик (англ. geek, IPA giːk) — человек, чрезвычайно увлечённый чем-либо; фанат. Один из самых захватывающих сюжетов в книгах или кино, это  погружение главного героя в некую жуткую авантюру, из которой он пытается выпутаться на потеху зрителю. Рождение GeekLoad как раз и было такой авантюрой: с одной стороны - в сфере нагрузочного тестирования уже так тесно, что не протолкнуться, а с другой - нас преследовало навязчивое ощущение возможности "сделать лучше". Хотя это не история в чистом виде, а скорее ход размышлений во время работы над проектом - дальнейший рассказ раскроет наши метания на пути к цели. Запасайтесь попкорном! Несмотря на большой опыт в разработке аналогичного приложения, решено было переосмыслить задачу и сделать что-то новое, свежее и функциональное. На этом этапе мы пробовали обсудить идею создания «не очередного лоад-тул» с коллегами и знакомыми QA, а также посмотреть на то, что предлагают разработчики аналогов и как это преподносят. И здесь нас ждало первое "препятствие": Реакцией на рассуждения о новом продукте в существующей нише, часто была следующая всепропальческая последовательность: Рынок переполнен предложениями. Старожилы индустрии вас задавят. Шансов нет. Займитесь чем-то другим. Но не каждый новый проект основан на совершенно уникальной и прорывной идее. Фейсбук или ChatGPT появляются не каждый день. И это не помешало в свое время появиться iPhone в виде гениальной компиляции существующих компонентов. Т.е. формально был представлен абсолютно не уникальный продукт. Дальнейшую его судьбу вы хорошо знаете. Мысли об этом дали нам надежду на успех в занятой нише. Далее, люди близкие к маркетингу, обычно задавали вопрос:  «Если продукт не уникальный, то в чем будет его особенность, какая киллер-фича?» Здесь есть интересный момент: если просто свести список фич аналогичных продуктов в таблицу, то часть их будет похожа до степени смешения. Соответствующие команды разработчиков не зря едят свой хлеб и без устали «выкатывают фичи». С небольшой долей условности, можно заявить: «У всех есть все. Лучше всех!». Как же поступают маркетологи в существующих продуктах? Выпячивают одну из фич и рекламируют противоположности! Т.е. при наличии подобного списка фич, разработчики различных инструментов, просто делают особенный упор на части этого списка. И отличием является разность того, как описан продукт. А чтобы пользователей совсем ввести в ступор, как преимущество преподносятся прямо противоположные решения: Визуальное представление проекта в виде дерева объектов против Project-as-code. Облачная реализация против локальной. Проприетарный движок симуляции против «реальных браузеров». JVM реализация против «производительных нативных языков». Open source против private source. Функциональный язык против императивного. Строгая типизация против автоматического приведения типов. В качестве примера квинтэссенции маркетингового бреда, приведу достоинства микроволновки из одного, случайно встреченного рекламного ролика: Плавная регулировка мощности. Все. Список был из одного пункта. Т.е. часто в рекламе главное - пафосный голос за кадром и впечатляющая картинка, а не информирование о преимуществах. А как решили поступить мы? Да, на сайте и в  рекламных материалах, будет что-то написано, так как умами правит реклама, а интернетом – SEO. Но главной фактической фичей должна стать ориентация на нужды потенциальных пользователей, а не на рекламные проспекты: Если продукт заявлен как удобный, то его легко развернуть и создать простой проект, который совершенно точно не окажется длиннее аналогов в два раза. Также это должно означать легкое написание плагина для уникального протокола или встраивание в процесс CI/CD. Без гугления. Если заявлено распределенное тестирование, то оно есть здесь и сейчас, интегрированное с планом тестирования, отчетностью и прочим, без дополнительных плагинов и требования перехода на платный облачный аккаунт и т.д. Главный вектор развития был выбран «в глубину проработки подходов» а не «в ширину списка фич», т.к. подходы менять в будущем на порядок сложнее, чем добавлять фичи по накатанной колее.  Причем, формально, все ориентируются на пользователя, но по факту часто делают как проще: K6, Gatling, Jmeter не имеют функционала агентов интегрированного в скрипт проекта. Jmeter при первом запуске на чистой машине предлагает самим догадаться какой и где взять наиболее подходящий JRE. И да, они дают разную производительность, и как следствие – разные результаты тестов. Gatling заявляет о «удобном DSL», но на практике - не самый лаконичный. Locust заявляет о «миллионах виртуальных пользователей», но описывая детали, путается в показаниях и лукавит. В прочем не он один. Про асинхронные запросы и явное управление соединениями, похоже, многие решили забыть. Так проще. Для разработчиков. После того как сомнения были отброшены, перешли к вопросам в практической плоскости. Новый продукт требует уникального имени. Вот здесь оказалось совсем беда. Почти все имена, которые только можно придумать, заняты реальными продуктами или доменными именами. Мы живем в сложное время в этом смысле. Как быть разработчикам в будущем, вообще не понятно. И имена заняты и программы написаны. Несмотря на то, что существует много примеров успешных продуктов с нерелевантными или просто странными именами, мы хотели релевантное и говорящее имя. Отталкиваясь от идеи,  сделать Load для гиков, так и назвали: GeekLoad. Ни чем известным это имя не было занято, но несколько странных строк в поисковой выдаче по этому имени имелось. Какой-то заброшенный аккаунт в конструкторе сайтов, влог обзорщика видеоигр на Youtube и т.д. Надеемся, это не станет большой проблемой. Хотя нет, неправда. Все было не так быстро и понятно. Вариантов было множество и выбирали долго. Но имя наполняется тем смыслом, который ему дает его обладатель. Поэтому в какой то момент поток коллективного сознания генерирующего варианты был просто остановлен, надеемся, на хорошем варианте. Следующим этапом было определение основных "китов", на которых будет держаться продукт. Здесь было решено, что делать как все - не вариант, иначе придется стать в длинную очередь прилежных учеников метагайдлайна обобщения опыта существующих аналогов. Речь о том, чтобы повторить фичи и подходы предшественников. Может чуть лучше. И вишенкой на  торте объявить некую киллер-фичу-флагоносец. Как результат, мы почти наверняка потерпели бы фиаско из-за вторичности и минимальной "добавленной стоимости". Еще одной причиной провала могла быть ошибка выжившего, когда успех некого успешного продукта был обусловлен не его выдающимися фичами, а некими другими факторами, которые с нами не случились. Итак, белой вороной быть. Но что дальше? Не буду юлить и скрывать то, что мы приняли несколько спорных решений. Каждое из них, это отдельный сезон в сериале "GeekLoad". Первым решением был выбор Kotlin-JVM в качестве основного инструмента разработки под эту задачу. Да, JVM для нагрузочного тестирования. С одной стороны - существует популярный JMeter на Java и т.д. C другой - в кругах DEV и QA сообщества распространено мнение, что JVM приложения хромают в плане производительности. Но ситуация немного сложнее, при пристальном рассмотрении. JVM языки имеют очень хорошее соотношение производительности к затратам на разработку. На наш взгляд лучшее, чем традиционные компилируемые при сборке (AOT) высокопроизводительные языки. Конечно, без «налога на роскошь» в виде нескольких минусов подобных приложений не обходится: Большее потребление памяти. Это критично в основном для долгоживущих приложений в облаках и т.д., когда  за дополнительную память приходится доплачивать. Необходимость распространять JRE вместе с приложением. Но это выливается только в увеличение docker контейнера или инсталляции. Никаких дополнительных действий пользователю делать в нормальных приложениях не нужно. Чуть большее время старта и низкая производительность в течение первых секунд его функционирования, когда происходит JIT компиляция. Тщательная оптимизация позволила нивелировать эту разницу. В процессе разработки, было предпринято несколько итераций повышения производительности симуляции GeekLoad с помощью различных подходов и инструментов, в результате которых удалось достичь производительности нативных решений и узнать много интересного. Например, то, что нативный код – не всегда самый быстрый. Шок от первого сравнения производительности двух тестовых приложений на одном из нативных языков и Java навсегда останется с нами. Ожидая некоторой величины отставания Java, мы получили ее значимое превосходство! И хотя самый быстрый код – всегда нативный, в реальных кейсах его очень сложно реализовать.  И не реализовывают, чего лукавить? За примером далеко ходить не нужно – известная разработка от Oracle, позволяющая «конвертировать» JVM приложений в нативные, называется GraalVM. В результате получается приложение, которое быстро стартует, но работает медленнее до 25%, чем «разогретое» JVM приложение. Это можно было бы списать на неудачу конкретной команды, но аналогичный проект Российских разработчиков – Excelsior Jet, рекламирует все что угодно, кроме производительности работы приложения. Здесь важно заметить, что под высокой производительностью JVM, в первую очередь понимается высокая в среднем производительность, что подходит для нагрузочного тестирования, но, например, не для высокочастотного трейдинга, где важно время абсолютно каждого запроса.    Конечно, глубокое понимание архитектуры приложения и филигранное владение одним из низкоуровневых нативных языков, помноженное на большие затраты во время начальной разработки и последующих доработок, позволят достичь максимума. Но этого, обычно, либо не делают вообще, либо делают ценой обилия багов и недостатка фич. Следующей обсуждаемой темой было явное управление соединениями – скользкая тема для большинства «симуляторов трафика». Так получилось, что работа с соединениями в k6, Gatling и т.д. спрятана в «черный ящик» встроенного пула соединений и пользователь практически не имеет рычагов влияния на этот механизм. Т.е. описывая некий запрос, вы не в силах указать соединение, в рамках которого этот запрос будет выполнен. Как результат - недостаточная гибкость в симуляции поведения клиентского приложения, невозможность тестировать именно установление соединений и отсутствие связанной фичи – асинхронных запросов. Мы решили не прятать соединения от пользователя, но и не усложнять простые сценарии. Пусть сложность сценариев растет вместе со сложностью решаемых задач.  В этой парадигме будут работать все поддерживаемые протоколы, включая реализованные в пользовательских плагинах: Выпуская продукт в существующей нише, невозможно избежать сравнения с аналогами. В том числе и в вопросе работы с соединениями. И здесь нас ждало разочарование: При сравнении производительности симуляции GeekLoad с k6, второй заметно выигрывал. Как так? На помощь пришел перехват трафика и изучение документации.  Выяснилось, что соединения, установленные в первой итерации сценария каждого виртуального пользователя,  повторно используются в последующих итерациях. Само собой, соединения в рамках одной итерации сценария также автоматически кэшируются. Такое агрессивное кэширование и оказалось причиной разницы в производительности, но является ли оно адекватным при симуляции отдельных сессий работы реальных пользователей? На наш взгляд - нет, так как в начале таких сессий всегда присутствует шаг установления соединений. "Но и это еще не все!" - как говорят в дешевой рекламе. Чтобы окончательно превратить соединения в "это больше не ресурс", те соединения, которые сервер закрывает по таймауту, тихо и без ошибок повторно открываются. Это окончательно уводит тестирование в отрыв от реальности. Кто виноват - не важно. "Что делать?" - вот вопрос. Как остаться верным выбранному пути корректной симуляции, но не распугать пользователей результатами слишком отличающимися от ранее получаемых? Мы попробовали начать с "эмуляции" распространенного поведения с агрессивным кэшированием и добавить возможность отключения его в скрипте проекта. Сделали, а осадок остался. Есть подозрение, что придется со временем поменять поведение по умолчанию. В отличие от настроек, тема синтаксиса для описания кода (скрипта) проекта - более чувствительна к изменениям. Поэтому она подверглась особому вниманию.  Более того, учитывая бесконечное количество различных предпочтений пользователей, мы изначально думали о поддержке нескольких языков. При этом, планируя поддержать языки с разной идеологией. Первым реализованным языком стал JavaScript, а в качестве следующего, своей очереди ждет Python, также есть задумка для ортогонального первым двум языка из функционального лагеря. Выбор языков был очевиден вследствие их популярности и необходимости создавать минимум проблем при описании тестового плана.  Обобщение опыта популярных аналогов, позволило предоставить наиболее простой синтаксис, позволяющий, при желании, реализовать сложные сценарии. Звучит как рекламный текст? Да! Но это именно то, чего мы добивались изначально в качестве самостоятельной цели, а не аврального разгребания свалки из наваленных в кучу протоколов и технологий. Когда фичи делались быстрее мысли до тех пор, пока  не станет понятно, что они плохо стыкуются и мешают друг другу. Вопрос сбора результатов – еще один краеугольный камень в тестирующем ПО. Для нагрузочных тестов с большим количеством собираемой информации – в особенности.  Очевидным решением вопросов производительности при сборе результатов, является получение агрегатов различных метрик непосредственно во время тестирования на стороне агентов. В этом случае, не нужно хранить, либо передавать большие объемы данных контроллеру-анализатору, конкурируя в нагрузке на сеть с тестовым трафиком. Да, так и поступают в ряде случаев, но в таком подходе мы видели недостатки. Самая очевидная проблема – не все метрики можно посчитать без доступа ко всему массиву исходных данных со всех агентов. Например - процентиль. Да, можно считать его по окну данных, но это приблизительное значение. Ошибок и округлений в реальных кейсах хватает и без этого. Также, развивая функционал анализа результатов, придется оглядываться на его вычислительную сложность, чтобы процесс симулирующего агента не погиб под грузом вычислений данных для всех возможных отчетов. Даже тех, которые в каждом конкретном случае не понадобятся пользователю. Альтернативный подход в работе с результатами – сбор всех возможных исходных данных со всех агентов в одну базу данных. Плюс этого подхода в том, что оффлайн анализ может быть сколь угодно сложным и многообразным без влияния на процесс симуляции, в тоже время, оставаясь точным. Но есть и плохие новости: большое количество данных в этом случае нужно передавать и хранить. Идеальных решений не существует. Но мы в данной ситуации попытались получить максимально выигрышное решение. И первая проблема виделась в том, что любой достаточно функциональный локальный DB движок может не успеть принять и обработать поток данных с результатами в реальном времени. Популярные NoSQL движки, быстрее, но менее функциональны при обработке сохраненных данных. Причем на практике, при правильном подходе, разница производительности SQL и NoSQL движков не велика. Сомневающимся, предлагаем попробовать пакетную вставку через бинарный протокол в Postgres. Если же отправлять данные контроллеру во время симуляции, то возникает нагрузка на канал передачи данных. Но передавать когда-то все равно нужно.  Чтобы выйти из этого противоречия, для хранения результатов во время симуляции на агентах, мы стали использовать примитивный локальный буфер в фиксированном формате, дающий минимальную нагрузку на производительность в силу простоты. В конце теста, результаты со всех агентов собираются в общей базе данных на контроллере, что гарантирует минимальное влияние на тесты ценой времени на скачивание и мержевание данных. Имея полноценную SQL DB по итогам теста, можно получать любые отчеты без оглядки на недостаток данных или ресурсов. Разумеется, состав хранимых данных можно и нужно регулировать находя для себя приемлемый баланс.  Агенты, генераторы, воркеры – общепринятых названий у непосредственного движка симуляции встречается несколько, но мы остановились на первом варианте, так как он лучше всего олицетворяет суть происходящего: представление инструмента на удаленных машинах (физических или облачных). Генерировать трафик можно и локально, а отдельное приложение или сервис агента необходимо именно для масштабирования и\или симуляции с различных удаленных географических зон. Следуя принципу простое должно быть простым, в требования к агенту сразу было заложено автоматическое масштабирование в рамках одного контейнера. Дальнейшие технические решения отталкивались от этого. Странное требование по ручному запуску нескольких процессов воркеров на одной машине мы оставили за гранью приемлемого.  Мы считаем, что хороший программный инструмент, должен быть целостным и последовательным. А нагрузка растет от одиночного запроса до распределенной группы агентов.  Поэтому логичным развитием механизма масштабирования стал автоматический запуск дополнительных контейнеров с агентами в различных средах виртуализации. Этот механизм был назван облаком агентов. Казалось бы, очевидное решение, но к нему, похоже, приходят не сразу. Это видно по отсутствию интегрированных средств управления агентами в некоторых аналогичных решениях и наличию «функционального разрыва» между базовым локальным приложением и его облачным расширением, которое уже может масштабироваться, разрастаясь контейнерами.  Здесь мы достигли главного на первом этапе: не множества совместимых средств виртуализации, а реализации ядра для масштабирования агентов. Асинхронность, как много в этом слове… или мало, судя по регулярному игнорированию в нагрузочном тестировании. Кажется очевидным, что один из его принципов - максимально точно повторить логику клиентских приложений, часто содержащих параллельное выполнение нескольких запросов в том или ином виде. Но как выполнить задачу без соответствующих инструментов?  Речь не о том, что все тестовые сценарии содержат асинхронные запросы или их наличие критически важно. Понятно, что с некоторой степенью достоверности можно обойтись последовательным выполнением. Но только лишь с некоторой. И специалист, занятый тестированием, вынужден задумываться над интерпретацией полученных результатов, когда виртуального пользователя невозможно никакими доступными средствами сделать эквивалентным физическому пользователю или клиентскому приложению. Еще одна причина, по которой мы акцентировали внимание на асинхронности – это более простое создание тестов на основе существующего клиентского кода, содержащего async и прочие Promises. Особенно при использовании для конвертации кода, генеративных нейросетей, которые вовсе не AI, и легко нафантазируют недостающий функционал.   На фоне невысоких затрат на реализацию такой функциональности на ранней стадии разработки, мы  не нашли причин не добавлять ее в продукт. И, надеемся, что наличие соответствующей возможности подтолкнет наших пользователей к простому созданию более качественных и полезных тестов, вместо поиска компромиссов. Потому, как инструмент должен помогать, а не создавать видимость. «Load != Functional»  - это правило, на наш взгляд, иногда понимается слишком буквально. Речь о распространенном подходе, когда ошибки тестируемого приложения фиксируются только количественно, без привязки к породившей их операции (запросу), что смешивает ситуации с разной степенью критичности. Мы считаем, что анализ причин ошибок и их классификация это необходимая часть нагрузочного тестирования, хотя бы по причине того, что некоторые функциональные ошибки проявляются только при высокой нагрузке или большой длительности тестов. Другая причина неразрывной связи нагрузки и правильного функционирования в том, что некорректно работающее приложение, это не то приложение, устойчивость к нагрузке которого нам нужно проверять. Обсуждая обработку и анализ ошибок в инструментах нагрузочного тестирования, мы заметили, что часто запросы, при работе которых возникли ошибки, включаются в расчет агрегированных метрик наравне с полностью успешными запросами. На наш взгляд это неверный подход, так как запрос с ошибкой может быть как очень быстрым, так и крайне медленным в силу различных причин и сильно выбиваться из общей картины, искажая ее. Тем не менее, предлагать свое решение просто так, мы не могли – привычки сложно менять. Поэтому в качестве режима по умолчанию, был использован «традиционный» подход, а в качестве опции – наше виденье, с фильтрацией ошибочных запросов перед анализом скоростных метрик в отчетах.  Вообще, в направлении анализа ошибок, нам еще предстоит большая работа, но изначальным решением было отталкиваться от важности этого вопроса. В какой то момент размышлений о том, как сделать работу наших пользователей еще удобнее, нам стало ясно, что роль тестового плана недооценена. Потому что, каким бы разнообразным не был набор поддерживаемых протоколов и т.д., без удобной упаковки в сущность тестового плана, уйти далеко от "библиотеки для выполнения запросов" не получится. Это как добавив двигатель от мерседеса в телегу, странно ожидать качественного роста комфорта. Чтобы проработать вопрос, мы заглянули "на огонек" к знакомым лоад тестерам и просто поговорили о фактических буднях их работы: "С чего вы начинаете тестирование?" - спросили мы. "Составляем тестовый план" - ответили они - "Он бывает простым или не очень, иногда из нескольких кейсов. По каждому из которых нужен репорт. Но и общий репорт тоже нужен." "Опаньки!" - триггернуло нас - "А комплексный тестовый план с явно прописанными кейсами-этапами это идея!" "И специализированный визуальных редактор для плана!" - прилетело из чата в телеге - "... и гибкое конфигурирование нагрузки на основе процентов от базового значения нагрузки проекте..." Цепная реакция в наших умах была запущена. Живое общение с целевой аудиторией - изумительная вещь. А как показывает наш опыт, оффлайн сбор фичареквестов - не всегда дает нужный результат, т.к. иногда реквесты не так понимаются или понимаются очень узко. В результате выяснилось, что на этапе создания тестового плана, тестерам приходится рисовать таблички, а потом конвертировать их в проект лоадтеста. Комплексный тестовый план порождает не один проект, а несколько, что не всегда удобно. И, как результат, интеграции тестового плана с отчетностью тоже нет. А вы говорите "Все уже есть на рынке"! Все, ушли писать код. В каждом подобном случае «пробуя идею на зуб», проверяя «хорошо ли лежит в руке», чтобы получить не просто «галку в списке фич», а лучший опыт, которым можно поделиться с нашими пользователями. Начиная работу над новым проектом из разряда «еще один …», мы осознавали объем работ до выхода на «рабочую высоту». Это осознание подталкивало к мыслям взять готовый открытый движок симуляции, а также, возможно, другие крупные компоненты приложения. Но получится ли в этом случае действительно Новый продукт, а не старый в новой обертке? Получится ли в этом случае что-то стоящее, чтобы занять свое место под солнцем? Уверены, что нет. И по причине прямого дублирования большого количества функционала из аналогичных продуктов и из-за сложности принципиальных изменений, необходимых для реализации свежих идей. Уверенности придавал тот факт, что однажды мы уже прошли этот путь под другим флагом. И сейчас, несмотря на более  насыщенный предложениями рынок, шаг за шагом обязательно пройдем свой новый путь и увидим свет в конце туннеля. И последнее: выше в этом тексте нет ни слова о поддерживаемых протоколах и т.д. – они просто должны быть и должны согласовываться с принятыми принципами, а недостающие – будут реализованы кастомными плагинами. Соревнование в количестве чего бы то ни было – не то, в чем хочется участвовать. Логичность, простота и внимание к деталям, вот те направления, над которыми стоит работать. Гики, заходите на сайт проекта, оставляйте фидбек, критикуйте в комментах статьи! Живое общение рождает лучшие вещи. Мы уверены.