Введение
Допустим, у вас есть идея нового проекта, который обязательно завирусится, как только он увидит свет. Однако вы не можете решить, какой же бэкенд-фреймворк вам стоит использовать. Не нужно отчаиваться. В этой статье мы рассмотрим три популярных Node.js фреймворка (Express.js, Koa.js, Fastify) и сравним их производительность, чтобы вы могли решить, какой из них лучше подходит для вашего проекта. Для замеров производительности мы будем использовать серверы с двумя простыми конечными точками. Первая конечная точка будет возвращать пустой ответ. Вторая – вычислит ряд Фибоначчи в зависимости от входных данных, которые мы предоставим, а затем вернет последовательность. Файлы конфигурации, используемые в этой статье, вы можете найти в этом репозитории. Перед бенчмаркингом давайте более подробно изучим наши фреймворки.
Express.js
Express.js — один из самых известных фреймворков на основе Node.js. Он значительно упрощает процесс разработки по сравнению с голым Node.js без потери его функционала. Среди этих трех фреймворков Express.js является старейшим, благодаря чему вокруг него успело сформироваться большое сообщество разработчиков. Express.js может похвастаться внушительным набором фич. Его архитектура активно использует концепцию middleware, которая обеспечивает модульность, возможность повторного использования, гибкость и простоту тестирования. Благодаря архитектуре на основе middleware Express.js можно использовать в качестве плагина с целью расширения доступного вам функционала. Однако асинхронная система Express.js построена на основе обратных вызовов (колбеков), что может стать причиной “ада обратных вызовов” (callback hell).
Koa.js
Koa.js разработан той же командой, что и Express.js. Они позиционируют его как Node.js-фреймворк нового поколения. Он более современный, минималистичный, гибкий и выразительный с более надежной основой для серверных служб. Команда разработчиков стремилась устранить ограничения Express.js и еще больше раскрыть его сильные стороны. По сравнению с Express.js, Koa.js уходит от использования обратных вызовов, задействуя генераторы для обработки асинхронных вызовов. Генераторы были ключевым компонентом в более ранних версиях Koa.js, что позволило создать более эффективный и элегантный способ асинхронной обработки. Это дает разработчикам возможность писать более чистый и удобный в сопровождении код. Последние версии Koa.js переключились на async/await, что является еще более современным и лаконичным способом обработки асинхронных операций. В отличии от двух других фреймворков, Koa.js использует “объект-контекст” под названием «ctx» для одновременной обработки объектов запроса и ответа.
Fastify
Основная цель Fastify — достижение высокой производительности за счет оптимизации различных аспектов приложения. Это самый молодой фреймворк из обозреваемой тройки. Он также использует архитектуру на основе middleware и способен работать с другим middleware, таким как Express.js и Koa.js, что помогает обеспечить модульность и позволяет разработчикам подключать различный функционал из того же Express.js и других подобных фреймворков. Однако в случае с Fastify эта архитектура нацелена на минимизацию накладных расходов и достижение максимальной скорости. Тот же аспект минимизации накладных расходов применяется и к маршрутизации. Система маршрутизации в Fastify позволяет приложениям обрабатывать большое количество запросов. Для разработчиков, которые хотят писать код с применением TypeScript, Fastify предлагает поддержку TypeScript прямо из коробки. Важно отметить, что Fastify поддерживает асинхронную обработку без ущерба для производительности.
Важность сравнительного анализа производительности для веб-фреймворков
Сравнительный анализ производительности (бенчмаркинг) веб-фреймворков является очень важным аспектом оценки и улучшения вашей системы, ведь они будут иметь прямое влияние на общее качество пользовательского опыта. Веб-фреймворки стремятся оптимизировать и улучшить взаимодействие с пользователями, масштабируемость и потребление ресурсов. Однако не все фреймворки одинаково хорошо справляются со всеми видами задач. Бенчмаркинг дает нам информацию, на основе которой мы можем выбрать наиболее подходящий для наших целей веб-фреймворков. Он помогает разработчикам принимать обоснованные решения, оптимизировать использование ресурсов и улучшать общее взаимодействие с пользователями. Ваши веб-системы могут отлично справляться с небольшими нагрузками, но что произойдет, если ваш сайт вдруг начнут использовать тысячи людей и нагрузка возрастет? Именно поэтому выбор правильного веб-фреймворка здесь имеет решающее значение.
Тестовая среда
В качестве нашей тестовой среды мы выбрали Ubuntu 22.04 с 2 процессорами и 4 ГБ памяти (c5.large) на AWS EC2.
Для симуляции нагрузки на сервер мы будем использовать опенсорсный инструмент для нагрузочного тестирования Ddosify. Ddosify Engine — это инструмент для тестирования производительности, ориентированный на нагрузочное тестирование бэкенда. Ddosify Engine написан на Golang, является полностью опенсорсным, и может быть использован на терминале по вашему выбору. Вы можете устанавливать количество запросов, их содержимое (тело, заголовок), продолжительность теста, тип нагрузки (например, линейный, инкрементный или волновой). Он также поддерживает сценарии нагрузочного тестирования, задаваемые с помощью простого JSON-файла, динамические переменные (параметризацию), утверждения, тестовые данные (CSV). Загляните в наш репозиторий на GitHub, чтобы получить больше информации.
Тестовый сетап
Как мы упоминали ранее, в нашей тестовой системе будут две конечные точки. Основная цель первой конечной точки с пустым ответом — проверить производительность фреймворков без каких-либо вычислений. С помощью второй конечной точки, где мы вычисляем последовательность Фибоначчи, мы хотим выяснить, будет ли разрыв в производительности между фреймворками при необходимости совершать дополнительные вычисления.
Вычисление ряда Фибоначчи
Для расчета ряда Фибоначчиы в каждом фреймворке мы будем использовать приведенную ниже функцию:
function fibonacciSequence(num) {
let result = [];
let num1 = 0;
let num2 = 1;
let nextTerm;
for (let i = 1; i <= num; i++) {
result.push(num1);
nextTerm = num1 + num2;
num1 = num2;
num2 = nextTerm;
}
return result;
}
Express.js
const express = require("express");
const app = express();
const port = 8000;
app.get("/", (req, res) => {
res.end();
});
app.get("/:number", (req, res) => {
const num = req.params.number;
res.send({ result: fibonacciSequence(num) });
});
app.listen(port, "0.0.0.0", () => {
console.log(Express server listening at ${port});
});
Koa.js
const Koa = require("koa");
const Router = require("koa-router");
const app = new Koa();
const router = new Router();
const port = 8000;
router.get("/", async (ctx) => {
ctx.body = "";
});
router.get("/:number", async (ctx) => {
const num = ctx.params.number;
ctx.body = fibonacciSequence(num);
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(port, "0.0.0.0", () => {
console.log(Koa server listening at ${port});
});
Fastify
const fastify = require("fastify")();
const port = 8000;
fastify.get("/", function (request, reply) {
return "";
});
fastify.get("/:number", (request, reply) => {
const num = request.params.number;
return fibonacciSequence(num);
});
fastify.listen({ port: port, host: "0.0.0.0" }, (err, address) => {
if (err) {
fastify.log.error(err);
process.exit(1);
}
fastify.log.info(Fastify server listening at ${address});
});
Несмотря на то, что синтаксис этих трех фреймворков очень похож, есть небольшие отличия. Например, с Express.js мы отправляем ответ с помощью функций res.end или res.send, тогда как в Koa.js мы используем объект context, где ответ возвращается с помощью присвоения значения его телу. В Fastify мы можем вернуть ответ напрямую с помощью обычной функции.
Тестовые конфигурации для Ddosify Engine
В рамках тестирования каждого фреймворка мы будем запрашивать две конечные точки, упомянутые ранее (с пустым ответом и с рядом Фибоначчи). Затем мы будем нагружать наши системы 10, 100 и 1000 запросами в секунду, используя Ddosify Engine. Ddosify Engine позволяет выбирать разные параметры для разных тестовых сценариев. Мы создадим базовый тест, используя следующую команду:
ddosify -t http://localhost:8000/ -d <Duration> -n <IterationNum>
Поскольку наш тестовый сценарий включает 10, 100 и 1000 запросов в секунду. Мы заменим длительность на 10 (секунд) и IterationNum на 10 * request number. Таким образом мы получим желаемые условия тестирования для нашей конечной точки с пустым ответом:
ddosify -t http://localhost:8000/ -d 10 -n 10000
Для второй конечной точки мы будем использовать функцию генерации случайных чисел Ddosify Engine, которая выдает числа от 1 до 1000. Таким образом, мы можем генерировать ряды Фибоначчи для случайных чисел и затем сравнивать результаты:
ddosify -t http://localhost:8000/{{_randomInt}} -d 10 -n 10000
Ddosify Engine также включает в себя различные флаги и возможности параметризации. Вы можете посмотреть их в нашем репозитории на GitHub. С помощью Ddosify Engine мы также можем измерить среднее время ответа на наши запросы и их показатели успешности. Он также показывает время, затрачиваемое на различные этапы обработки, такие как установка соединения, запись запроса, обработка сервером и т.д. Он также показывает код состояния ответа, что особенно полезно для анализа ошибок. В качестве примера, если мы запустим Ddosify Engine на одной из наших конечных точек с данной CLI-командой, то мы получим вывод такого вида. Мы проведем остальные тесты аналогичным образом и поделимся с вами результатами в следующем разделе.
ddosify -t http://localhost:8000/{{_randomInt}} -d 10 -n 10000
![Example Run of Ddosify Engine Example Run of Ddosify Engine](https://habrastorage.org/getpro/habr/upload_files/0d5/74a/f7a/0d574af7a2bdc561b1f3e3f972b40856.png)
Анализ результатов
Результаты для первой конечной точки
![Таблица 1: Среднее время отклика для конечной точки с пустым ответом. Table 1- Response Time per request empty endpoint.](https://habrastorage.org/getpro/habr/upload_files/010/bd6/a5d/010bd6a5d342a5fc1e035403ec5d559f.png)
Как видно из таблицы выше, разница между этими тремя фреймворками для конечных точек с пустым ответом очень мала. По мере увеличения числа запросов от 10 до 1000 все они замедляются примерно на 18%. В данном случае это может быть не самым точным метод тестирования, поскольку в подавляющем большинстве случаев конечные точки сервера выполняют некоторые вычисления, а затем возвращают результат. Однако, если ваша конечная точка возвращает ответ сразу без какой-либо обработки, все эти 3 веб-фреймворка должны работать одинаково хорошо. Стоит отметить, что процент успешных ответов составляет 100% для всех фреймворков.
Показатель успеха рассчитывается в зависимости от ответов сервера. Если не используется функция утверждения и сервер возвращает ответ, независимо от status_code, это считается успехом. Однако, если запрос не получает ответа и истекает таймаут, то он помечается как неудачный. Если мы посмотрим на таблицу ниже, то мы видим, что показатель успеха Express.js при 1000 запросов в секунду составляет 84%. Это означает, что около 160 запросов из 1000 получили ошибки таймаута.
Результаты для второй конечной точки
![Таблица 2: Среднее время отклика для конечной точки с рядом Фибоначчи. Response time per request fibonacci sequence endpoint.](https://habrastorage.org/getpro/habr/upload_files/e00/744/130/e007441300496379871da788cd4ddb63.png)
![Таблица 3: Процент успешных запросов для конечной точки с рядом Фибоначчи. Success rate of responses.](https://habrastorage.org/getpro/habr/upload_files/a46/461/2b6/a464612b65f0439a999e2b1d2579eb4f.png)
При вычислении ряда Фибоначчи, хоть мы и не наблюдаем большой разницы при 10 запросах в секунду, мы четко видим, что она начала расти при 100 запросах в секунду по сравнению с конечной точкой с пустым ответом. При 1000 запросов в секунду средний показатель времени отклика значительно выше, чем в предыдущих результатах. До 100 запросов в секунду, все три фреймворка работали примерно одинаково. Однако, как только количество запросов достигло 1000 в секунду, мы видим, что времени отклика Express.js примерно на 10% лучше. Но было бы неправильно рассматривать здесь только время отклика, потому что по мере увеличения количества запросов показатель успешной обработки падает. Мы видим, что Express.js имеет здесь самый высокий показатель успеха. За ним следуют Fastify, а Koa.js занимает последнюю строчку.
Подводя итог, согласно результатам наших бенчмарков, Express.js показал себя лучшим фреймворком в отношении времени отклика и показателей успешно обработанных запросов. Причина может быть в том, что продукт находится на рынке дольше всех и, следовательно, лучше оптимизирован для наших тестовых сценариев. Однако при работе с более сложными проектами и при использовании middleware-агентов с колбэк-функциями производительность Express.js может начать снижаться. Два других фреймворка потенциально могут справиться с такими сценариями лучше, чем Express.js, поскольку они являются более современными. Вы также должны учитывать, что эти результаты могут различаться в разных системах. Наша тестовая среда сравнительно мала и начинает давать сбои уже на 1000 запросов в секунду.
Наш вердикт – все эти три фреймворка работают хорошо, но Express.js показал себя немного лучше остальных, и их все определенно можно рассматривать в качестве рабочего варианта для вашего проекта. Чтобы еще больше расширить свои возможности тестирования производительности, зарегистрируйтесь на платформе Ddosify Cloud, которая предлагает глобальное no-code решение для точного и непрерывного бенчмаркинга.
Материал подготовлен в преддверии старта курса "Node.js Developer". Недавно в рамках этого курса прошел открытый урок «Создание микросервисов при помощи NodeJS и GRPC». На этой встрече участники познакомились с плюсами и минусами микросервисного подхода, как мигрировать монолит на микросервисы. Также разобрали решение на базе NodeJS + протокола GRPC. Если эта тема вам интересна, запись урока можно посмотреть на странице курса.