На определённом этапе своей карьеры я задался вопросом: может ли Next.js работать в многопоточном режиме? Оказалось, что нет. Это побудило меня разобраться, как можно организовать многопоточную работу Next.js и насколько это оправдано для сайтов с высокой нагрузкой.
Содержание
Какую страницу будем нагружать?
Для тестирования я использую страницу своего проекта по мониторингу падений сайтов с уведомлениями в Telegram. Основная страница сайта генерируется в статичную страницу (SSR), имеет картинки визуализации работы, на ней нет JS-интерактива. Одна из ключевых задач этой страницы — корректный рендеринг для SEO-оптимизации.
В статичном виде вес страницы составляет 71,2 КБ, а в полностью отрендеренном виде — 4,2 МБ.
Выглядит страница вот так:
Код страницы включает метаданные, Open Graph и HTML, написанный с использованием JSX (внутри используются такие же React-компоненты без интерактива, структурированные по секциям):
import { Metadata } from "next"; import HeaderComponent from "../pages-components/header/HeaderComponent"; import MainComponent from "../pages-components/main/MainComponent"; import FeaturesComponent from "../pages-components/features/FeaturesComponent"; import FooterComponent from "../pages-components/footer/FooterComponent"; import AdvantagesComponent from "../pages-components/advantages/AdvantagesComponent"; import Price from "../pages-components/price/Price"; import HowItWorksComponent from "../pages-components/how-it-works/HowItWorksComponent"; import { MessageUsComponent } from "@/util/components/MessageUsComponent"; export const metadata: Metadata = { title: "Мониторинг сайтов | Проверятор", description: "Бесплатный мониторинг доступности сайтов 24\\7. Уведомим о сбоях в работе сайта в Telegram, по почте или SMS. Проверка сайта раз в минуту (включая Nginx, WordPress и другие сайты)", keywords: "мониторинг сайтов, проверка доступности, проверка сбоев сайта", icons: { icon: "/favicon.ico", }, alternates: { canonical: `https://proverator.ru`, }, openGraph: { title: "Мониторинг сайтов | Проверятор", description: "Бесплатный мониторинг доступности сайтов 24\\7. Уведомим о сбоях в работе сайта в Telegram, по почте или SMS. Проверка сайта раз в минуту (включая Nginx, WordPress и другие сайты)", url: "https://proverator.ru", type: "website", images: ["https://proverator.ru/banner.png"], }, }; export default function Home() { return ( <> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({ "@context": "https://schema.org/", "@type": "WebPage", name: "Мониторинг сайтов | Проверятор", description: "Бесплатный мониторинг доступности сайтов 24\\7. Уведомим о сбоях в работе сайта в Telegram, по почте или SMS. Проверка сайта раз в минуту (включая Nginx, WordPress и другие сайты)", url: "https://proverator.ru", }), }} /> <HeaderComponent /> <div style={{ width: "100%", maxWidth: "100vw", overflowX: "hidden", position: "relative", }} > <main> <MainComponent /> </main> <HowItWorksComponent /> <FeaturesComponent /> <AdvantagesComponent /> <Price /> <FooterComponent /> <MessageUsComponent /> </div> </> ); }
Скрипт для стресс-теста
Скрипт для стресс-тестирования я написал на Python с помощью ChatGPT o1. Запросы к сайту выполняются в 10 параллельных процессах (а не потоках, так как GIL не поддерживает «полноценную» многопоточность).
В течение минуты мы отправляем асинхронные запросы, а затем подсчитываем количество успешных ответов:
import asyncio import aiohttp import time import multiprocessing async def fetch(session, url, success_counter, end_time): while time.time() < end_time: try: async with session.get(url) as response: if response.status == 200: with success_counter.get_lock(): success_counter.value += 1 except Exception: pass # Ignore exceptions to continue the stress test async def runner(url, success_counter, end_time): async with aiohttp.ClientSession() as session: await fetch(session, url, success_counter, end_time) def process_function(url, success_counter, end_time): asyncio.run(runner(url, success_counter, end_time)) def main(url): success_counter = multiprocessing.Value('i', 0) end_time = time.time() + 60 # Run for 1 minute processes = [] for _ in range(10): # Up to 10 processes p = multiprocessing.Process(target=process_function, args=(url, success_counter, end_time)) p.start() processes.append(p) for p in processes: p.join() print(f"Number of successful requests: {success_counter.value}") if __name__ == "__main__": import sys if len(sys.argv) != 2: print("Usage: python stress_test.py <URL>") sys.exit(1) url = sys.argv[1] main(url)
Конфигурация компьютера
Стресс-тест я запускаю на своём рабочем компьютере с процессором AMD Ryzen 9 7950X (16 ядер, 32 потока), 64 ГБ оперативной памяти и NVMe-диском. Операционная система — Windows 11.
Тестирование провожу локально, чтобы избежать ограничений по пропускной способности домашнего интернета и издержек коммуникации с сервером через интернет.
Стресс-тест в однопотоке
Собираю и запускаю сайт:
> npm run build > npm run start
Запускаю тест в три итерации:
> python .\stresstest.py http://localhost:3000 > Number of successful requests: 94482 > python .\stresstest.py http://localhost:3000 > Number of successful requests: 92523 > python .\stresstest.py http://localhost:3000 > Number of successful requests: 93764
Загрузка компьютера во время теста (в среднем ~24% CPU, в простое ~2-7%):
Как запустить многопоточность?
Запуск многопоточного режима в Next.js оказался нетривиальной задачей. Я пробовал разные варианты с PM2, но безуспешно. После нескольких часов изучения нашёл статью на dev.to для старой версии Next.js 12.
Конечно, с первой попытки ничего не заработало, но после некоторых манипуляций удалось нащупать рабочий скрипт:
// start-multicore.js const cluster = require("node:cluster"); const process = require("node:process"); const CPU_COUNT = require("os").cpus().length; if (cluster.isPrimary) { cluster.setupPrimary({ exec: require.resolve("next/dist/bin/next"), args: ["start", ...process.argv.slice(2), "-p", "3000"], stdio: "inherit", shell: true, }); for (let i = 0; i < CPU_COUNT; i++) { cluster.fork(); } cluster.on("exit", (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`, { code, signal }); }); }
Далее, запускаю NextJS на все ядра компьютера (получается 32 потока):
> node .\start-multicore.js
P.S. Тут учитываем, что и скрипт теста, и фоновые задачи будут всё на тех же физических ядрах.
Стресс-тест в многопотоке
Собираю и запускаю сайт:
> npm run build > node .\start-multicore.js
Запускаю тест в многопотоке в три итерации:
> python .\stresstest.py http://localhost:3000 > Number of successful requests: 231428 > python .\stresstest.py http://localhost:3000 > Number of successful requests: 235704 > python .\stresstest.py http://localhost:3000 > Number of successful requests: 239138
Загрузка CPU во время теста (~44%):
Результаты
Результаты теста показали, что многопоточная версия обрабатывает примерно в 2.5 раза больше запросов, чем однопоточная. График для наглядности:
Однако стоит учесть несколько факторов:
-
что в фоне запущены другие программы;
-
во время теста скрипт и сайт работают на одном и том же компьютере;
-
несмотря на рост мощности в 32 раза (по количеству ядер), число обработанных запросов увеличилось всего в 2.5 раза.
Заключение
Стоит ли использовать многопоточность для Next.js? Вопрос неоднозначный. Я считаю, что это всё же не лучший подход.
В синтетическом тесте удалось увеличить количество обрабатываемых страниц в локальной сети. Но в реальных условиях серверы обычно менее мощные, и их пропускная способность варьируется от 100 Мбит/с до 1 Гбит/с.
Максимум, которого я достиг — около 250 000 запросов в минуту для статичной страницы без рендеринга. При этом в отрендеренном виде сайт весит 4.2 МБ.
Предположим, сайт идеально оптимизирован: вся статика вынесена в CDN, а сервер отдаёт 1 МБ данных на запрос. Даже допустим, что половина пользователей уже имеет закэшированную версию сайта, и фактически с сервера передаётся только 0.5 МБ на каждый запрос.
При пропускной способности 1 Гбит/с (125 МБ/с) сервер сможет обработать максимум 15 000 запросов в минуту (в сферически идеальных условиях). Мы всё ещё не приближаемся к пределам однопоточного Next.js. Да и большой вопрос, насколько многопоточный режим будет хорошо работать в связке с другими процессами, Nginx’ом и т.д.
Поэтому, если сайт растёт, разумнее начинать горизонтально масштабироваться. Всё-таки мы не хотим единую точку отказа, а хотим много серверов с load balancing’ом. В этой ситуации однопоточной версии Next.js будет достаточно. Так будет и надёжнее, и более рентабельно используем ресурсы.
Тем более, использование нестандартных скриптов — усложняет поддержку. С новой версией NextJS такой способ «распараллеливания работы» может перестать работать. Лучше отдать задачу оптимизации NextJS команде разработки NextJS.
Надеюсь, мой эксперимент оказался для вас наглядным.
ссылка на оригинал статьи https://habr.com/ru/articles/848052/
Добавить комментарий