GOAL24 Mini App
GOAL24 Mini App

Привет, Хабр!

В этой статье — история запуска Telegram Mini App, куда за трое суток пришло 100 000 реальных пользователей.

Покажу, как мы масштабировали Node.js приложения на многоядерных серверах, увеличивали RPS в 10 раз, боролись с N+1 проблемой в MongoDB и снижали нагрузку на CPU. А ещё расскажу как мы быстро настроили мониторинг через Grafana, подключили Cloudflare и интегрировали Sentry. Поделюсь практическими инсайтами о том, на что стоит обращать внимание в первую очередь, и как эти инструменты помогли нам оперативно находить узкие места и устранять сбои в реальном времени. Всё, о чём будет в этой статье, основано на том, что действительно сработало. Кроме того, расскажу, какие моменты мы упустили до запуска.

Это разбор с цифрами, графиками и практическими выводами. Он может сэкономить вам время, нервы и деньги, если вы готовитесь к запуску Telegram Mini App или просто работаете с Node.js приложениями, которые могут оказаться под серьёзной нагрузкой.

Это первая часть истории — про то, как мы готовились к запуску, что предусматривали и на что делали ставку.

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

Про автора

Меня зовут Женя, сейчас я работаю разработчиком в Google. В стартапах был в роли CTO более 4 лет, а всего в IT — уже больше 9 лет, преимущественно в крупных компаниях и в области веб-разработки. И мне невероятно интересно с помощью технологий и людей реализовывать классные проекты, помогать бизнесу достигать целей и делать полезные сервисы для людей.

Что за приложение?

Это мини-приложение для Telegram на футбольную тематику, созданное специально для аудитории канала GOAL24. Если интересно посмотреть, как его анонсировали, вот пост в канале. Само приложение доступно по ссылке: GOAL24APPBOT.

Анонс приложения в канале
Анонс приложения в канале

Мы были приятно удивлены тёплой реакцией сообщества на анонс — это добавило волнения перед релизом.

С технической стороны всё устроено так: фронтенд на Next.js, бэкенд — несколько сервисов на Nest.js, база данных MongoDB, всё развёрнуто в Docker контейнерах с Docker Compose и деплоится через GitLab CI.

Как мы готовились к запуску (и почему выбрали день эль-классико)

Обычно наши проекты стартовали плавно: сначала релиз, потом — постепенный рост аудитории (хотя, будем честны, не всегда), и как результат — предсказуемая нагрузка. Но этот случай был другим. Мы точно знали: сразу после запуска в приложение зайдут минимум 5–10 тысяч человек. И это ещё не предел.

Наше приложение по характеру нагрузок очень похоже на онлайн-маркет в чёрную пятницу. Сбой в критичный момент будет означать провал. Основные пики активности будут приходиться на время матчей, особенно — когда включён матч-центр, отображающий футбольные события в реальном времени.

Запуск мы назначили на субботу 26 апреля — день эль-классико, когда Барселона встречается с Реалом. Идеальный момент, чтобы привлечь пользователей. Но и большой риск: сервису сразу после запуска предстоит выдержать большой наплыв аудитории.

Чтобы не провалиться в день запуска, мы начали готовиться задолго до него. Ниже — что именно сделали.

Облачная инфраструктура

И нет, это не GCP или AWS.

Мы - маленькая инди-компания стартап, а это значит, что классические клауды для нас — непозволительная роскошь на данном этапе.

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

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

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

  • Фронтенд: 16 CPU + 64 GB RAM

  • Бэкенд: 24 CPU + 128 GB RAM

После тестов мы пришли к выводу, что этих мощностей вполне хватит. В итоге за два сервера мы платим меньше 200 $ в месяц, что для нас вполне подходит.

Для сравнения: в GCP за аналогичные мощности нам пришлось бы отдать около 2 350 $ в месяц. В AWS — примерно те же цифры.

Примерно столько стоил бы наш фронтенд сервер в GCP
Примерно столько стоил бы наш фронтенд сервер в GCP
И примерно столько стоил бы наш бэкенд сервер в GCP
И примерно столько стоил бы наш бэкенд сервер в GCP

Выходит, что просто выбрав провайдера по потребностям проекта, мы экономим более 2 000 $ каждый месяц — без потери в производительности. Это существенно, особенно если проект пока не приносит выручку.

К тому же, в тех же GCP или AWS получить подобные конфигурации "с ходу" не получится. Некоторые конфигурации могут быть не доступны в нужном регионе, требуется длительная верификация юридического лица, заключение отдельного договора с поддержкой, а иногда и дополнительные платежи — например, в районе 50 $ в месяц только за поддержку.

Для хранения статики мы выбрали S3-совместимое хранилище от альтернативного провайдера. Оно выполняет ту же задачу, что и Amazon S3, но обходится нам значительно дешевле — 6 $ за терабайт против 26 $ в AWS.

Нагрузочное тестирование

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

Но в нашем случае было очевидно одно: нагрузочное тестирование необходимо. Мы хотели заранее понять:

  • какую нагрузку наше приложение вообще способно выдержать,

  • когда и по каким причинам оно начнёт падать,

  • какие метрики стоит мониторить особенно внимательно после запуска.

Какие инструменты мы использовали

Разумеется, существуют готовые сервисы для распределённого нагрузочного тестирования. Они позволяют эмулировать масштабную активность с разных узлов и запускать сложные сценарии. Но на старте вникать в эти инструменты — долго: нужно поднять инфраструктуру, разобраться в конфигурации, всё настроить. А у нас самым жёстким ограничением было время.

Поэтому мы пошли максимально прямым путём — использовали для генерации нагрузки свои локальные машины. Это дало нам достаточное понимание поведения системы без долгой подготовки.

Сами тесты мы запускали через K6 и JMeter. Использование двух инструментов помогло нам убедиться, что цифры — объективные, и что они не искажены особенностями конкретного инструмента.

Кстати, JMeter-тесты можно удобно генерировать с помощью LLM. У него довольно неудобный XML-синтаксис тест-планов, и писать всё вручную — то ещё удовольствие. Сейчас это уже не проблема.

Что именно мы тестировали

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

Для бэка выбрали несколько ключевых эндпоинтов: один с тяжёлой логикой, другой — попроще. Было интересно посмотреть, как именно приложение на Nest.js расходует ресурсы в условиях пиков.

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

Проблема интерпретации

Очень важно правильно интерпретировать результаты. Например, если вы запускаете тест с одной машины, есть риск, что упираетесь не в тестируемый сервис, а в ограничения именно этой машины, которая генерирует нагрузку: сокеты, CPU, сетевая карта. Ошибки при этом могут выглядеть как ошибки со стороны приложения.

JMeter, например, по умолчанию просто показывает ошибки без подробностей. Нам помогло простое решение — логирование ошибок в отдельный файл. Когда что-то идёт не так, то можно легко и быстро узнать детали ошибки. Так же было полезно автоматически останавливать тест при первой же ошибке, чтобы не собирать заведомо бесполезную информацию.

Результаты по фронтенду

До 110 запросов в секунду всё было в порядке: задержка держалась на уровне <100 мс. Но дальше начались проблемы. При ~140 RPS задержка доходила до одной секунды, а на 250 запросах часть ответов начинала приходить с задержкой в 20 секунд и больше. Понятно, что в продакшене такие значения неприемлемы.

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

Сервер для фронтенда у нас не слабый: 16 ядер, но почти вся нагрузка обрабатывалась только одним. Средняя загрузка CPU во время теста была около 9%. Это ожидаемо для Node.js — он по умолчанию работает в одном потоке, но с точки зрения использования ресурсов это крайне неэффективно. Особенно если рассчитывать на рост и необходимость масштабирования.

Также в процессе стало понятно, что мы забыли про rate limit и защиту от DDoS. В боевых условиях без этого очень легко положить сервис буквально одним скриптом. Это сразу попало в список приоритетов.

Результаты по бэкенду

С бэкендом картина была похожей. Мы тестировали два эндпоинта — один с тяжёлой логикой, другой — относительно лёгкий.

Для тяжёлого получили около 110 RPS. После этого начали расти задержки:

110 RPS для тяжелого эндпоинта
110 RPS для тяжелого эндпоинта
Задержка в миллисекундах по мере роста количества отправляемых запросов
Задержка в миллисекундах по мере роста количества отправляемых запросов

Лёгкий эндпоинт держался лучше — около 260 RPS:

260 RPS для лёгкого эндпоинта
260 RPS для лёгкого эндпоинта
И так же похожий рост задержки
И так же похожий рост задержки

Примерно после 700 отправляемых запросов в секунду начались проблемы с таймаутами, а ближе к 1000 — стали появляться 502 ошибки.

CPU, как и на фронтенде, почти не чувствовал нагрузки — одно ядро было занято, остальные 23 почти простаивали. Суммарная загрузка не поднималась выше 7%.

В целом, мы изначально ожидали, что без дополнительной настройки и оптимизаций так примерно и будет: слабые показатели RPS, большие задержки, неэффективное использование ресурсов серверов.

Эти тесты стали нашей отправной точкой. Дальше по плану — кластеризация и распределение нагрузки по ядрам. Плюс Rate Limit, DDoS Protection и мониторинг.

Распределение нагрузки по ядрам

Распределение нагрузки по ядрам в Node.js приложении — это отдельная интересная тема.

Кратко по теории: Node.js — это один поток на один процесс. Но, несмотря на это, Node.js хорошо масштабируется на многоядерных машинах.

Чтобы добиться распределения нагрузки по ядрам в Node.js у нас есть 2 базовых варианта:

  1. Запускать дочерние процессы или отправлять сообщения дополнительным worker процессам, которые будут выполняться на других ядрах — подходит в ситуациях, когда есть много CPU-зависимых задач, например обработка изображений.

  2. Запуск нескольких Node.js серверов на одной многоядерной машине — это вариант проще и гораздо лучше подходит для веб-сервисов.

Мы выбрали второй вариант. В качестве инструмента взяли PM2 — это продвинутый менеджер процессов для Node.js, в котором есть много полезных возможностей. Нам особенно был нужен кластерный режим, чтобы запустить много реплик приложения и использовать больше доступных ядер при обработке нагрузки.

Да, с точки зрения Docker решение спорное. Много процессов внутри одного контейнера, накладные расходы на дополнительный уровень распределения нагрузки по репликам. Вот отличная статья по этому поводу, где прямо написано “Don’t Use Process Managers In Production”. Но, у нас оставалось несколько недель до запуска, и нам нужно было срочно увеличить пропускную способность, чтобы наши стыдные 100-200 RPS умножить хотя бы на 10, с учетом наших мощностей.

Поэтому, мы приступили к внедрению PM2.

Конфигурация оказалась довольно простой:

 PM2 конфигурация
PM2 конфигурация

Контейнер в Docker запускается командой:

CMD ["pm2-runtime", "pm2.config.js"]

На этом, по сути, всё.

Проверить, что всё работает корректно, можно через:

docker exec -it <container_id> pm2 list
 Вывод команды pm2 list внутри контейнера
Вывод команды pm2 list внутри контейнера

Мы запустили 12 экземпляров приложения на фронтенде и 18 — на бэкенде. После чего сразу же повторили нагрузочное тестирование, чтобы понять, что изменилось.

Нагрузочное тестирование с PM2

Методология оставалась неизменной — те же инструменты (K6, JMeter), те же эндпоинты, идентичные условия проведения.

Фронтенд - Next.js, SSR

Исходные показатели:

  • Максимум 110 RPS до роста задержек

  • При 140 RPS время ответа достигало 1 секунды

После запуска 12 инстансов через PM2:

  • 800 RPS с p50=50 мс и p95=100 мс

  • При 1000 RPS:

    • Всего 0.2% 5хх ошибок

    • Максимальная задержка 200 мс

  • 8-кратный рост производительности

Детальная информация про нагрузочный тест фронтенда
Детальная информация про нагрузочный тест фронтенда
Нагрузка на CPU 16 ядерного сервера во время нагрузочного теста
Нагрузка на CPU 16 ядерного сервера во время нагрузочного теста

Важно отметить: все тесты проводились без какого-либо кэширования — каждый запрос запускал полноценный SSR-рендеринг в Next.js. Мы понимали неэффективность такого подхода, однако даже в этих условиях система показала достойные результаты. Это давало нам уверенность, что после внедрения кэширования и SSG производительность вырастет ещё значительнее.

Понятно, что 1 000 RPS точно нельзя назвать рекордными. Но это уже то число, которое в теории должно позволить нам хорошо запуститься, и уже дальше работать над оптимизациями.

Бэкенд - Nest.js

Для бэкенда результаты оказались похожими. На сложном эндпоинте API стабильно выдавала 1 200 RPS при средней задержке всего 20 мс. Это значит, что более легкие эндпоинты спокойно будут отдавать больше 2 000 RPS.

Детальная информация про нагрузочный тест бэкенда
Детальная информация про нагрузочный тест бэкенда

Мы увидели, что нагрузка равномерно распределяется по ядрам процессора. В итоге мы добились 10-кратного увеличения производительности по сравнению с исходными показателями.

Это был важный этап — удалось решить ключевую проблему, которая потенциально могла сорвать запуск. Хотя конечно мы понимали, что эти показатели для нашего железа явно не большие и что нам предстоит серьезная работа по дальнейшей оптимизации.

Cloudflare

Cloudflare очень быстро подключать к домену и он из коробки даёт очень много полезных возможностей.

Я думаю это must-have для любого production проекта, особенно учитывая, что можно пользоваться бесплатным тарифом, либо версией за 20 $ в месяц, разница между ними незначительная.

Первое, что мы подключили — это DDoS защита и Rate Limit. Всё очень легко подключается, конфигурации применяются почти мгновенно. Очень удобно можно видеть сколько трафика было отфильтровано, с каких IP адресов поступают запросы, находится ли домен под атакой.

Отфильтрованные по Rate Limit запросы
Отфильтрованные по Rate Limit запросы

Сразу хочу предостеречь — не стоит включать все оптимизации скорости, которые предлагает Cloudflare без точного понимания того, как это скажется на вашем приложении. Мы изначально попробовали это сделать, и наше приложение перестало открываться в Telegram. Тогда мы просто выключили все оптимизации и включили только те, которые нам действительно нужны.

Все оптимизации Cloudflare
Все оптимизации Cloudflare

Кэширование статики

Отдельно хочу остановиться на кэшировании в Cloudflare. Оно включается буквально по нажатию двух кнопок и начинает очень эффективно кэшировать всю статику в проекте. Особенно актуально для фронтенда. Мы увидели, что более 80% всех фронтенд запросов раздаётся из кэша, что очень сильно снижает нагрузку на оригинальный сервер.

Это особенно актуально для Next.js, потому что в нашем случае больше половины всех запросов к фронтенду составляли запросы к /_next/images, и это очень существенно нагружает CPU. Происходит это потому что Next.js не оптимизирует изображения заранее при сборке, а делает это только после запроса от пользователя на конкретное изображение.

Поэтому после того, как мы подключили кэширование нагрузка на CPU фронтенд сервера упала на 80%.

Кэширование запросов к фронтенду на уровне Cloudflare за 24 часа
Кэширование запросов к фронтенду на уровне Cloudflare за 24 часа

Однако, не всё так гладко. Когда мы запускались - картинки у нас не кэшировались, потому что для этого нужно было добавить правило кэширования! Мы это сделали только спустя неделю после релиза, потому что сразу просто упустили этот момент. Поэтому первую неделю нам было тяжело. Вот это простое правило, пользуйтесь:

Правило кэширования для Next.js картинок
Правило кэширования для Next.js картинок

Забегая вперёд скажу, что это было одним из самых эффективных способов снизить нагрузку на CPU фронтенд сервера.

Аналитика трафика

В Cloudflare есть широкие возможности для анализа трафика. Можно смотреть все запросы к приложению, объём передаваемых данных, количество просмотров разных страниц, информацию про пользователей. Так же есть очень гибкая система фильтров и сортировок. К примеру, если нужно посмотреть сколько запросов было сделано в конкретный промежуток времени к одному конкретному API эндпоинту, то это можно сделать буквально в несколько кликов.

И при всём этом очень небольшая задержка в актуальности данных, около пяти минут.

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

Анализ кодов ответов за период времени в Cloudflare
Анализ кодов ответов за период времени в Cloudflare

Так же Cloudflare имеет возможности по Web аналитике. Можно узнать число уникальных визитов, количество просмотров страниц, время их загрузки, а так же Core Web Vitals показатели и динамику изменения всех этих значений во времени.

Дополнительно Cloudflare показывает лучшие возможности для оптимизации по Core Web Vitals, что тоже очень круто. Если сравнивать это с PageSpeed Insights от Google, то это в разы удобнее, хотя бы потому что можно легко и удобно отслеживать динамику и смотреть какой результат дают те или иные изменения.

Пример того, как это выглядит для показателя LCP (Largest Contentful Paint)
Пример того, как это выглядит для показателя LCP (Largest Contentful Paint)

Логирование

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

Тем не менее, у нас есть события, которые мы хотели логировать. Мы логируем взаимодействие с внешним API, которое является источником данных о матчах. Это помогает понять, что именно возвращал API, и увидеть возможные проблемы в данных. Также мы логируем события, связанные с выполнением Cron задач — они используются в нескольких частях приложения.

База данных

С MongoDB есть пару важных вещей, которые нужно было сделать перед релизом.

Во-первых, стоит ограничить максимальный размер кэша. Если ничего по этому поводу не сделать, то MongoDB будет съедать вплоть до 50% доступной оперативной памяти хост машины под кэширование. Почитать подробнее про это можно тут.

Во-вторых, необходимо настроить автоматическое резервное копирование. Мы реализовали выгрузку данных в объектное хранилище по определённому расписанию.

Поскольку у нас БД работает не в кластерном режиме, то больше ничего особого для запуска мы не делали.

Sentry

Мы понимали, что хотим иметь возможность после запуска видеть, какие у наших пользователей возникают проблемы. Иначе узнать о них будет очень сложно, особенно если речь идёт о проблемах, которые воспроизводятся не у всех, а только у какого-то процента пользователей.

Sentry показался нам идеальным инструментом для решения этой задачи. Он даёт необходимую информацию об ошибках пользователей: показывает, где именно они возникли, у какого количества пользователей и когда именно. Также он предоставляет много информации об устройствах пользователей и их операционных системах. Есть графики частоты той или иной ошибки, а также проигрывание сессий пользователей с ошибками. Очень похоже на Вебвизор в Яндекс.Метрике, но круче — потому что, помимо самого реплея сессии, есть доступ ко всем запросам и ответам в рамках этой сессии, доступ к консоли — по сути, почти как DevTools.

Пример ошибки в Sentry
Пример ошибки в Sentry
Реплей ошибки у пользователя с информацией про все его запросы в этой сессии
Реплей ошибки у пользователя с информацией про все его запросы в этой сессии

У Sentry есть self-hosted и облачные версии. Мы выбрали облачную, поскольку её проще и быстрее развернуть, не нужно настраивать отдельную машину и заниматься её обслуживанием. К тому же цены на облачную версию радуют. Можно использовать даже бесплатный тариф: единственное существенное ограничение — это только 5 000 последних ошибок в мониторинге. За 26 $ в месяц уже доступно 50 000 ошибок, что гораздо удобнее, и нам этого хватает с запасом.

Мы подключили Sentry только к нашему фронтенд-приложению. Сейчас я понимаю, что стоило подключить его и к бэкенду — это сильно бы нам помогло, но об этом позже.

Grafana

Grafana — отличный инструмент для визуализации метрик и мониторинга состояния инфраструктуры. Мы настроили связку Grafana + Prometheus + node-exporter — классическое решение для отслеживания ключевых показателей сервера: загрузки CPU, потребления оперативной памяти, дискового пространства и сетевой активности.

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

Например, у нас были случаи, когда из-за мусора, оставшегося после многочисленных деплоев Docker-контейнеров, диск оказывался полностью забит. В результате виртуальная машина просто переставала работать.

Внедрить Grafana вместе с Prometheus и node-exporter оказалось довольно просто. Мы используем self-hosted версию Grafana, а лимиты хранения метрик задаём на уровне Prometheus — в нашем случае они ограничены только объёмом диска. Это удобно: можно хранить большой объём данных и анализировать метрики за длительный период.

Итоги подготовки и анонс второй части

На этом завершается первая часть нашего пути — та, что была посвящена подготовке инфраструктуры. Мы оптимизировали серверные конфигурации, провели нагрузочное тестирование, внедрили кластеризацию через PM2 и настроили ключевые инструменты вроде Cloudflare и Sentry.

Но настоящие испытания начались после запуска, когда в игру вступили реальные пользователи.

Во второй части расскажу, что именно сломалось первым после релиза, как мы это чинили и какие решения помогли удержать приложение под нагрузкой.

Спасибо, что дочитали!

Буду рад вашим вопросам и комментариям — и до встречи в следующих постах.

P.S.

Если вам близки темы про продукты, людей и технологии — буду рад видеть вас в своём Telegram-канале. Там делюсь короткими выжимками, полезными наблюдениями и опытом, который накапливается в процессе запуска и развития проектов. Там уже опубликовано краткая выжимка из этой статьи, а ещё там есть пост про типовые ошибки в айтишных резюме которые я заметил, пока отсматривал 400+ резюме программистов и шаблон резюме, который помог мне пройти HR фильтры Google, Boeing, Visa и Revolut.

Комментарии (14)


  1. ivantgam
    31.05.2025 19:05

    Спасибо за интересный опыт!

    Насчёт серверов, вы взяли классические x86 или на arm? Монга хостится на том же хосте, где и бэкенд?


    1. pnmv
      31.05.2025 19:05

      на Google Tensor


    1. mav3riq Автор
      31.05.2025 19:05

      Спасибо за комментарий! Во второй части будет ещё интереснее :)

      Брали классические x86 root сервера. Монга пока на том же хосте, да.


  1. Politura
    31.05.2025 19:05

    Конфигурации, которые нам показались оптимальными:

    • Фронтенд: 16 CPU + 64 GB RAM

    • Бэкенд: 24 CPU + 128 GB RAM

    Один большой сервер на фронтенд и один большой сервер на бакенд это, не слишком оптимально - единая точка отказа.

    Если уж все в докер контейнерах, то стоило-бы попробовать AWS ECS который хостит контейнеры в Fargate + автоскейлер и все это за ALB, вышло-бы сильно дешевле 200 долларов в месяц, за счет автоскейрера, плюс надежность существенно выше, ибо контейнеры распределяются по разным физическим машинам, и даже по-умолчанию по двум авайлабилити зонам, так что даже если у Амазона датацентр целиком умрет, у вас все будет работать.

    Про фронтенд, есть какая-то острая необходимость в серверном рендеринге? Просто если обычное Реакт приложение с рендерингом на клиенте, то фронтнд сервер вообще не нужен, файлы кладутся в S3 и дистрибьюция через CloudFront, там FreeTier что-то типа 10 терабайт, так что обошлось-бы вам оно бесплатно.


    1. mav3riq Автор
      31.05.2025 19:05

      Спасибо за комментарий!

      На этапе первого запуска мы сознательно выбрали простой и предсказуемый сетап — тот, в котором всё работает именно так, как мы ожидаем, и стоит заранее известную сумму. При этом да, у нас есть единая точка отказа, это не очень надёжно.

      Сейчас уже начинаем рассматривать более гибкие решения, похожие на то, что вы описали.

      Наш характер нагрузки хорошо под это подходит: чаще всего в приложении онлайн до 500 пользователей, но бывают резкие всплески — когда за несколько секунд заходят 10–15 тысяч человек.

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

      Мы планируем серьёзные продуктовые доработки и хотим, чтобы пользователи заходили не только во время матчей, но и между ними. Это может привести к росту постоянного онлайна и изменению профиля нагрузки — соответственно, изменятся и расходы.

      Я думаю, что ваш подход на текущей нагрузке точно будет надежнее и выгоднее. Но в будущем нужно будет посчитать.

      Острой необходимости в SSR нет, и на текущий момент мы уже полностью перешли на SSG, про это будет подробнее во второй части статьи. Вы правы, на счёт того, что фронтенд сервер вообще не нужен, планируем убирать его.


  1. gfiopl8
    31.05.2025 19:05

    100т уников за 3 суток для вебаппки это же околонулевая нагрузка. Что надо делать что бы для этого не хватило 5 долларовой впс от ноунейм провайдера?


    1. dph
      31.05.2025 19:05

      Мне вот тоже кажется, что основной вывод из статьи - никогда не использовать стек JS+MongoDB. Ну или искать тех, кто умеет им пользоваться. Такого железа хватит на нагруженный сервер на несколько миллионов DAU...


      1. mav3riq Автор
        31.05.2025 19:05

        Не соглашусь с тем выводом, который вы делаете.

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

        Мы понимали, что вопрос именно в оптимизациях и корректной конфигурации.

        На самом деле, сейчас у нас всё очень комфортно по нагрузке: даже в пиковые моменты фронтенд сервер загружен не больше чем на 4%, а бэкенд — максимум на 18%. Это ситуации когда большой единомоментный трафик, больше чем 15 тысяч пользователей заходят за несколько секунд. А в обычных ситуациях нагрузка на фронт в районе 1%, на бэк - 3%.

        Поэтому, не думаю, что есть какая-то проблема в стеке JS + MongoDB.

        Запас по ресурсам у нас сейчас приличный, и видно, что есть ещё куда оптимизировать, например, внедрить кэширование уже на уровне бэкенда (пока его у нас нет). Так что вопрос скорее не в стеке, а в том, как его использовать и насколько грамотно всё настроено.


        1. dph
          31.05.2025 19:05

          Т.е. вы поставили железа примерно в пять раз больше, чем планировали?
          Тогда о чем статья-то? О том, что надо нанимать опытных разработчиков?

          Да и получить 800rps на 16 CPU и 1200rps на 24 CPU - это очень грустно. Там же не платежи за каждым rps-ом прячутся, а что-то более простое...


    1. mav3riq Автор
      31.05.2025 19:05

      Соглашусь, 100 000 уников за 3 дня это не много. Но, ключевой фактор тут - это характер нагрузки. Есть ярковыраженные вспелски, когда за несколько секунд 10-15 тысяч пользователей одновременно заходят в приложение.

      Мы изначально брали сервера с запасом, потому что не было чёткого понимания, сколько реально придёт пользователей. По прогнозам, максимум могло быть до миллиона за три дня, минимум — тысяч 50. В итоге, вышло что-то среднее, но инфраструктуру закладывали именно с прицелом на максимальные значения, чтобы не получить аврал в случае удачного запуска.

      VPS за 5 долларов у ноунейм-провайдера, как правило, сильно ограничена по ресурсам. Если вдруг в течение пары секунд в приложение зайдёт 20 тысяч человек — такой сервер просто ляжет, особенно если это не статичный сайт, а динамическое приложение с авторизацией, API и базой данных. Тут вы, мне кажется, немного не учитываете реальные ограничения дешёвых VPS.

      Ну и плюс — мы специально брали инфраструктуру с запасом, чтобы потом не возвращаться к этому вопросу через месяц и не заниматься экстренным переносом. Это даёт возможность спокойно развивать продукт, не боясь внезапных всплесков трафика. При этом соглашусь, мы бы могли обойтись серверами не за 200 $, а скажем за 50 $, реалистично. Но разница между этими цифрами не такая уж существенная.


      1. poriogam
        31.05.2025 19:05

        Если за пару секунд 20т зайдет то 100т за 10 секунд закончатся и к исходу третьего дна на счетчике будут сотни миллиардов.


  1. markelov69
    31.05.2025 19:05

    1) Зачем, а главное для чего(цензура) для вашего приложения нужен Next.js? Это якорь который:
    - Тупо вставляет палки в колеса при разработке, накладывая свои ограничения.
    - Ставит на колени производительность из-за ненужного в вашем случае SSR на ровном месте.

    2) Зачем вы берете Nest.js, когда есть Express.js? Nest в свою очередь:
    - Так же тупо вставляет палки в колеса из-за своих ограничений и прихотей, это же именно фреймворк монструозный, жирнющий и уродливый.
    - Ставит на колени производительность просто тупо за счет своего runtime. А если вы ещё используете ORM, то вы ставите производительность ан колени в квадрате.
    - С точки зрения кода, на выходе получается месиво из декораторов.

    3) Зачем вы взяли MongoDB? Это же классическая ошибка и классический эпик фэйл, через который проходят все кто ее берет, потом в любом случае будет переезд на Postgres или MariaDB/MysQL. Почему просто сразу не взять Postgres или MariaDB и всё?

    4) Надеюсь вы знаете, что когда вы запускаете в Docker'e базу данных или Node.js приложение или что угодно ещё, вы теряете так же в районе 20-30% производительности на ровном месте. В зависимости от сценария, я тестил по классике через apache bencmark замеряя RPS, где endpoint делает 3 запроса в БД, парсит json, преобрзаует объект в json и отдает ответ. Я проводил бенчмарки и в том время когда Docker только появился, и в прошлом году, в плане просадки по производительности ничего не изменилось в лучшую сторону. Всё так же приходится платить эту цену запуская что-то в докер контейнере.


    1. Politura
      31.05.2025 19:05

      2) Зачем вы берете Nest.js, когда есть Express.js? Nest в свою очередь:

      ...

      - Ставит на колени производительность просто тупо за счет своего runtime

      Разве? Есть какие-нибудь тесты, которые показывают это? Быстрогуглом нашел только минимальное влияние, но я особо долго и не искал. И что там за рантайм? По идее, DI разруливается во время запуска, а во время работы уже прямые вызовы.


  1. php7
    31.05.2025 19:05

    Это ваш первый проект на Node?