Здесь, в Klarna, мы прилагаем много усилий, чтобы дать возможность нашим разработчикам предоставлять высококачественные и безопасные услуги. Одна из услуг, которые мы предоставляем нашим разработчикам, - это платформа для проведения A / B-тестов. Критическим компонентом этой платформы является набор процессов, которые для каждого входящего запроса принимают решение: какой вариант теста (A или B) подвергнуть запросу. Это, в свою очередь, определяет, какого цвета визуализировать кнопку, какой макет показывать пользователю или даже какой сторонний сервер использовать. Эти решения имеют прямое влияние на пользовательский опыт.

Производительность каждого процесса в этом парке имеет решающее значение, поскольку он используется синхронно на важнейших этапах принятия решений в экосистеме Klarna. Типичное требование в таких потоках - принимать решение с задержкой, равной одной цифре, для 99,9% запросов. Чтобы быть уверенными в том, что мы продолжаем придерживаться этих требований, мы разработали конвейер тестирования производительности для нагрузочного тестирования этой службы.

Вывод №1: тестирование производительности может дать нам уверенность в том, что мы не снижаем производительность с каждым выпуском.

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

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

Вывод №2: «нагружая» нагрузку, мы можем выявить проблемы еще до того, как они дойдут до производства.

Следует также отметить, что для появления проблем требуется около двух или трех минут. На первых итерациях мы запускали этот тест всего за две минуты. Только после увеличения продолжительности теста до десяти минут мы обнаружили эту проблему.

Вывод № 3: длительные нагрузочные тесты могут выявить различные проблемы. Если все в порядке, попробуйте продлить тест.

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

Но эти показатели не дают никаких сведений о том, что служба работает неправильно. Когда что-то идет не так, вам нужно знать, где находится узкое место. Для этого вам необходимо отслеживать ресурсы, которые использует среда выполнения Node.js. Самыми очевидными из них являются использование ЦП и памяти. Но иногда это не настоящие узкие места. В нашем случае загрузка ЦП была низкой, как и использование памяти.

Еще один ресурс, который использует Node.js, - это цикл событий. Точно так же, как нам нужно знать, сколько мегабайт памяти использует процесс, нам также необходимо знать, сколько задач необходимо обработать циклу обработки событий. Цикл событий реализован в библиотеке C под названием libuv (здесь - отличный рассказ Кеннета Гибсона о цикле событий). Термин, который он использует для этих задач, - Активные запросы. Еще одна важная метрика, которой нужно следовать, - это количество активных дескрипторов, то есть количество дескрипторов открытых файлов или сокетов, которые содержит процесс Node.js (полный список типов дескрипторов см. В документации libuv). Так что, если в тесте используется 30 соединений, имеет смысл увидеть около 30 активных дескрипторов. Активные запросы - это количество операций, ожидающих выполнения с этими дескрипторами. Какие операции? Полный список доступен в документации libuv, но это могут быть, например, операции чтения / записи.

Глядя на показатели, сообщаемые службой, было что-то не так. Хотя количество активных дескрипторов вполне ожидаемо (около 30 в этом тесте), количество активных запросов было непропорционально большим - несколько десятков тысяч:

Однако мы до сих пор не знали, какие типы запросов были в очереди. После разбивки количества активных запросов по их типам картина прояснилась. В отчетных показателях выделялся один тип запроса: UV_GETADDRINFO. Этот тип запроса создается, когда Node.js пытается разрешить DNS-имя.

Но почему он генерирует так много запросов на разрешение DNS? Оказывается, клиент StatsD, который мы используем пытается разрешить имя хоста для каждого исходящего сообщения. Честно говоря, он предлагает возможность кэширования результатов DNS, но этот вариант не учитывает TTL этой записи DNS - он кэширует результаты на неопределенный срок. Поэтому, если эта запись обновляется после того, как клиент уже разрешил ее, клиент никогда об этом не узнает. Поскольку балансировщик нагрузки StatsD может быть повторно развернут с другим IP-адресом, и мы не можем принудительно перезапустить нашу службу для обновления кеша DNS, этот подход к кэшированию результатов на неопределенный срок не подходил для нас.

Решение, которое мы придумали, заключалось в том, чтобы добавить правильное кеширование DNS вне клиента. Это несложно сделать, установив патч модуля «DNS». И результаты были лучше:

Вывод №4: не забывайте учитывать разрешение DNS, думая об исходящих запросах. И не игнорируйте TTL записи - это может буквально сломать ваше приложение.

После решения этой проблемы мы повторно включили в сервисе еще несколько функций и снова протестировали. В частности, мы включили часть логики, которая создает сообщение в тему Kafka для каждого входящего запроса. Тесты снова выявили значительные всплески времени отклика (в секундах) в течение значительных периодов:

Анализ показателей службы показал очевидную проблему именно в этой функции, которую мы только что включили - задержка отправки сообщений в Kafka была чрезвычайно высокой:

Мы решили попробовать тривиальное улучшение - поставить исходящие сообщения в очередь в памяти и пакетно сбрасывать их каждую секунду. Снова запустив тест, мы увидели явное улучшение времени отклика службы:

Вывод №5: пакетные операции ввода-вывода! Даже в асинхронном режиме ввод-вывод стоит дорого.

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

Вывод № 6: прежде чем пытаться что-либо улучшить, вы должны пройти тест, чтобы убедиться, что вы доверяете его результатам.

РЕДАКТИРОВАТЬ: Я получил несколько вопросов о том, какие инструменты использовались для выполнения здесь тестов. Здесь используется пара инструментов:

Нагрузка создается внутренним инструментом, который упростил работу Locust в распределенном режиме. По сути, нам просто нужно запустить одну команду, и этот инструмент запустит генераторы нагрузки, предоставит им тестовый сценарий и соберет результаты на панели мониторинга в Grafana. Это черные скриншоты в статье. Это точка зрения (клиента) в тестах.

Тестируемый сервис сообщает метрики в Datadog. Это белые скриншоты в статье.