惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 司徒正美
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
MIT News - Artificial intelligence
腾讯CDC
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
M
Microsoft Research Blog - Microsoft Research
阮一峰的网络日志
阮一峰的网络日志
H
Help Net Security
L
LangChain Blog
G
Google Developers Blog
Stack Overflow Blog
Stack Overflow Blog
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 【当耐特】
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
U
Unit 42
Recent Announcements
Recent Announcements
S
SegmentFault 最新的问题
大猫的无限游戏
大猫的无限游戏
博客园 - Franky
T
The Blog of Author Tim Ferriss
罗磊的独立博客
宝玉的分享
宝玉的分享
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
雷峰网
雷峰网
D
DataBreaches.Net
爱范儿
爱范儿
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
Hugging Face - Blog
Hugging Face - Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
K
Kaspersky official blog
P
Privacy & Cybersecurity Law Blog
博客园_首页
T
Threat Research - Cisco Blogs
I
InfoQ
有赞技术团队
有赞技术团队
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recorded Future
Recorded Future
量子位
H
Hackread – Cybersecurity News, Data Breaches, AI and More
GbyAI
GbyAI
Cyberwarzone
Cyberwarzone
B
Blog
C
Check Point Blog
P
Proofpoint News Feed
S
Securelist
A
Arctic Wolf

Все публикации подряд на Хабре

Почему советские программисты не сделали GTA Последовательное иерархическое распределение сумм. Создание БД. Распределение сумм по правилам средствами PostgreSQL Язык программирования T Может ли большая языковая модель обладать сознанием? Ключ к вычислимости ℵ₋₁ Как подсадить разработку на ИИ Кодировка: почему « ё » оказалось не моё? Использование SNMP Trap/Inform сообщений в мониторинге сети Как я не нашёл нормальную альтернативу WinSSHTerm на macOS, психанул и написал свою UUID мертв? Да здравствует Smart ID! Почему ваш проект заслуживает лучшего «Слепой прогон»: почему ваш IPS начинает стрелять по своим в первый же день Из жизни провайдеров: история одного факапа Линии влияния в многопролётных шарнирных балках: бесплатный веб-инструмент для проверки расчётов Я перевёл 200K строк JS на TS с Claude Code. Что прошло, что сломалось Telegram-бот, который молча скачивает видео по ссылкам в групповых чатах: как это сделать, не ломая приватность Три попытки обогнать в бенче базовую Gemma 4 дообучением — и все три мимо Создал свой генератор случайных чисел на потоках Как уйти в тень: Полный гайд по анонимным платежам от рублей до виртуальной карты в 2026 Реверс-инжиниринг, цифровой двойник и ESP32 — что эти трое забыли на производстве? Задачка со звездочкой Как я создал систему, которая знает меня лучше чем я сам Root в контейнере — это root на хосте? Разбираю особенности прав доступов в контейнерах Docker/Podman Ультимативный гид по Codex CLI: от первой установки до воркфлоу io_uring без розовых очков: 5 граблей, которые сожгли мне неделю, и где он реально быстрее epoll Я протестировал 8 VPN-сервисов в России в 2026 году. Вот честный результат Улучшаем поисковые подсказки — от retrieval к генерации Налоговая отказала в вычете НДС на 48 млн руб. по IT-услугам и аэросъёмке БПЛА. При чём тут майнинг-оператор BitRiver Сложный проект как трамплин: как остановка на полгода, новые роли и поддержка команды помогли разработчику вырасти Слишком много открытых файлов: лимит Linux, который валит прод в 3 часа ночи «Мы сегодня молодцы»: анатомия одного провального совещания Талант, удача или среда: что на самом деле влияет на успех Как мы научили ИИ-агента отвечать за свои слова: 10 000 сообщений, Венгерский алгоритм и немного магии Как оживить фото нейросетью — Где бесплатно оживить старое фото через ИИ в 2026 году? VBoxGuest для KolibriOS: архитектура и устройство драйвера Я торгую саженцами на 500 заказов в год без рекламы. Маркетплейсы убили бы мой бизнес Назирокодил утилиту на Kotlin для создания аккордов в любой тональности Помидор, которого нет: почему VLA-модели не понимают, что они держат NXS Universal Chart v3.1.0: умный autoRollout, новые subcharts и MCP сервер Промты для ИИ-фотосессии: 50 готовых промтов для нейросети и идей для фото в 2026 году Кто твой клиент, если клиента нет? Исповедь Internal PO в банковском автокредитовании Программирование блока питания АКИП-1160/6 Байты, нибблы, и подсветка: пишем свой TUI hex-редактор на Python С днём рождения, Хабр! Как я сходил на IT-дачу будущего «Пропал интернет — продажи встали»: популярные мифы и неудобные вопросы про облачные онлайн-кассы Как вредоносный код переписал мой Git-коммит и заразил десятки проектов и несколько рабочих машин Какие методы оценки персонала реально работают в 2026 году Худшее собеседование в моей жизни 14 лет Solar JSOC: кто стоит за защитой от киберугроз в крупнейшем коммерческом SOC страны Где в IT джуны получают больше всего и куда пойти учиться Healthchecks в Docker Compose для Laravel: как сделать так, чтобы сервисы запускались в правильном порядке Биокомпьютер из живых нейронов: что на самом деле построила FinalSpark Знания без практики — мертвы | Разница между «декларативной» и «процедурной» памятью у LLM Поднимаем Llama 3 в облаке: Ollama и Open WebUI SAST прямо в IDE: как Veai ищет уязвимости в Java/Kotlin-проекте и помогает их исправлять Почему мы до сих пор пользуемся Markdown? Архитектура безопасности во frontend-приложениях: Server Actions и защита данных в эпоху Next.js Torque — ваши сверхспособности для отладки k8s Антипаттерны Zabbix в крупной инфраструктуре: каталог базовых граблей Мёд, крабы и чипы ИИ фото и нейросети для создания картинок в 2026: ТОП-6 моделей для генерации реалистичной фотосессии с ИИ Горячо-холодно: как определить температуру бизнеса с помощью тепловой карты BPMSoft «Насколько вы контролируете то, из чего состоит ваш продукт?». Как и зачем проводить Open Source Analysis Дезагрегированный инференс LLM в Kubernetes: префилл, декодирование и планирование подов Как стать Go-разработчиком с нуля? Бесплатная программа обучения Разработка эмулятора NES на отечественном микроконтроллере К1921ВГ1Т predict_proba выдаёт 0.9 — но это не вероятность 90% OneClickRelease, или как мы ставим релизы одной кнопкой Ускорение INSERT/COPY в логической репликации PostgreSQL Полиморфные ссылки в PostgreSQL: три попытки помочь оптимизатору Ransomware: математический аппарат на службе зла Блеск и нищета SMM hh.ru Пишем универсальную глитч-машину Как не похоронить бизнес на старте: анатомия корпоративных конфликтов при учреждении ООО Как стиль общения может создавать карьерный тупик в ИТ Ответы с «деврел‑супервизии», вопрос восьмой: как держать веру команды и ЛПР, когда метрики шатаются Новинка: Прикладные API для искусственного интеллекта и Data Science Миграция с ingress-nginx: выбор нового контроллера Как мы «взломали» MasterSCADA4D: выкинули стандартные блоки и заставили SCADA работать на SVG Ожидание: сделать ИИ-примерочную обоев за 2 дня. Реальность: пришлось добучать свою модель на SD Как мы тестируем в Профи.ру: почему у нас нет пирамиды, зато есть ромб и матрица Об Open-source — спасителе человечества и kernel-сообществе пророке его… ТОП-10 сайтов мебельных магазинов: лучшие UX-решения и приемы юзабилити QSEAL: новый подход в резервном копировании средствами СХД Книга: «Windows Server 2022. Полное руководство по администрированию» Нейросети для работы с Excel: Выбираем ИИ для создания таблиц и написания формул Совместимость Test IT и RedOS: опыт автоматизации сборки, тестирования и сертификации RAG-Anything: Как собрать по-настоящему мультимодальный RAG Как я готовился к Certified Kubernetes Security Specialist (CKS) в 2026 году Я держал кафе 16 лет и кормил полгорода. Потом пришли зумеры и всё посыпалось Есть ли жизнь на фазе: откуда берёт энергию умный выключатель без подключённой нейтрали Go Computer. История удивительного планшета из 1992 года с графическим интерфейсом Экономия GPU-часов в 2,5 раза, уход ИИ в бэкенд и новые стандарты агентских систем: ML-дайджест Что скрывается за AI-стратегией SAP, Oracle и Palantir: зачем корпоративному ИИ семантическое ядро Почему RAG — фундамент любой AI-трансформации Персонализация как баг Одна на 9 команд: как я внедряла квартальное планирование в трайбе, который сопротивлялся переменам После ИИ писать код руками ощущается уже не как норма Языковые модели без машинного обучения Обмен через интернет между мобильными приложениями ТСД и 1С От плановых ремонтов к предиктивному обслуживанию: дорожная карта для главного инженера Параллельный импорт техники закрыли или нет? Юридический разбор
Как я 8 дней ловил утечку памяти в Nuxt 3 SSR, и несколько раз думал, что починил
vadimbydanov · 2026-05-28 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение7 мин

Охват и читатели115

Кейс

Всем привет. Я занимаюсь фронтендом в небольшой команде сервиса бронирования отелей. Расскажу, как 8 дней ловил утечку памяти на проде, несколько раз думал, что починил, и каждый раз ошибался. Последний фикс был не в нашем коде, а в патче Vue, который через неделю апстрим откатил как регрессионный. В результате мы остались на одной патч-версии без утечки; обычный minor/patch update теперь для нас не безопасен без проверки heap-снапшотами.

Наш стек: Nuxt 3.18 + Vue 3.5.x + TypeScript, SSR, Pinia, PM2 cluster, nginx перед Node. Обычный каталог отелей с тысячами SEO-страниц вида /oteli-v-{город}/{подборка}.

Вкратце

  1. В отчете Ahrefs тысячи 502 у ботов, у живых пользователей почти нет. Снаружи 502, изнутри 200 — смерть воркера в момент запроса.

  2. Первая причина: SIGABRT от V8 по забытому --max-old-space-size от старого сервера. Лимит подняли, краши прекратились, память продолжала течь.

  3. Дифф heap-снапшотов показал: в нашей связке Nuxt 3.18 + Vue 3.5.x watch() в setup() на SSR оседает в heap без очистки. Известные апстрим-issue Vue/Nuxt — задеть может не каждого, у нас совпало.

  4. Обернул клиентские watch в if (import.meta.client). Ошибки у пользователей почти исчезли, скорость утечки осталась прежней. Вотчи оказались главным источником GC-давления, но не объясняли основной рост RSS.

  5. Закрылось апгрейдом Vue до 3.5.31 (апстрим-фикс SSR scope cleanup) и снятием серверных useFetch/useMediaQuery вотчеров.

  6. В Vue 3.5.32 фикс откатили как регрессионный. Сидим на 3.5.31; следующий апгрейд Vue — только с повторной проверкой heap-снапшотами.

До и после

До, пик

После, прошло 2 недели

502 в час

2444

0-1

502/504 за день

50 000 за 3-4 дня

6

RSS воркера

до 2907 МБ, ротация ~50 мин

плоские 350 мб, аптайм 14+ ч

Рост RSS

65 мб/мин

пила GC, дрейф ±2 мб/мин

CPU в простое

27% (GC трешинг)

2%

Что видят боты

Началось всё с отчета в Ahrefs: 1670 ссылок на наш сайт с кодом ответа 502 Bad Gateway. Боты сканируют сериями и попадают в 5-10 секундное окно недоступности воркера; реальный пользователь обновляет страницу через пару секунд и получает 200.

Масштаб: 51 467 ошибок и забытый PM2-конфиг

Первая теория: тупо мало RAM. В dmesg запись OOM-киллера:

Killed process 1364988 (node) total-vm:36643296kB, anon-rss:1395632kB

Каждый воркер на прогретом приложении ест ~1 гб RSS, два воркера + nginx + система = потолок. На скорую руку поправил кластер до instances: 1 и max_memory_restart: 900M, заказал апгрейд сервера.

Через пару дней сервер апгрейднули до 4 CPU / 8 гб RAM. Сел разбирать прод заново — масштаб оказался другим. В nginx access.log: 51 467 ответов HTTP 502 за 3-4 дня, около 2100 ошибок в час. Топ URL — неожиданный: статический JS-чанк /_nuxt/Bwfv1ZSS.js (1835 хитов), /favicon.ico (490), / (485). Статика /_nuxt/ исторически проксировалась через Node, а не отдавалась с диска. Падает Node — отваливается вся статика.

pm2 describe показал стек падения:

node::OOMErrorHandler
  → v8::Utils::ReportOOMFailure
  → v8::internal::V8::FatalProcessOutOfMemory
  → Heap::PerformGarbageCollection
  → Runtime_StringBuilderConcat

Умирал процесс по SIGABRT: V8 в FatalProcessOutOfMemory по --max-old-space-size. Не SIGKILL от ядра (в dmesg/journalctl пусто), не SIGTERM от PM2.

Открываю ecosystem.config.cjs: сервер апгрейднули, а PM2-конфиг остался от 2-гигабайтной эпохи. Прогретое приложение ест ~800 мб, на параллельные SSR-рендеры остается ~400 — под нагрузкой кончаются. max_memory_restart: 1700M не успевал, V8 умирал по внутреннему лимиту раньше.

Фикс занял несколько минут:

instances: 2,
max_memory_restart: '4G',
node_args: [
  '--max-old-space-size=5120',                // 1200 → 5120 (почему так много — дальше)
  '--heapsnapshot-signal=SIGUSR2',
  '--env-file=/var/www/website/shared/.env',
],

Плюс /_nuxt/ в nginx переключил на отдачу с диска. Закешированные URL переживали краш через proxy_cache_use_stale, страдал только первый-холодный запрос на каждый уникальный путь — а их у ботов как раз тысячи.

SIGABRT-краши прекратились. 502/час упал до 0-1. Победа?

Память все еще течет

Снял бейслайн через 37 секунд после прогрева — 64 мб. Через 3.5 часа воркеры доходили до RSS 1.7 гб / heap_used 1.25 гб. Утечка в районе 400 мб/час на воркер. Кластер + поднятый лимит просто превратили краш каждые 15 минут в медленный рост — симптом спрятан, причина осталась.

Решил снять второй снапшот для диффа. kill -SIGUSR2 <pid> — и оба воркера падают по SIGABRT во время сериализации дампа (RSS 1.7 → 3.5 гб → OOM). По V8-блогу снапшот занимает ~2х от heap. У меня на конкретном окружении (Node 20, Nuxt SSR, heap_used ≈ 1.25 гб) пиковый RSS уходил в 4.63 гб — то есть ~3.7-5х.

По моим наблюдениям снимать дамп безопасно при heap_used ≤ 15% от лимита (на других heap-формах картина может быть другой). Поэтому --max-old-space-size и поднят с 1200 до 5120.

Дампы шли по 50-600 мб, Chrome DevTools такие не вывозит — написал два Node-скрипта: diff-heap.mjs (агрегирует ноды по constructor name) и who-retains.mjs (обратный обход графа ссылок до глубины 4).

Сигнатура утечки и retainer chains

=== TOP GROWERS BY constructor (delta_size desc) ===
delta_size   delta_count   key
 +12.83 MB   +152,919      object/Dep
  +4.59 MB    +37,605      object/ComputedRefImpl
  +2.01 MB    +32,850      object/RefImpl
+760032 B      +7,917      object/EffectScope
+448800 B       +5,100     object/ReactiveEffect
+285600 B       +5,100     closure/watchHandle

На 3 датапоинтах (64 → 154 → 234 мб) считаю не приросты, а отношения счетчиков. В окнах 0-15 и 15-40 мин они получились идентичные — нормирую на «единицу источника» (один утекший композабл/стор с 2 вотчами):

Счетчик

прирост на 1 инстанс источника

watchHandle

2

ReactiveEffect

2

RefImpl

~4.2

ComputedRefImpl

~3.4

EffectScope

~0.55

Dep

~11.6

Стабильные отношения = один и тот же источник штампует одну и ту же структуру. Это не случайный набор объектов, а связанный граф реактивности Vue — watch() без очистки scope. Эвристика, не доказательство, но повторения заметны.

who-retains.mjs для object/EffectScope (3318 нод): 49% через Object.scope <- Object.component (Vue-компоненты, не размонтированные после SSR-рендера), 15% через EffectScope.prevScope chain, 5% через nuxtApp.ssrContext.__watcherHandles.

Диагноз

В нашей связке Nuxt 3.18 + Vue 3.5.x SSR-teardown не диспозит EffectScope: вотчи в setup() компонента / composable / Pinia-стора оседают в heap. На сервере нет «размонтирования» — компонент отрендерился в строку, но watch держит его живым. Для любой Vue 3 + Nuxt 3-конфигурации это утверждать не буду, проверять стоит heap диффом.

Это известный апстрим-баг: nuxt#33705 (цифры репортера почти совпадают с моими, heap с 80 до 600 мб за 4 ч при 60 rpm), vuejs/core#5208 и др. по теме.

И тут трафик ботов перестает быть фоновой деталью и становится множителем утечки: тысячи уникальных URL (типа /oteli-v-sochi) = тысячи SSR-рендеров = тысячи неубранных вотчей.

Шаблон фикса и 6 волн правок

// Было:
watch(source, handler);

// Стало:
if (import.meta.client) {
  watch(source, handler);
}

В серверном бандле вотча после этого нет, семантика не теряется — это всё клиентские вещи (аналитика, скролл, document.title, login/logout).

Убрал все вотчи — а память течет ровно так же

Деплою новый фикс, снимаю замеры. Пик 502/504 в час — с 2444 до ~5. CPU воркера в простое — с 26-29% до 1.8-2.3%. SSR-ответ при heap 95% — раньше упирался в 60-секундный таймаут, теперь 4.3 с. И одна строка, которая все ломает: скорость роста RSS — была 65 мб/мин, и осталась 65 мб/мин.

Ошибок у пользователей стало в ~500 раз меньше, а скорость утечки не изменилась. Вотчи были не главным потребителем памяти, а главным источником GC-давления: тысячи живых ReactiveEffect/Dep заставляли V8 на каждом minor-GC обходить большой граф, 27% CPU воркера в простое уходило в сборку мусора, и под этим давлением SSR-склейка не укладывалась в 60-секундный таймаут nginx -> 504. Убрал вотчи -> граф маленький -> CPU 2% -> SSR в таймауте. А память по-прежнему текла: я закрыл последствие, не причину.


Серверные вотчеры и фикс в Vue 3.5.31

Новый дифф после волн показал, что прирост сместился. Топ растущих — не watchHandle, а:

+70.66 MB   array/
+34.05 MB   string
+12.83 MB   object/Dep
+10.19 MB   object/Link          <- внутренности реактивности Vue 3.5
 +7.25 MB   object/system / Context   <- SSR async context

Dep/Link — внутренняя doubly-linked-list реактивности Vue 3.5, system / Context — объекты асинхронного контекста SSR. Подозрение на фреймворк. Что попробовал:

1. useMediaQuery из VueUse на SSR заменил на статический shallowRef по ширине из user-agent.

2. useApi без useFetch. В asyncData.js:123 видно: useFetch строит key = computed(...) и передает watch: [...watchSources, _fetchOptions] в useAsyncData, плюс еще один watch(key, setImmediate, ...). Каждый useFetch на сервере = один-два вотча в том же паттерне утечки. На сервере хожу в API руками: написал createServerManualApi с ручным $fetch и shallowRef.

3. Бамп Vue до 3.5.31. В патчноуте фикс PR #14548: «fix(server-renderer): cleanup component effect scopes after SSR render». Из описания: scopes «skip the normal unmount path», в результате «scope-bound effects and cleanup callbacks persist beyond the request lifetime». Слово в слово мой диагноз. Бампнул — утечка закрылась. Точную долю каждого из трех поздних шагов (3.5.31 / useApi / useMedia) я не вычленял: снапшот после стабилизации уже не снимал.

А теперь поворот: в Vue 3.5.32 этот фикс откатили (PR #14674). #14548 звал scope.stop() на каждом EffectScope после SSR-рендера, а тот по контракту дергает onScopeDispose()-колбэки. В Vue есть негласный контракт «на SSR onScopeDispose не срабатывает», под него написана куча композаблов — после 3.5.31 клинап хуки начали стрелять на сервере. Узкая замена через unwatch context.__watcherHandles пока не вернулась.

И нюанс под наш случай: через __watcherHandles держалось только 5% утечки, основная часть (49%) через Object.scope <- Object.component. Узкий фикс для нас закроет малую долю. Нас спасала ровно «слишком грубая» #14548. Регрессия у нас не стреляет: своего onScopeDispose нет, VueUse-composables на сервере либо не создают ресурс, либо вычищены руками. Варианты при той же беде: остаться на 3.5.31, написать свой teardown в Nuxt-плагине или ждать узкий апстрим фикс с поправкой про неполноту для цепочек Object.scope <- Object.component.

Проверка через 2 недели

Утечка закрыта, прод спустя 2 недели: воркеры по 14 часов аптайма при ~350 мб RSS, рост за 60 секунд +5.9/−1.5 мб (пила GC), 502/504 за весь день — 6. Будь там утечка хоть на 30 мб/мин, давно уперся бы в max_memory_restart: 4G. С 2100 ошибок 502/час до 6 за день.

Что в итоге

  1. В нашей связке Nuxt 3.18 + Vue 3.5.x watch() в setup() SSR-компонента оставался в heap. Клиентские вотчи лучше явно убирать из сервер-бандла через if (import.meta.client). useFetch и useMediaQuery тоже могут создавать вотчи под капотом — если heap diff ведет туда, заменяйте на ручной $fetch и статические SSR-ветки без реактивности.

  2. Heap снапшот в момент сериализации может занять не ~2x от heap как в доке, а 5x — снимайте при heap_used ≤ ~15% от лимита.

  3. Сначала heap-diff + retainer chain, потом код. Гадать по исходникам — терять время.