Сколько запросов действительно может обработать сервер Node.js?
Сколько запросов на самом деле может обработать сервер Node.js? Тесты нагрузки с использованием Express на Heroku и AWS по сравнению с Django, оценка пропускной способности в 40 тыс. запросов в секунду и факторы, замедляющие работу API.
Источники: How Many Requests Can a Real-World Node.js Server-Side App Handle?
Каждый, кто только начинает работать с Node.js, рано или поздно сталкивается с одним и тем же практическим вопросом: сколько запросов может обработать одно приложение Node.js в производственных условиях и сколько серверных инстансов потребуется для обработки определённого объёма трафика? Ответ зависит от гораздо большего количества факторов, чем только от самой фреймворковой среды.
Цель, к которой стоит стремиться
Сообщается, что платформа для создания изображений PicsArt обрабатывает около 40 000 запросов в секунду с использованием в основном Node.js в качестве бэкенда.
Это огромное число, и оно зависит от множества факторов, помимо самого бэкенд-сервера. В рамках данного анализа все эти дополнительные факторы отбрасываются, оставляя во внимании только аппаратное и программное обеспечение бэкенда, а также Node.js в частности.
Что показывают синтетические тесты
Команда, разработавшая Fastify, опубликовала тесты, согласно которым Express обрабатывает примерно 15 000 запросов в секунду, в то время как простой HTTP-модуль Node достигает примерно 70 000 запросов в секунду.
MacBook Pro 15″ 2015 года показал практически идентичные результаты. Даже 15 тыс. запросов в секунду от Express — это отличный показатель, но подобные тесты проводятся с использованием крайне примитивного обработчика, который далек от реального API. То, что делает API «реальным», — это данные, способ их передачи и взаимодействие компонентов; поэтому реалистичный обработчик должен взаимодействовать с базой данных или другими сервисами.
Разница между прямой обработкой и использованием Express в любом случае не так уж и важна. Такие устоявшиеся фреймворки, как Express, обычно предпочтительнее, поскольку на практике ни один отдельный экземпляр не сможет обрабатывать 100 тыс. запросов в секунду. Поэтому для этих тестов был установлен Express, а также примитивный обработчик, выполняющий различные операции.
Обработчик тестов
Обработчик выполняет шесть шагов:
- Вставка нового пользователя в базу данных.
- Загрузка 20 пользователей из базы данных.
- Преобразование массива из 20 пользователей в строку и обратно.
- Рекурсивно преобразуйте эти 20 объектов пользователей в формат camelCase.
- Отсортируйте массив.
- Выполните простые вычисления и операции с выводом в стандартный вывод, такие как
Date.nowиconsole.log, которые также потребляют вычислительные ресурсы.
Размер ответа составил примерно 12 КБ. Каждый объект пользователя имел размер 609 байт и выглядел следующим образом:
Результат 1: Node.js на Heroku с AWS RDS
Приложение было развернуто на Heroku на оборудовании стоимостью 25 долларов в месяц, и запросы отправлялись с максимальной частотой, не приводившей к увеличению средней задержки. Инстанс обработал 31 000 запросов за 60 секунд, что соответствует среднему показателю в 515 запросов в секунду.
Результат 2: Django на Heroku с AWS RDS
Чтобы узнать, как бы себя повела технология, не являющаяся однопоточной, в Django был создан эквивалентный обработчик в идентичных условиях и размещён на том же сервере. Результат оказался немного хуже, чем у Node.js: было обработано 23 000 запросов за 60 секунд, что соответствует 389 запросам в секунду.
Результат 3: три инстансы Node.js на AWS Elastic Beanstalk
Третья конфигурация имитировала реальную производственную среду на AWS. База данных и серверный слой находились в Франкфурте (eu-central-1) — ближайшем регионе, расположенном примерно в 3 000 км от тестировщика. За работой трёх инстансов Node.js следил механизм балансировки нагрузки AWS. Они обработали 19 000 запросов за 10 секунд, что равно 1 858 запросам в секунду. В ходе непрерывного теста нагрузки за 13 минут было выполнено 1,5 миллиона успешных запросов.
Масштабирование до 40 000 запросов в секунду
Одна инстанция обрабатывает примерно 600 запросов в секунду, так сколько же потребуется для 40 000 запросов? Предположим, что средний конечный пункт на крупной платформе в два раза сложнее по обработке, чем тестовый механизм. Это дает 40 000 / (600 / 2) = 133 инстанции, распределённые по всему миру, для приложения глобального масштаба. Это не так уж и много.
Более сложные конечные пункты могут требовать в три раза больше ресурсов, но кэширование и коммуникация на основе событий могут восстановить пропускную способность, поэтому 2 раза — разумная средняя оценка.
Что замедляет API
Скорость API может снижаться из-за бесчисленного количества факторов. Это факторы, связанные с производительностью, которые встречаются в большинстве веб-приложений с серверной стороны.
Размер данных
Объем данных, хранящихся в базе данных или загружаемых из нее, имеет большое значение, особенно в реляционных базах данных, где большие объемы данных часто приводят к сложным запросам или интенсивной работе между СУБД и ее дисковой памятью. В любом случае больше байт означает больше времени на передачу. Хранение файлов в базе данных является неправильным подходом; для этого лучше использовать объектное хранилище. В идеале размер записи должен составлять несколько десятков килобайт, обычно от 1 до 20 КБ.
Расстояние до базы данных
Большее расстояние между бэкендом и базой данных приводит к более длительному передаче запросов и результатов. Возьмём два бэкенда — один в Вирджинии, другой во Франкфурте — и одну базу данных в Вирджинии. Запрос американского пользователя доходит до бэкенда в США примерно за 10–40 мс, а передача данных от бэкенда к базе занимает 1–5 мс. Это нормально. Германский пользователь достигает бэкенда во Франкфурте за 10–20 мс, но запрос к базе в Вирджинии занимает 120–130 мс. Поэтому приложения, работающие в одном регионе, должны размещать данные и логику бэкенда рядом друг с другом, а глобальные приложения должны быть готовы к сложной инженерной работе, чтобы сохранять высокую скорость.
Аппаратное обеспечение базы данных
Аппаратное обеспечение также оказывает значительное влияние. Небольшое приложение должно приобретать более совершенное оборудование, соответствующее объёму трафика, в противном случае снова потребуется сложная инженерная работа.
Конфигурация базы данных
Базы данных поставляются с параметрами по умолчанию, чтобы не создавать дополнительных проблем с управлением, но в масштабах им требуется внимание. Одним из важных параметров является максимальное количество соединений. Оно может стать причиной замедления работы приложения или перегрузки сервера базы данных, к тому же библиотеки клиента, такие как ORM, часто изменяют этот параметр. Ещё одним важным параметром является время ожидания соединения.
Количество запросов к базе данных
Настоящий API может выполнять десятки отдельных запросов к базе данных для завершения задачи, а также обращаться к другим сервисам, каждый из которых выполняет свои десятки запросов. Один запоминающийся пример — API банковского монолита с временем ответа 9 секунд в облачной среде разработки. Его обработчик выполнял 300 строк кода, включающих примерно 30 запросов к базе данных и около 10 запросов к внутренним сервисам. Такое часто происходит при подходе «сначала код», а не «сначала дизайн».
Решения зависят от бизнес-логики. Очереди и другие асинхронные техники позволяют переносить часть работы с базой данных в фон, что требует разделения крупных сервисов на более мелкие. Объединение запросов также может сократить количество вызовов.
Хуки
Популярные ODM и ORM предлагают хуки или промежуточное программное обеспечение, которые удобны для обработки определенных логических операций. Однако при массовых операциях они могут стать скрытым узким местом, поскольку такие плагины, как конвертеры в формат camelCase или pascal_case, также потребляют вычислительные ресурсы и время. Хуки помогают, но то, что в них написано и где, заслуживает дополнительного рассмотрения.
Ресурсоемкая работа с CPU
Задачи, требующие больших вычислительных ресурсов процессора, замедляют работу сервера и заставляют конечного пользователя ждать. В Node.js многие из таких задач основаны на пуле потоков. Это не блокирует асинхронный ввод-вывод, но всё равно замедляет работу API. Например, при нажатии на кнопку регистрации приложение ждёт, пока пул потоков обработает хеширование пароля, что занимает более 100 миллисекунд.
Узкие места в цикле событий
Node.js выполняет код в рамках цикла событий на одном потоке, поэтому производительность кода влияет на всё приложение. Методы JSON.parse и JSON.stringify могут замедлять работу API, а вложенные циклы и рекурсивные функции представляют опасность. Игнорирование принципов работы Node.js может привести к недоступности приложения в зависимости от объёма данных и объёма трафика.
Плохая разработка инфраструктуры
Крупная инфраструктура имеет не только больше проблем, чем монолит, но и проблемы иного характера, которые зачастую требуют сложных архитектурных решений. При наличии множества компонентов плохая архитектура сильно снижает скорость работы API и делает систему неэффективной с точки зрения использования ресурсов, в результате чего приходится платить больше за то, что могло бы стоить дешевле. При проектировании сложных приложений стоит подумать о том, какие проблемы решает та или иная технология, и знать альтернативы. Необходимо учитывать, как могут измениться требования через два или шесть месяцев, и проверять каждое решение с учетом этих предположений. Также важно учитывать SLA и нефункциональные требования, включая ситуацию, когда объем трафика увеличивается.
Масштабирование и настройка компонентов
Дополнение инфраструктуры компонентами может замедлить работу всей платформы. Возьмем, к примеру, RabbitMQ — у него, как и у любой технологии, множество настроек. Для начала подойдут стандартные параметры, но благодаря исследованиям, более тщательному изучению конкретного компонента, а также анализу настроек или их переработке инфраструктуру можно значительно улучшить. Компоненты также должны масштабироваться вместе с объемом использования, чтобы не становиться узким местом.