Часто в интернетах пишут про NODE_COMPILE_CACHE и ускорение скриптов.
Но добавляю к скрипту который запускает webpack сборку небольшого example приложения (соответственно считывает просто кучу модулей по пути, в том числе babel, его плагины и тд), и вижу либо отсутствие изменений либо ухудшение (штраф за формирование этого кэша).
При отладке через
У кого-нибудь есть успешный опыт интеграции NODE_COMPILE_CACHE?
Также, пока писал пост понял что при сборе CPU profile в Node.js не вижу сколько эта компиляция в принципе занимает времени, в отличие от обычной performance вкладки в девтулзах клиентских приложений, где есть время Compile code / Compile script. Можно ли это собрать для Node.js скрипта?
Но добавляю к скрипту который запускает webpack сборку небольшого example приложения (соответственно считывает просто кучу модулей по пути, в том числе babel, его плагины и тд), и вижу либо отсутствие изменений либо ухудшение (штраф за формирование этого кэша).
При отладке через
NODE_DEBUG_NATIVE=COMPILE_CACHE
, логи показывают успешное переиспользование кэша.У кого-нибудь есть успешный опыт интеграции NODE_COMPILE_CACHE?
Также, пока писал пост понял что при сборе CPU profile в Node.js не вижу сколько эта компиляция в принципе занимает времени, в отличие от обычной performance вкладки в девтулзах клиентских приложений, где есть время Compile code / Compile script. Можно ли это собрать для Node.js скрипта?
И раз уж зашел разговор о CLI, поделюсь одной из актуальных задач - разработка обновленной @tramvai/cli (уже писал про это короткий пост)
Во вложении - дизайн новой CLI, он уже претерпел ряд изменений, но основные концепции остались.
Какие основные цели для новой CLI:
- решить базовые проблемы с перформансом - основная, webpack MultiCompiler запускает все сборки в одном процессе, серверная и клиентская конкурируют между собой
- реализовать удобную систему плагинов (и первым же новым плагином интегрировать rspack)
- полностью разделить JS API и CLI API
- сделать общий набор тест-кейсов, который будет удобно запустить с разными плагинами - вебпак+бабель, вебпак+swc, rspack
- избавиться от легаси, улучшить отладку, упростить структуру
Итак, основная техническая задачка тут - ускорение двух параллельных вебпак сборок.
Тут очевидное решение - вынести их в worker_threads, что из коробки webpack и его MultiCompiler не умеет.
И главный челлендж тут - как передать конфигурацию из CLI в воркеры, если там могут быть плагины - то есть не сериализуемые методы/классы/прочие объекты?
Этот кейс решил следующим образом:
- есть общая логика - чтение
- есть набор входящих сериализуемых параметров, которые можно передать через CLI (`tramvai start ...`) или JS API (`new Tramvai().start(...)`)
- и основной процесс и webpack воркер - считывают один и тот же конфигурационный файл
- входящие параметры пробрасываются при старте воркера из основного процесса
Вокруг воркеров сделал небольшие удобные обертки для контроля и коммуникации.
Основной пакет @tramvai/api определяет базовые интерфейсы - DevServer и Builder, ждет их в DI контейнере, и запускает их жизненный цикл.
Вся логика с webpack, реализация DevServer на основе webpack-dev-middleware и соответствующие зависимости - в отдельном @tramvai/cli-plugin-webpack плагине, аналогичный будет для rspack.
Все babel зависимости и фабрика babel конфига - в отдельном @tramvai/cli-plugin-babel, и соответственно такой же будет для swc.
Набор тестов сделан как фабрика, под разные плагины но с общими кейсами и фикстурами.
Стараюсь добавлять тест-кейс на каждый значимый параметр сборки, который мы явно конфигурируем.
Похоже основным челленджем далее будет - миграция пользователей и временная поддержка двух реализация команды tramvai start.
Во вложении - дизайн новой CLI, он уже претерпел ряд изменений, но основные концепции остались.
Какие основные цели для новой CLI:
- решить базовые проблемы с перформансом - основная, webpack MultiCompiler запускает все сборки в одном процессе, серверная и клиентская конкурируют между собой
- реализовать удобную систему плагинов (и первым же новым плагином интегрировать rspack)
- полностью разделить JS API и CLI API
- сделать общий набор тест-кейсов, который будет удобно запустить с разными плагинами - вебпак+бабель, вебпак+swc, rspack
- избавиться от легаси, улучшить отладку, упростить структуру
Итак, основная техническая задачка тут - ускорение двух параллельных вебпак сборок.
Тут очевидное решение - вынести их в worker_threads, что из коробки webpack и его MultiCompiler не умеет.
И главный челлендж тут - как передать конфигурацию из CLI в воркеры, если там могут быть плагины - то есть не сериализуемые методы/классы/прочие объекты?
Этот кейс решил следующим образом:
- есть общая логика - чтение
tramvai.config.ts
конфигурационного файла, где могут быть плагины- есть набор входящих сериализуемых параметров, которые можно передать через CLI (`tramvai start ...`) или JS API (`new Tramvai().start(...)`)
- и основной процесс и webpack воркер - считывают один и тот же конфигурационный файл
- входящие параметры пробрасываются при старте воркера из основного процесса
Вокруг воркеров сделал небольшие удобные обертки для контроля и коммуникации.
Основной пакет @tramvai/api определяет базовые интерфейсы - DevServer и Builder, ждет их в DI контейнере, и запускает их жизненный цикл.
Вся логика с webpack, реализация DevServer на основе webpack-dev-middleware и соответствующие зависимости - в отдельном @tramvai/cli-plugin-webpack плагине, аналогичный будет для rspack.
Все babel зависимости и фабрика babel конфига - в отдельном @tramvai/cli-plugin-babel, и соответственно такой же будет для swc.
Набор тестов сделан как фабрика, под разные плагины но с общими кейсами и фикстурами.
Стараюсь добавлять тест-кейс на каждый значимый параметр сборки, который мы явно конфигурируем.
Похоже основным челленджем далее будет - миграция пользователей и временная поддержка двух реализация команды tramvai start.
🔥5👍2
Одна из классных идей в новой CLI - кастомные трейсы в формате Trace Event Format
Идея взята у Parcel, Rspack и Next.js, примеры:
- https://parceljs.org/features/profiling/#tracing
- https://github.com/parcel-bundler/parcel/blob/v2/packages/core/profiler/src/Tracer.js
- https://rspack.dev/contribute/development/tracing
Написал кастомный трейсер поверх либы chrome-trace-event, пример API:
Во вложении пример визуализации кастомного трейса на сборку и несколько ребилдов, в интерфейсе https://ui.perfetto.dev/. Очень удобно смотреть сколько времени занимают основные операции, какие блокируют друг друга, где произошла ошибка (трейсы пишутся на диск не в конце а все время жизни скрипта)
В идеале - еще собирать более подробные трейсы по сборке через хуки бандлера.
Идея взята у Parcel, Rspack и Next.js, примеры:
- https://parceljs.org/features/profiling/#tracing
- https://github.com/parcel-bundler/parcel/blob/v2/packages/core/profiler/src/Tracer.js
- https://rspack.dev/contribute/development/tracing
Написал кастомный трейсер поверх либы chrome-trace-event, пример API:
const tracer = new Tracer();
tracer.wrap({ event: 'event' }, async () => {
await doSomethingAsync();
});
Во вложении пример визуализации кастомного трейса на сборку и несколько ребилдов, в интерфейсе https://ui.perfetto.dev/. Очень удобно смотреть сколько времени занимают основные операции, какие блокируют друг друга, где произошла ошибка (трейсы пишутся на диск не в конце а все время жизни скрипта)
В идеале - еще собирать более подробные трейсы по сборке через хуки бандлера.
🔥9
Привет!
Достаточно давно делился статьей где описывал различные механизмы и подходы которые мы применяем для SSR приложений на Tramvai (сейчас доступна на хабре).
Один из механизмов - Request Limiter, модуль который ограничивает количество параллельно обрабатываемых запросов при перегруженном Event Loop приложения, для возможности стабильно отдавать 2xx ответы и рендерить странички даже под большими нагрузками.
Работает по похожим принципам с https://github.com/fastify/under-pressure, только не отбрасывает все запросы при нагрузке, а держит еще LIFO очередь что бы обеспечить большее количество успешных ответов без сильной деградации времени ответа.
Когда переводили наши интеграционные тесты на 20 версию Node.js, начали падать нагрузочные тесты Request Limiter. Основная проблема - перестали быстро отвечать health-чеки приложения (а отзывчивые health-чеки и метрики очень важны и что бы в реальном времени понимать что происходить и для graceful рестартов и так далее)
Долго исследовал проблему, опытным путем обнаружил что проблема где-то на уровне http модуля ноды, запросы как будто в какой-то момент скапливаются в очередь и не обрабатываются далее.
Завел issue, получил много интересного и ценного фидбека от Matteo Collina - https://github.com/nodejs/node/issues/57364
Оказалось, что изменения в libuv убрали запуск таймеров в начале цикла на старте event loop, теперь они запускаются только в конце - то есть после poll (входящие запросы и прочее) и check (setImmediate).
В веб-сервере под капотом Tramvai мы испольуем setImmediate для того что бы иметь возможность "разорвать" event loop между тяжелыми задачами по SSR и ответить на легкие запросы /metrics, /health и так далее.
У libuv есть логика по ограничению одновременно обрабатываемых immediate коллбэков - https://github.com/libuv/libuv/blob/49b6e4db0cfc2bdb4c4151030618981c2fc0795b/src/unix/core.c#L462-L465
http модуль внутри использует различные таймеры/интервалы, до обновления ноды все это вместе с request limiter работало так скажем в гармонии, логика по обработке запроса (в том числе таймеры) выполнялась на одной фазе event loop до immediate коллбэков.
После обновления libuv в Node.js, если я все правильно понял, логика обработки запроса теперь раскидана до и после check фазы с immediate коллбэками, и происходит что-то вроде взаимной блокировки - мы не можем полноценно обрабатывать новые запросы (в том числе быстро ответить 429 кодом) пока в очереди есть много immediate коллбэков.
Также Маттео накинул несколько кейсов почему в целом опасно использовать setImmediate для дробления обработки запросов и что это просаживает перф - https://github.com/fastify/fastify/pull/545
Проблем тут вижу несколько:
- в нашем кейсе, SSR это не тысячи а десятки RPS как в бенчмарках fastify/h3, и такие проблемы нам не важны
- но при этом у нас была реальная и очень полезная возможность оставаться отзывчивыми, и держать под нагрузкой адекватные 2xx RPS
- также я не вижу интеграционных тестов в репозитории under-pressure и сомневаюсь до конца что инструмент работает ожидаемо
Пару дней назад Маттео написал даже в блок Platformatic (это их коммерческий продукт для Node.js стека) про кейс, итого получился подробный обзор проблемы (правда черезчур нейросетевой):
- https://x.com/matteocollina/status/1951322487595090177?s=19
- https://blog.platformatic.dev/the-dangers-of-setimmediate
Основной поинт Маттео который я полностью поддерживаю - надо использовать worker_threads, и само приложение поднимать в воркере, таким образом изолировать его event loop (тут он рекламирует их сервер Watt который так делает из коробки)
Для Tramvai тут проблема что это сильно не вписывается в текущую архитектуру.
Придется делать еще один заход и смотреть как мы можем избавиться от setImmediate, и что в итоге выжать из Request Limiter под нагрузками на свежих версиях Node.js
Достаточно давно делился статьей где описывал различные механизмы и подходы которые мы применяем для SSR приложений на Tramvai (сейчас доступна на хабре).
Один из механизмов - Request Limiter, модуль который ограничивает количество параллельно обрабатываемых запросов при перегруженном Event Loop приложения, для возможности стабильно отдавать 2xx ответы и рендерить странички даже под большими нагрузками.
Работает по похожим принципам с https://github.com/fastify/under-pressure, только не отбрасывает все запросы при нагрузке, а держит еще LIFO очередь что бы обеспечить большее количество успешных ответов без сильной деградации времени ответа.
Когда переводили наши интеграционные тесты на 20 версию Node.js, начали падать нагрузочные тесты Request Limiter. Основная проблема - перестали быстро отвечать health-чеки приложения (а отзывчивые health-чеки и метрики очень важны и что бы в реальном времени понимать что происходить и для graceful рестартов и так далее)
Долго исследовал проблему, опытным путем обнаружил что проблема где-то на уровне http модуля ноды, запросы как будто в какой-то момент скапливаются в очередь и не обрабатываются далее.
Завел issue, получил много интересного и ценного фидбека от Matteo Collina - https://github.com/nodejs/node/issues/57364
Оказалось, что изменения в libuv убрали запуск таймеров в начале цикла на старте event loop, теперь они запускаются только в конце - то есть после poll (входящие запросы и прочее) и check (setImmediate).
В веб-сервере под капотом Tramvai мы испольуем setImmediate для того что бы иметь возможность "разорвать" event loop между тяжелыми задачами по SSR и ответить на легкие запросы /metrics, /health и так далее.
У libuv есть логика по ограничению одновременно обрабатываемых immediate коллбэков - https://github.com/libuv/libuv/blob/49b6e4db0cfc2bdb4c4151030618981c2fc0795b/src/unix/core.c#L462-L465
http модуль внутри использует различные таймеры/интервалы, до обновления ноды все это вместе с request limiter работало так скажем в гармонии, логика по обработке запроса (в том числе таймеры) выполнялась на одной фазе event loop до immediate коллбэков.
После обновления libuv в Node.js, если я все правильно понял, логика обработки запроса теперь раскидана до и после check фазы с immediate коллбэками, и происходит что-то вроде взаимной блокировки - мы не можем полноценно обрабатывать новые запросы (в том числе быстро ответить 429 кодом) пока в очереди есть много immediate коллбэков.
Также Маттео накинул несколько кейсов почему в целом опасно использовать setImmediate для дробления обработки запросов и что это просаживает перф - https://github.com/fastify/fastify/pull/545
Проблем тут вижу несколько:
- в нашем кейсе, SSR это не тысячи а десятки RPS как в бенчмарках fastify/h3, и такие проблемы нам не важны
- но при этом у нас была реальная и очень полезная возможность оставаться отзывчивыми, и держать под нагрузкой адекватные 2xx RPS
- также я не вижу интеграционных тестов в репозитории under-pressure и сомневаюсь до конца что инструмент работает ожидаемо
Пару дней назад Маттео написал даже в блок Platformatic (это их коммерческий продукт для Node.js стека) про кейс, итого получился подробный обзор проблемы (правда черезчур нейросетевой):
- https://x.com/matteocollina/status/1951322487595090177?s=19
- https://blog.platformatic.dev/the-dangers-of-setimmediate
Основной поинт Маттео который я полностью поддерживаю - надо использовать worker_threads, и само приложение поднимать в воркере, таким образом изолировать его event loop (тут он рекламирует их сервер Watt который так делает из коробки)
Для Tramvai тут проблема что это сильно не вписывается в текущую архитектуру.
Придется делать еще один заход и смотреть как мы можем избавиться от setImmediate, и что в итоге выжать из Request Limiter под нагрузками на свежих версиях Node.js
Хабр
Масштабирование SSR-приложений
Привет! Меня зовут Олег Драпеза, я работаю техлидом в Тинькофф в команде Coretech Frontend. Мой основной проект — SSR мета-фреймворк tramvai , на котором работают несколько десятков фронтовых...
👍14🔥10❤3